From 37e9b9a798309e886efa897a12c51d6d5fc99afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 29 Oct 2026 19:45:50 +0100 Subject: [PATCH 01/24] plan: add: define HT A-MPDU reception stages Before this commit, the branch has no plan for HT aggregate reception or implicit Block Ack integration. After this commit, the plans define separate PHY and MAC duties, staged changes, required evidence, and acceptance limits. Both plans use the integrated master base. Their first stages define wire representation, bounded member ranges, and cached PHY outcomes. MAC acceptance remains separate. This commit changes no runtime behavior. Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: plan | behavior.add | - | ieee80211-ampdu-phy-reception --- ...ee80211-ampdu-implicit-blockack-summary.md | 374 ++++++++++ .../ieee80211-ampdu-implicit-blockack.md | 657 ++++++++++++++++++ plan/pending/ieee80211-ampdu-phy-reception.md | 408 +++++++++++ 3 files changed, 1439 insertions(+) create mode 100644 plan/pending/ieee80211-ampdu-implicit-blockack-summary.md create mode 100644 plan/pending/ieee80211-ampdu-implicit-blockack.md create mode 100644 plan/pending/ieee80211-ampdu-phy-reception.md diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md new file mode 100644 index 00000000000..3253f199433 --- /dev/null +++ b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md @@ -0,0 +1,374 @@ +# A-MPDU with implicit Compressed Block Ack: plan summary + +> **Kind:** reference · **Status:** draft · **Seal:** none · **Owns:** — · **Stands on:** [feature plan](ieee80211-ampdu-implicit-blockack.md), [PHY plan](ieee80211-ampdu-phy-reception.md) + +This plan proposes several independently acknowledged MPDUs in one HT transmission. The recipient sends one Compressed Block Ack after the transmission ends. The aggregate requests that response without a separate BAR. The originator retries only the MPDUs that remain unacknowledged. + +This summary describes the [feature plan](ieee80211-ampdu-implicit-blockack.md) and [PHY plan](ieee80211-ampdu-phy-reception.md). Both plans use `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. The MAC feature remains proposed. The diagrams describe the design, not executed simulation traces. + +Master contains the merged explicit Compressed Block Ack implementation from PR #1345. Rx supplies local interframe observations. The radio supplies metadata for Radiotap. The PHY topic must preserve these contracts. Only the final commit receives build and test execution. + +The PHY plan supplies aggregate reception before the MAC feature uses it. Its first gate proves deterministic member outcomes and includes the shared NIST/Yans implementation. It also preserves default HT operation and reception without an error model. Its second gate extends the NIST/Yans and compatibility evidence. The MAC feature can reach deterministic integration between those gates. + +The series defines PHY P0 through P4 and gates D1 and D2. The final execution record states verification and its limits. VHT identity, bounded inspection, deaggregation use, and HT-SIG representation have separate commit boundaries. P3 checks NIST/Yans probabilities and seeded outcomes. P4 supplies ordinary controls. MAC aggregate receipt and implicit Block Ack remain pending. + +The merge prerequisite is satisfied on the integrated master base. MAC step A0 must compare the contracts and record a feature-off baseline before MAC implementation. + +## What changes after S5 + +S5 is the stage that supplies explicit Compressed BAR exchanges for individual MPDUs. The originator sends those MPDUs in separate PPDUs. It later sends BAR to request their receipt status. The proposed path puts several MPDUs into one A-MPDU and requests receipt status through that aggregate. [Problem and intended behavior](ieee80211-ampdu-implicit-blockack.md#1-problem-and-intended-behavior). + +This illustration assumes an accepted agreement and three MPDUs. Both paths end with a Compressed Block Ack. The proposed path needs fewer separate PHY transmissions for those MPDUs. Protection, contention, response rates, and loss still affect goodput; the plan promises no fixed performance gain. + +```mermaid +flowchart TB + subgraph Current["Current S5: explicit request"] + direction LR + C1["DATA 100
One PPDU"] --> C2["DATA 101
One PPDU"] + C2 --> C3["DATA 102
One PPDU"] + C3 --> CB["BAR
One PPDU"] + CB --> CR["Compressed Block Ack"] + end + subgraph Proposed["Proposed: implicit request"] + direction LR + A["One A-MPDU in one PPDU
MPDUs 100, 101, 102"] --> R["Compressed Block Ack
after SIFS"] + end +``` + +An A-MSDU and an A-MPDU combine different units. An A-MSDU combines MSDUs inside one MPDU. An A-MPDU combines complete MPDUs, each with its own FCS. Separate receipt bits therefore permit selective retry within the aggregate. + +## What the aggregate contains + +One common PHY preamble and header serve the complete aggregate. Each MPDU retains its MAC header, payload, sequence number, and FCS. A delimiter identifies each MPDU boundary and length. The aggregate has no additional outer MAC header or outer FCS. [Representation and limits](ieee80211-ampdu-implicit-blockack.md#42-eligibility-and-limits). + +PHY P1 extends the modeled HT-SIG fields and their serializer together. HT-SIG is the HT signal field that carries PSDU length and the Aggregation property. Golden bytes and independent decode must preserve both values in the 48-bit header. A singleton must retain Aggregation equal to one after byte conversion. This work covers the modeled HT-SIG header without a new waveform model. [HT-SIG representation and verification](ieee80211-ampdu-phy-reception.md#41-representation-and-bounded-ranges). + +The radio already assigns that 48-bit header length. P0 records its current value and the mode/PSDU duration path. P1 completes byte conversion within the same length; ordinary HT controls must preserve airtime under identical inputs. Changed byte content alone establishes no airtime change or reason to update a fingerprint expectation. + +Correct bytes alone do not establish rejection of an invalid received header. P1 also defines one stateless HT-SIG validator. P2 applies it before member outcomes and before radio decapsulation trusts HT Length. Invalid CRC or unsupported fields cause HEADER failure without delimiter or member draws. The current header-failure paths prevent receipt updates, delivery, and responses. [HT-SIG receive validation](ieee80211-ampdu-phy-reception.md#ht-sig-receive-validation). + +A separate bounds check refuses modeled PSDU content shorter than a valid header's HT Length. HEADER retains success and the decoded Aggregation property. DATA reports failure without range outcomes or draws, and MAC refuses the content before header or trailer peeks. Decapsulation skips length-based trailer removal. This case proves bounds safety for malformed input. It establishes no physical carrier-loss behavior or acceptance of a received prefix. + +The diagram shows two members. Nonfinal HT subframes need alignment padding of zero to three octets. The final HT subframe has no alignment padding. Extra zero-length delimiters can provide the minimum MPDU start spacing that the receiver requires. + +```mermaid +flowchart TB + PHY["One common HT PHY preamble and header"] --> BODY + subgraph BODY["One PSDU: the A-MPDU"] + direction LR + D1["Delimiter
4 octets"] --> M1["MPDU 100
Header, MSDU, FCS"] + M1 --> P["Alignment padding
0 to 3 octets"] + P --> S["Zero-length delimiters
if spacing requires them"] + S --> D2["Delimiter
4 octets"] + D2 --> M2["Final MPDU 101
Header, MSDU, FCS"] + end +``` + +Each complete HT member must fit the 4095-octet delimiter limit. The whole aggregate must fit both the peer's advertised limit and the HT maximum of 65535 octets. Its airtime must also satisfy the PHY and TXOP limits. A byte count alone cannot establish that an aggregate qualifies. + +## Which traffic can use the first version + +The first transmit profile uses packet-level `Ieee80211Radio`, scalar or dimensional reception, HT mixed format, 20 MHz, and long GI. Aggregate transmission requires fixed DATA rates and no HCF rate-control submodule. Each aggregate serves one recipient and TID under an accepted immediate Compressed agreement. Compressed responses retain the 64-bit bitmap. Multiple peers and agreements can coexist; each retains separate state. [Supported profile](ieee80211-ampdu-implicit-blockack.md#first-supported-profile). + +Receive support also covers advertised HT20 short-GI modes at the permitted MCS and NSS values. Before the MIB update, the MAC supplies its exact mode set, effective channel widths, and NSS limit for receiver validation. The check covers advertised modes; unsupported catalog modes outside those limits do not cause rejection. A peer can select an advertised short-GI receive mode even when the local aggregate transmitter uses long GI. The new transmit policy remains limited to long GI. [Required receive coverage](ieee80211-ampdu-phy-reception.md#42-optional-role-and-shared-implementation). + +HCF coordinates eligibility and the exchange. Management and the MIB supply local and peer capabilities. The proposed HT policy selects members within those capabilities and the agreement's transmit window. The policy cannot grant an agreement or bypass a PHY limit. + +```mermaid +flowchart TD + P{"Does the transmitter select
the new HT aggregation policy?"} + P -->|No| OLD["Use the current individual-frame path"] + P -->|Yes| H{"Do the actual HT mode
and peer state qualify?"} + H -->|No| FALL["Apply current fallback rules
No A-MPDU transmission"] + H -->|Yes| A{"Is an immediate Compressed
agreement active for this peer and TID?"} + A -->|No| SETUP["Use current setup or fallback
No aggregate before acceptance"] + A -->|Yes| L{"Do members, spacing, window,
length, and airtime qualify?"} + L -->|No| SMALL["Reduce the batch or use
an eligible individual frame"] + L -->|Yes| TX["Prepare one aggregate exchange"] +``` + +The transmit feature requires an explicit policy choice. The default policy continues to select no A-MPDU. A receiver must recognize supported received aggregates even when its own transmitter uses the default policy. Absent peer HT state retains S5's Normal Ack fallback. + +The WLAN receiver checks role presence at PHY initialization. The MAC requests exact coverage validation later, before its MIB update at link-layer initialization. Absent roles and incomplete advertised coverage cause failure before management sends capability information. These checks apply even when local transmit aggregation remains disabled. Non-HT custom models retain ordinary MPDU reception without that role. [Early compatibility checks](ieee80211-ampdu-phy-reception.md#44-model-detail-and-diagnostics). + +HT length exponent zero still advertises 8191 octets; it cannot disable aggregate reception. A model that exposes the role but lacks advertised short-GI support therefore fails initialization. A one-antenna configuration must pass when the model supports every advertised mode. The catalog can contain unsupported modes for more spatial streams. The validation adds no second capability store or independent filter. + +BerTable remains outside the HT aggregate profile. Its current initializer rejects HT operation strings before the new role check. This delivery adds no aggregate role to BerTable. The migration guide directs supported HT configurations to NIST or Yans. The PHY fixture compares a non-HT parent control and verifies the current HT refusal separately from the custom-model role diagnostic. [Compatibility decision and checks](ieee80211-ampdu-phy-reception.md#5-affected-components-and-compatibility). + +An absent error model retains the current error-free abstraction after PHY acquisition, possibility, attempt, and SNIR checks. Successful common checks permit physical success for bounded delimiter and member ranges without new error draws. Delimiter, FCS, address, and agreement checks still control MAC acceptance. This case adds no replacement module or interface requirement. + +The proposed settings are: + +```ini +**.mac.hcf.isBlockAckSupported = true +**.mac.hcf.blockAckVariant = "compressed" +**.mac.hcf.rateControl.typename = "" +**.mac.hcf.rateSelection.dataFrameBitrate = 6.5Mbps +**.mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +**.mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +**.mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +``` + +`HtMpduAggregationPolicy` does not exist yet. The fixed rate above selects HT MCS 0 with one spatial stream under the required HT20 long-GI configuration. These settings also need compatible modes, usable peer HT state, and accepted agreements. An `ac` interface qualifies only through an actual supported HT transmission. It does not establish VHT A-MPDU support. [Configuration and compatibility](ieee80211-ampdu-implicit-blockack.md#5-affected-components-and-compatibility). + +## How partial loss leads to selective retry + +Consider this hypothetical exchange with MPDUs 100 through 103. The recipient receives 100, 102, and 103 correctly. MPDU 101 fails. The recipient reports those separate results in one bitmap after the complete PPDU and SIFS. + +```mermaid +sequenceDiagram + participant O as Originator HCF + participant R as Recipient PHY and HCF + participant U as Recipient upper protocol + Note over O,R: Accepted immediate Compressed agreement for one peer and TID + O->>R: One A-MPDU: 100, 101, 102, 103 + Note over R: 101 fails. Retain receipt evidence for 100, 102, 103. + R->>O: After SIFS: bitmap 100=1, 101=0, 102=1, 103=1 + Note over O: Release acknowledged MPDUs. Retain 101 with its original deadline. + O->>R: Singleton A-MPDU: retry 101 + R->>O: After SIFS: Compressed Block Ack + Note over R,U: Apply reorder rules. Deliver each MSDU exactly once. +``` + +The originator releases 100, 102, and 103 from its transmit store. It retains 101 until acknowledgment or terminal disposal. The retry preserves 101's sequence number and absolute deadline. A singleton A-MPDU still requests Block Ack. One member does not turn it into a standalone MPDU with an individual ACK. + +The wire ACK policy bits remain `00`. In a standalone MPDU, those bits ordinarily mean Normal Ack. In the supported aggregate context, they mean Implicit BAR. The PHY aggregation property and MAC exchange context preserve that distinction. [Implicit exchange and recovery](ieee80211-ampdu-implicit-blockack.md#47-originator-exchange-timing-and-recovery). + +## Why preparation must preserve the source frames + +The MAC must know whether the complete exchange fits before it removes source MSDUs from their queues. The QoS data service therefore creates previews first. HCF selects one DATA mode and checks the batch with that mode. A refusal leaves the source MSDUs, deadlines, and sequence counter intact. [Preparation and ownership](ieee80211-ampdu-implicit-blockack.md#43-preparation-commitment-and-ownership). + +The current fixed-rate rules select the DATA mode by recipient before the service calculates mode-dependent spacing and aggregate length. If padding makes the batch too large, HCF reduces it and checks the smaller batch at that same mode. The design adds no size-aware DATA rate-control API. + +HCF rejects aggregate transmit opt-in with a rate-control submodule, also when fixed-rate settings would bypass that controller's rate choice. AARF can change its rate and probe state after a member callback. Separate callbacks for one PPDU can therefore describe a probe at a rate that no transmission used. The first profile avoids that false result through fixed rates. Adaptive aggregate feedback needs a separate design; existing individual-frame configurations retain their current rate-control behavior. + +The proposed policy selects transmitted retries first, numbered unsent MPDUs second, and queued new members last. Both committed groups use cyclic sequence order; queued new members use queue order. This order is an INET choice. Previews of queued members use candidate numbers without counter advancement. Commitment validates that reference before actual assignment. Already numbered members retain their numbers and deadlines. + +An internal collision can cause the current HCF handler to extract and number one frame without a DATA transmission. The EDCAF that loses the collision retains its current single-frame recovery, including terminal disposal. Aggregate preparation accepts a retained numbered frame without another sequence allocation. Current acknowledgment state keeps Retry clear for its first DATA transmission. The feature plan's [source-state table](ieee80211-ampdu-implicit-blockack.md#43-preparation-commitment-and-ownership) defines that distinction. + +A1 supplies capability propagation and aggregate construction calculations for spacing, serialization, and member layout. A3 uses those calculations for dynamic member selection, spacing at the retained mode, and batch reduction. A3 also retains canonical member ownership. [Implementation responsibilities](ieee80211-ampdu-implicit-blockack.md#6-implementation-steps-and-dependencies). + +```mermaid +flowchart TD + Q["Source MSDUs remain in their queues"] --> P["Create previews
Select one DATA mode"] + P --> F{"Does a permitted batch fit?"} + F -->|No| KEEP["Preserve queue contents,
deadlines, and sequence state"] + F -->|Yes| PROTECT["Perform RTS/CTS if required"] + PROTECT --> START["At the existing Tx IFS boundary
Call HCF before actual submission"] + START --> V{"Are sources, agreement,
deadlines, and modes still valid?"} + PROTECT -->|RTS fails| RTSFAIL["Release unused previews
Apply one contention failure"] + V -->|No| CANCEL["Cancel the unsent aggregate
Create no DATA failure feedback"] + V -->|Yes| COMMIT["Commit new members
Assign their sequence numbers"] + COMMIT --> STORE["Transmit store retains
independent MPDUs"] + STORE --> FINAL["Finalize each member header
Calculate its own FCS"] + FINAL --> ENV["Aggregate envelope references
immutable member content"] + ENV --> TX["Transmit one PPDU"] + TX --> FREE["Release the envelope
Retain unresolved MPDUs"] +``` + +The transmit store, `InProgressFrames`, owns each committed MPDU until acknowledgment, expiration, or another terminal outcome. The envelope holds references to member content. Envelope disposal must not destroy the MPDUs that still need retry. Each MPDU retains its own lifetime; aggregate construction does not restart it. + +DATA submission means the actual call from Tx to the radio. Tx acceptance before SIFS leaves source MSDUs uncommitted. At its existing IFS timer boundary, Tx calls HCF before it copies or sends the PSDU. HCF validates the batch before the service commits new members. Tx then finalizes each member and submits the envelope within that synchronous call chain. Zero IFS uses the same callback immediately. + +HCF sets each member's Retry bit, ACK policy, and common Duration/ID. Tx applies the local TA and calculates each member's FCS after those header edits. The builder then constructs the delimiter envelope without an outer MAC header or outer FCS. Selective retry requires a fresh member FCS after any header change. [Member finalization](ieee80211-ampdu-implicit-blockack.md#43-preparation-commitment-and-ownership). + +If the originator inactivity timer retires the agreement during SIFS, the start callback refuses the cancelled preparation. No DATA reaches the radio, and no source receives a new sequence number. The frame sequence context retains the preparation until submission or cancellation. Reset cancels the pending Tx request before it releases that context. + +With an active agreement, aggregate RTS failure applies one contention update for new members, committed members, or both together. It adds no member retry increment, DATA attempt, DATA outcome, or pending probe. Queued sources remain queued; committed members retain their numbers, acknowledgment state, recovery state, and deadlines. The contention retry boundary resets access-category state without member disposal. Independent expiry or agreement retirement can still remove members. Current individual-frame RTS recovery and internal collision recovery retain their existing rules, including suppression of obsolete completion feedback. + +## Why the PHY needs separate member results + +The current receiver obtains a whole-data success decision. That decision cannot represent one failed MPDU beside three successful MPDUs. The plan adds separate delimiter and MPDU outcomes, with common PHY acquisition and header success. [Partial reception design](ieee80211-ampdu-implicit-blockack.md#44-phy-representation-and-partial-reception). + +```mermaid +flowchart TD + H{"Did PHY acquisition
and header reception succeed?"} + H -->|No| NONE["No valid aggregate content"] + H -->|Yes| E["Score every delimiter and MPDU
Leave alignment padding unscored"] + E --> C["Reuse PREAMBLE and HEADER decisions
Retain DATA outcomes and derive WHOLE"] + C --> D["Decode bounded delimiter chains
Preserve any continuity break"] + D --> V["Set each failed member Packet flag
Apply FCS and address checks"] + V --> GOOD["Valid members update
receipt and reorder state"] + V --> BAD["Invalid members cause
no receipt or success update"] + GOOD --> END{"At PPDU end, does a valid
addressed member request a response?"} + BAD --> END + END -->|Yes| BA["Send one Compressed Block Ack
after SIFS"] + END -->|No| SILENT["Send no Block Ack"] +``` + +The cache keeps separate entries for each receiver, transmission, and signal part. PREAMBLE preserves the current per-part checks without early HEADER or DATA calculation. HEADER reuses PREAMBLE before its own header calculation. DATA owns bounded range outcomes or structural refusal after successful header reception. A WHOLE query derives its summary from those entries without another error draw. Aggregate success queries must use those same entries. + +In separate-part mode, PREAMBLE or HEADER failure aborts the attempted reception before a container reaches MAC. Existing radio state notifications let HCF invalidate its physical history. Whole-signal mode can supply a failure container that MAC rejects before header or trailer parsing. Both paths prevent acceptance, responses, and range draws. The design preserves their existing event boundaries. + +The PHY scores every four-octet delimiter, also when its MPDU length is zero. It scores each nonzero MPDU range once. Alignment padding still contributes airtime but has no separate error draw. The model cannot represent errors confined to alignment padding. Header failure prevents all range draws. A failed spacing delimiter preserves a continuity break even if a later delimiter becomes recoverable. + +P0 checks cache insertion, replacement, cleanup, and direct success queries against the merged base. P2 checks PREAMBLE at its actual boundary before any HEADER or DATA calculation. Later HEADER, DATA, WHOLE, and repeated result queries must reuse their retained outcomes. The [PHY design](ieee80211-ampdu-phy-reception.md#43-decision-ownership-and-random-draws) defines that ownership. The aggregate result path preserves valid members despite a failed PPDU summary. + +With a present error model, aggregate results retain `ErrorRateInd` with all three probability fields set to `NaN`. The new range model supplies retained outcomes without an aggregate probability estimate. Result construction must preserve applicable power, SNIR, timing, mode, and channel metadata. Without an error model, the common base's existing probability tag presence and exact values remain valid. Ordinary MPDU metadata keeps its current calculations. [Aggregate metadata contract](ieee80211-ampdu-phy-reception.md#aggregate-error-rate-metadata). + +The tag contract permits `NaN` for unknown probabilities. P0 must audit readers, filters, and recorders that can receive aggregate metadata before P2 implements this contract. A reachable incompatibility blocks that implementation until the PHY plan records its resolution. Tag omission requires evidence that all affected consumers accept absence. The PHY fixture must verify the identified consumer paths before D1 closes. + +The [PHY plan](ieee80211-ampdu-phy-reception.md#42-optional-role-and-shared-implementation) defines an optional WLAN role for per-range probabilities and a pure mode-support query. A shared WLAN base implementation reuses the current length-based calculations for NIST, Yans, and compatible custom subclasses. P2 supplies this implementation and the MAC validation call before it enables the HT compatibility checks. The receiver checks the exact MAC capability inputs without a probability call or random draw. It calls the probability operations only for aggregates. An absent model follows the explicit error-free case. + +Deterministic tests supply probabilities of zero or one through the same role. The real receiver still produces and caches the outcomes. Those tests establish receiver integration and selective recovery. They do not establish the full NIST/Yans profile. + +The shared NIST/Yans implementation uses independent delimiter and MPDU outcomes under the current scalar SNIR reduction. This is a packet-level abstraction. It does not model correlated FEC errors or reconstruct interference within each MPDU. D2 extends the evidence for that implementation before completion of the documented profile. + +The decoder checks lengths, alignment, delimiter CRC, and signature before it trusts a member boundary. It can recover a later valid delimiter after corruption. That recovery does not erase the earlier continuity break. + +The decoder creates a separate Packet for each complete recoverable member. A failed PHY range sets only that member's bit-error flag. Rx rejects the flag before any FCS-mode branch. Other valid members retain their own outcomes and intact shared chunks. A valid FCS cannot override the member's error flag. [Receive validation](ieee80211-ampdu-implicit-blockack.md#45-receive-validation-and-one-response-per-ppdu). + +Rx reports one complete aggregate outcome for contention after all member checks. A correctly received member suppresses EIFS, also when that member addresses another station or lacks an agreement. A delivered container with no correct member invokes the existing corruption procedure once. Separate-part header failure retains the earlier radio abort. An individual member failure must not invoke HCF's ordinary corruption callback or erase valid aggregate history. The PHY's WHOLE summary cannot replace this MAC decision. + +## Why receipt and delivery need separate state + +Receipt state answers whether an MPDU arrived correctly. The reorder buffer answers when the recipient can deliver its contents. A positive Block Ack bit does not prove immediate upper delivery. Conversely, delivery must not erase receipt evidence that a later Block Ack needs. + +```mermaid +flowchart TD + V["Valid received MPDU"] --> S["Receipt record
Preserve its receipt bit"] + V --> R["Reorder buffer
Retain data until a release rule applies"] + S --> BA["Compressed Block Ack bitmap"] + S --> CHECK["Evaluate release conditions"] + R --> CHECK + FACTS["Member position
Delimiter continuity
Previous PPDU or response history"] --> CHECK + CHECK -->|Release required| U["Deliver eligible MSDUs once"] + CHECK -->|No release condition holds| HOLD["Retain buffered data"] + U -.-> KEEP["Delivery preserves receipt evidence
until scoreboard retirement"] +``` + +Aggregate release rules need more information than S5's individual-MPDU interframe check. The reorder service needs member position, delimiter continuity, prior receipt evidence, and the applicable physical interval. Rx will own the local history. HCF will pass copied results to the reorder service. The reorder service releases eligible data when an applicable condition requires that action. [Reorder release and history](ieee80211-ampdu-implicit-blockack.md#46-reorder-release-and-physical-history). + +The hypothetical loss of 101 does not imply that every later MPDU must remain buffered. Clause 10.25.6.6.2.1 requires release when an applicable condition in (a)(2)(i–iv) holds. Conservative gap retention cannot replace that action. Tests must prove each reachable condition and its required release action. Sequence arithmetic must also remain correct across 4095 and 0. + +## How timing constrains an aggregate + +HCF checks the complete exchange: optional RTS/CTS, required interframe spaces, the DATA PPDU, and Block Ack. The actual selected mode and delimiter padding determine DATA airtime. All members carry the same Duration/ID value, referenced to PPDU end. The PHY mode calculation remains the authority for airtime. + +RTS selection uses the complete PSDU length, including members, delimiters, alignment, and spacing. Clause 10.3.5 requires RTS/CTS when that length exceeds the threshold. The current policy also selects protection at equality, which the clause permits. HCF supplies an exact aggregate preview and a separate valid member header to that policy. Every batch reduction reevaluates protection and complete-cost admission at the same retained DATA mode. The preview reaches no radio and establishes no commitment or attempt. + +In a hypothetical case, a retained retry and a new preview each contain a complete 1500-octet MPDU. With zero extra spacing, the PSDU measures 3008 octets and exceeds a 2346-octet RTS threshold. HCF therefore selects RTS/CTS although each member falls below the threshold. If CTS does not arrive, HCF records one contention failure and releases the previews. The queued source stays queued; the retained member keeps its retry state and deadline. [RTS selection and recovery](ieee80211-ampdu-implicit-blockack.md#47-originator-exchange-timing-and-recovery). + +For a hypothetical later exchange, assume 200 microseconds remain in the TXOP. If the complete aggregate exchange needs 240 microseconds, HCF must reduce the batch or defer it. The source MSDUs remain available for a later attempt. A positive amount of time left does not prove that the exchange fits. + +| Condition | Proposed result | +| --- | --- | +| TXOP limit is zero. | Permit one A-MPDU DATA PPDU with its required protection and acknowledgment. Do not add a second DATA PPDU. | +| TXOP limit is positive and the aggregate contains multiple MPDUs. | The complete exchange must fit, even when every member is a retry. | +| A singleton retry can exceed the limit. | Apply the same-size retry exception only when its full conditions and TXOP history permit it. | +| An initial singleton carries an ordinary MSDU under the agreement. | Apply its separate exception only when the TXOP conditions permit it. A first-attempt A-MSDU cannot use that exception. | +| HT mixed-format duration reaches its L-SIG limit. | Require calculated `L_LENGTH` at or below 4095. For this profile, `TXTIME - SignalExtension` must not exceed 5484 microseconds. | +| HT PPDU duration reaches its separate PHY limit. | Also apply the HT limit of 10 ms independently of the L-SIG and TXOP rules. | + +For a hypothetical 5 GHz aggregate that needs 6 ms, the 10 ms check passes but calculated `L_LENGTH` reaches 4482. HCF must reduce the batch or defer it. The signal extension changes the corresponding total-duration boundary in 2.4 GHz. [HT duration conditions](ieee80211-ampdu-implicit-blockack.md#42-eligibility-and-limits). + +The first profile uses legal non-HT RTS/CTS and non-HT OFDM or ERP-OFDM Block Ack responses. The recipient first selects the highest permitted BSS basic rate at or below the actual HT PPDU's non-HT reference rate. If none qualifies, it selects the highest permitted mandatory rate at or below that same reference rate. A legal alternate must preserve response duration and modulation class. HCF must reject a forced incompatible control format before aggregate submission. + +The response deadline starts at PHY transmission completion. It covers SIFS, one slot, and the PHY receive-start delay. Once an eligible reception starts, HCF waits for reception end and validates the response. It does not replace that procedure with an arbitrary whole-response delay. [Timing and response rules](ieee80211-ampdu-implicit-blockack.md#47-originator-exchange-timing-and-recovery). + +## What happens when the exchange fails + +One aggregate produces one contention outcome, but it can produce different delivery outcomes for its members. A valid matched Block Ack completes the response exchange even when its bitmap contains no positive bits. Members without positive acknowledgment still need retry. An absent or invalid response produces one contention failure. + +| Event | Result for the proposed path | +| --- | --- | +| A valid bitmap acknowledges some members. | Release those members. Retain the other members with their original deadlines. | +| A valid bitmap acknowledges no members. | Complete the response exchange. Retain unresolved DATA for retry. | +| The implicit wait ends without a valid matched Block Ack. | Report one response-exchange failure. End the sequence and arm one explicit BAR probe before further DATA for that agreement. | +| DATA lifetime expires. | Dispose of the affected member through its current owner. Do not restart its deadline through another aggregate. | +| Valid addressed Implicit BAR DATA arrives without an agreement. | Discard the DATA. Send no Block Ack or individual ACK. That DATA triggers no DELBA or receive state. | +| A valid addressed implicit aggregate conflicts with an active Basic agreement. | Report the profile conflict before receipt or reorder updates. Do not silently send a Basic response. | + +Silent discard is the first profile's explicit policy choice. Clause 11.5.4 names Block Ack-policy DATA in its absent-agreement DELBA rule. This plan does not extend that rule to Implicit BAR DATA. Current explicit BAR and inactivity recovery retain their separate triggers. [Receive validation and absent-agreement recovery](ieee80211-ampdu-implicit-blockack.md#45-receive-validation-and-one-response-per-ppdu). + +Silence gives the originator no immediate agreement-loss notice. Finite batches already have a recovery trigger. After the last DATA expires, HCF checks transfer completion at a safe exchange boundary. It retires the accepted agreement when that peer/TID has no queued or in-progress DATA. This works with inactivity timeout zero. + +Continuous arrivals can keep DATA queued. The first failed implicit response wait therefore sets one boolean probe requirement in the current originator agreement. The BAR policy selects one real explicit Compressed BAR before further DATA for that agreement. Admission refusal preserves the flag. Actual submission consumes it. Current EDCA classification derives the probe's access category from the agreement's TID; no category is stored with the flag. + +The probe is INET policy beyond the standard's requirements. Clause 10.25.6.7.1 also permits direct DATA retransmission after a lost Block Ack. A submitted probe adds BAR airtime, channel access delay, and a response exchange or timeout. This cost also applies when the agreement remains healthy. Transfer completion can retire an empty agreement before probe submission. + +A valid probe response applies the current bitmap rules. Probe failure returns to DATA retry if the agreement remains active. It does not arm another probe by itself. For each agreement, at most one submitted probe occurs between DATA attempts. A valid zero-bit implicit Block Ack does not arm a probe. The probe adds no DATA attempt or duplicate member feedback. + +In this hypothetical case, fresh DATA continues to arrive, but the recipient lacks the agreement. It silently discards an implicit aggregate. The failed wait causes an explicit probe. The recipient answers that BAR with DELBA under S5's current rule. The originator retires its stale agreement and can establish a later agreement without queue drain. + +```mermaid +flowchart TD + FAIL["Implicit wait ends
No valid matched Block Ack"] --> ARM["Set one probe flag on the agreement
End the sequence and return to contention"] + ARM --> EMPTY{"Does this peer/TID
have queued or in-progress DATA?"} + EMPTY -->|No| DONE["Current transfer completion
Retire agreement and send DELBA"] + EMPTY -->|Yes| BAR["At the next permitted access
Submit one explicit Compressed BAR"] + BAR --> OUT{"What occurs after the probe?"} + OUT -->|Recipient DELBA| RETIRE["Current lifecycle handler
retires the stale agreement"] + OUT -->|Valid Block Ack| BITMAP["Apply transmitted-only bitmap
Permit normal DATA eligibility"] + OUT -->|No valid matched response| RETRY["Current BAR control recovery
Permit DATA retry before another probe"] + RETIRE --> SETUP["Current setup procedures
can establish a later agreement"] +``` + +The diagram describes the proposed recovery policy. The probe waits for Block Ack. DELBA uses the current Action path and can arrive after that wait. It still retires the agreement. + +A6 checks finite batches and continuous arrivals with zero and nonzero inactivity values. The finite case forces probe failure so DATA expiry reaches the current completion trigger. The continuous case verifies an actual BAR, recipient DELBA, and later setup while fresh DATA remains queued. [Recovery transitions and checks](ieee80211-ampdu-implicit-blockack.md#appendix-c-recovery-transitions-and-acceptance-cases). + +A separate A6 case keeps a healthy agreement active while selected implicit Block Ack responses fail. Full, partial, and absent probe responses must permit actual subsequent DATA transmission before another probe. Original deadlines, per-member feedback, and exactly-once upper delivery remain required. This controlled case establishes no general fairness or goodput bound. + +With an active agreement, valid Implicit BAR DATA refreshes recipient inactivity. A duplicate can refresh inactivity without another delivery. Invalid members cannot refresh it. The originator accepts positive bitmap bits only for applicable transmitted MPDUs. + +The originator validates received responses against the active receive interval, peer, TID, profile, and frame fields. Frames outside that interval and callbacks from retired local contexts cannot complete the wait. Compressed Block Ack has no identifier for an individual exchange. A hypothetical delayed frame with identical fields inside a later valid interval follows ordinary response validation. The plan promises no rejection based on a hidden link to an earlier exchange. Supported peers send their immediate responses after the required SIFS. + +Recovery and observations report each attempted member's actual outcome. The first aggregate transmit profile has no rate-control submodule, so member results cause no rate-control callbacks. Contention counters must not increase once for every member of the same failed exchange. Block Ack DATA retries remain bounded by the current lifetime policy; the plan adds no new member retry limit. General stop/start recovery remains outside this work. + +## How the work reaches a complete feature + +Two linked plans separate the receiver prerequisite from the MAC feature. The PHY plan owns representation, bounded delimiter parsing, cached outcomes, and error-model integration. The feature plan owns selection, independent member state, reception, mandatory release, responses, and recovery. + +D1 is the PHY plan's receiver integration gate. It includes deterministic reception evidence, the shared NIST/Yans implementation, and basic compatibility checks. Feature A2 accepts copied PHY indications and refusal behavior; A4 supplies MAC reception and required release. A5 connects complete exchanges, with the short-GI response check, and A6 verifies negotiation and recovery. None of those steps can omit A4. [Feature steps](ieee80211-ampdu-implicit-blockack.md#6-implementation-steps-and-dependencies). + +D2 is the PHY plan's broader NIST/Yans and compatibility evidence gate. Those checks can follow deterministic MAC integration because D1 already includes the implementation and preserves default HT operation. Feature A7 needs both A6 and D2 before the supported profile reaches completion. This split gives each delivery a clear evidence boundary. [PHY steps and gates](ieee80211-ampdu-phy-reception.md#6-implementation-steps-and-dependencies). + +```mermaid +flowchart TD + MERGE["Integrated master
Check the implementation contracts"] --> P1 + P1["PHY P1
Representation and bounded ranges"] --> D1 + P1 --> PREP["Feature A1 and A3
Capabilities and member preparation"] + D1["PHY P2 / D1
Receiver integration, shared models,
and basic compatibility"] --> ACCEPT + PREP --> ACCEPT["Feature A2 and A4
Accept outcomes and apply mandatory release"] + ACCEPT --> A5["Feature A5 and A6
Complete exchanges, negotiation, and recovery"] + D1 --> D2["PHY P3 and P4 / D2
NIST/Yans and compatibility evidence"] + A5 --> A7["Feature A7
Document and verify the supported profile"] + D2 --> A7 +``` + +The two plans now contain ten fixtures across unit, module, and protocol tests. The PHY plan owns three implemented fixtures; the feature plan owns seven proposed fixtures. P4 adds the parent-compatible ordinary reception fixture. The complete feature still needs source ownership, reorder release, exchanges, recovery, timing, and production negotiation checks. Current Basic, explicit Compressed, lifetime, reset, and disabled-feature checks provide regression coverage. Compatible configurations with disabled transmit aggregation must preserve current frame sequences and random draws. [Verification methods](ieee80211-ampdu-implicit-blockack.md#7-verification-methods-and-expected-results). + +Direct exchange assertions must prove one DATA PPDU, one response after SIFS, correct receipt bits, selective retry, and exactly-once upper delivery. Physical reception tests must produce the partial results through the real receiver. An injected bitmap cannot prove that PHY integration. Fingerprints can detect a changed trajectory; they cannot replace these assertions. + +The planned campaign also checks member flags under declared-correct and computed FCS modes. It covers 2.4 GHz and 5 GHz, HT MCS 0 and 7, legal response modes, and supported protection combinations. Boundary checks cover sequence wrap, buffer sizes 1, 2, and 64, expiration order, and independent peers and TIDs. Deterministic loss cases prove the causal paths. The small additional seed campaign establishes no statistical goodput claim. + +The corrected checks also cover production member FCS before and after selective retry, partial-reception EIFS suppression, and agreement removal during SIFS. Admission checks distinguish the HT mixed-format L-SIG condition from the separate 10 ms limit. A nonzero-spacing case proves that batch reduction retains one DATA rate decision. + +RTS checks use complete PSDU lengths below, equal to, and above the configured threshold. Spacing and batch reduction must cause renewed protection selection. Recovery checks cover mixed batches, numbered unsent members, internal collision survivors, and terminal collision disposal. Repeated aggregate RTS failure reaches the contention retry boundary without member retry increments, member disposal, DATA outcomes, or a pending probe. Probe checks derive ownership from the agreement's TID after member expiry. These cases remain proposed verification. + +PHY checks also prove HT-SIG golden bytes and independent decode, PREAMBLE query order, and the two failed-header delivery paths. Invalid CRC and unsupported fields cause HEADER failure with successful header probabilities and without an error model. A separate malformed-input case retains HEADER success but refuses insufficient modeled PSDU content at DATA. Metadata checks verify the aggregate probability fields and the applicable receive tags without another probability call or random draw. The PHY fixtures own these cases. None of these checks establishes physical carrier-loss behavior. + +The proposed MAC checks reject aggregate transmit opt-in with AARF. Proposed response checks separate observable mismatches and retired callbacks from indistinguishable valid frames inside an active interval. MAC FCS acceptance remains pending. + +Completed PHY checks reject absent aggregate roles and incomplete advertised receive-mode coverage before the MIB update. The PHY controls preserve ordinary HT reception with NIST, Yans, and an absent error model. Aggregate checks without an error model retain common PHY checks without new error draws. The PHY execution record states the direct evidence and its limits. + +A fixture peer supplies a valid HT20 short-GI aggregate through the real radio. D1 and A2 verify copied PHY indications without MAC receipt or response requirements. After A4 supplies member validation, A5 verifies MAC receipt and one Compressed response after SIFS. A6 verifies production capability negotiation with the local aggregate transmit policy at long GI. This receive case does not establish short-GI selection from production transmit queues. [Receive acceptance cases](ieee80211-ampdu-implicit-blockack.md#proposed-direct-checks). + +## What the first version leaves for later + +The first transmit policy selects ordinary MSDU members. It does not add nested A-MSDU selection. Current individual A-MSDU operation remains available, and reception preserves the current negotiated A-MSDU checks and deaggregation. + +The first version excludes these larger features: + +- VHT PPDUs, HT40, HE/EHT, Multi-TID, and multi-user aggregates. +- Short-GI aggregate transmission through the new HT policy. +- Delayed Block Ack, partial-state scoreboards, protected agreements, dynamic fragmentation, and group-addressed aggregates. +- Reverse direction, mixed control/data aggregates, adaptive aggregate transmission, new rate-control algorithms, and general MAC stop/start recovery. +- Bit-level FEC behavior, correlated error accuracy, errors confined to alignment padding, and a guaranteed performance gain. + +Custom data services, MPDU policies, Tx/Rx modules, response policies, and response-rate selectors can need interface updates. The DATA rate-control API remains unchanged; aggregate transmit opt-in requires its HCF submodule to be absent. Present custom HT models need the optional PHY role with coverage of advertised receive modes. This includes configurations that previously used only ordinary MPDUs. Non-HT custom reception and operation without an error model remain available without that role. The implementation must document those obligations. + +The design reuses the public Packet API and proposes no edit to its sealed implementation. Related feature branches remain separate integration work. [Compatibility and scope](ieee80211-ampdu-implicit-blockack.md#5-affected-components-and-compatibility). + +The two linked plans own the detailed decisions, source references, standard clauses, commands, and verification limits. The execution record supplies the completed PHY evidence. MAC aggregate acceptance and implicit Block Ack remain pending. diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack.md b/plan/pending/ieee80211-ampdu-implicit-blockack.md new file mode 100644 index 00000000000..242d8c3ace1 --- /dev/null +++ b/plan/pending/ieee80211-ampdu-implicit-blockack.md @@ -0,0 +1,657 @@ +# Implementation plan: A-MPDU with implicit Compressed Block Ack + +> **Kind:** procedure · **Status:** draft · **Seal:** none · **Owns:** — · **Stands on:** [PHY prerequisite plan](ieee80211-ampdu-phy-reception.md), [S5 execution record](../done/ieee80211-compressed-blockack-s5-execution.md), [implementation plan procedure](../../doc/project/guide/write-an-implementation-plan.md), [IEEE 802.11 rules](../../doc/project/domain/ieee80211.md) + +Checkout: `/home/user/omnetpp_ws/inet-ieee80211-ampdu-implicit-blockack`, branch `work/ieee80211-ampdu-implicit-blockack`. The implementation base is `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. + +Master contains the merged explicit Compressed Block Ack implementation from PR #1345. Rx owns local interframe observations. Future aggregate acceptance must update that history through Rx, without radio-medium timing queries. MAC implementation and expectation changes remain outside this PR. + +This plan defines PSDU-based RTS selection, recovery before DATA submission, and a boolean probe requirement with a derived access category. The PHY plan supplies the reception prerequisites. + +The project plan procedure remains a draft; this document uses its structure without an additional approval requirement. + +This feature plan consumes the [PHY prerequisite plan](ieee80211-ampdu-phy-reception.md). That plan owns aggregate representation, bounded delimiter parsing, cached partial reception, and error-model integration. This plan owns member selection, MAC reception, mandatory release, implicit responses, and recovery. + +Master contains the prerequisite Block Ack changes, so the merge prerequisite is satisfied. A0 must compare the MAC contracts and record a feature-off baseline before MAC implementation. + +The [delivery table](#6-implementation-steps-and-dependencies) identifies the stages and their prerequisites. The [fixture table](#7-verification-methods-and-expected-results) identifies acceptance evidence. Detailed standards and execution commands remain in the appendices. + +## 1. Problem and intended behavior + +The proposed feature sends multiple QoS MPDUs in one HT PPDU. The recipient returns one Compressed Block Ack after SIFS. The aggregate itself requests that response, so the successful exchange needs no separate BAR. Each MPDU retains its own sequence number, FCS, lifetime, and retry state. + +S5 supplies explicit Compressed BAR exchanges for individual MPDUs. It does not connect A-MPDU selection, aggregate reception, or the implicit response sequence. The current aggregate builder alone cannot provide those mechanisms. This plan adds the complete path through production queues, PHY reception, and acknowledgment recovery. + +This hypothetical case defines the main acceptance scenario. An immediate Compressed agreement exists for one peer and TID, with a transmit window that starts at 100. Four queued MSDUs become MPDUs 100 through 103 in one A-MPDU. The recipient receives 100, 102, and 103 correctly; MPDU 101 fails its FCS check. The recipient sends one Compressed Block Ack after the PPDU ends. The bitmap reports 100, 102, and 103 as received. + +The originator retains 101 for retry and releases the other transmitted MPDUs. A singleton A-MPDU can carry the retry of 101 and request another Block Ack. Each MSDU reaches the upper protocol exactly once. The reorder service applies every release condition that the supported aggregate path requires; an applicable release condition requires delivery across a gap. + +```mermaid +sequenceDiagram + participant Q as Originator queue and HCF + participant P as Recipient PHY and HCF + participant U as Recipient upper protocol + Note over Q,P: Accepted immediate Compressed agreement for one peer and TID + Q->>P: One HT PPDU: MPDUs 100, 101, 102, 103 + Note over P: 101 fails. Record valid MPDUs and apply reorder rules. + P->>Q: After SIFS: Compressed Block Ack, bits 100=1, 101=0, 102=1, 103=1 + Note over Q: Release 100, 102, 103. Retain 101 with its original deadline. + Q->>P: Singleton A-MPDU: retry 101 + P->>Q: After SIFS: Compressed Block Ack + P->>U: Deliver each eligible MSDU once +``` + +The diagram describes proposed behavior. It is not an executed trace. + +## 2. Current implementation and evidence + +The facts below describe the pinned master base. The [S5 record](../done/ieee80211-compressed-blockack-s5-execution.md) explains the explicit Block Ack contracts. This plan supplies no MAC execution result. + +| Component and source | Confirmed behavior | Required consequence | +| --- | --- | --- | +| [QoS data service](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc) | Lines 309–313 contain the commented A-MPDU invocation. Reversible preparation currently produces one MPDU or A-MSDU. | Extend preparation to a batch without premature queue extraction. | +| [Default MPDU policy](../../src/inet/linklayer/ieee80211/mac/aggregation/BasicMpduAggregationPolicy.cc) | The policy always returns `nullptr`. | Add an explicit HT policy; preserve the current default. | +| [Aggregate builder](../../src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.cc) | The helper concatenates MPDUs with delimiters and alignment padding. It deletes its inputs. | Retain canonical MPDUs separately for selective retry. | +| [Aggregate decoder](../../src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc) | The helper assumes valid lengths and derives names from the aggregate name. | Consume bounded parsing from the PHY plan. Apply MAC validation without name-dependent behavior. | +| [HT capability state](../../src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h) | Directional state retains the receiver's maximum A-MPDU length exponent. It omits minimum MPDU start spacing. | Carry spacing through local advertisement, peer state, and directional negotiation. | +| [Management conversion](../../src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h) | The wire element contains spacing, but conversion does not retain it. | Preserve all supported spacing values through real management paths. | +| [MAC receive entry](../../src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc) and [Rx](../../src/inet/linklayer/ieee80211/mac/Rx.cc) | Rx expects one MAC trailer and one MAC header. | Recognize an aggregate before the ordinary MPDU FCS and header path. | +| [Tx](../../src/inet/linklayer/ieee80211/mac/Tx.cc) and [its contract](../../src/inet/linklayer/ieee80211/mac/contract/ITx.h) | Tx finalizes one MPDU before SIFS, retains a copy, and sends that copy when its timer expires. | Add aggregate submission at the actual start, with per-member finalization and an explicit cancellation result. | +| [RTS policy](../../src/inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.cc) and [its contract](../../src/inet/linklayer/ieee80211/mac/contract/IRtsPolicy.h) | The policy reads the supplied Packet's length and a separate MAC header. Its current threshold comparison includes equality. | Supply the complete aggregate preview and a valid member header. Reevaluate protection after batch reduction. | +| [HCF recovery before DATA](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) | Internal collision recovery can number an unsent frame. RTS failure updates only contention for an uncommitted preview; committed frames also receive member retry accounting. | Include numbered, unsent candidates. Define aggregate RTS recovery independently of one representative member's state. | +| [Rx failure processing](../../src/inet/linklayer/ieee80211/mac/Rx.cc) and [HCF corruption callback](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) | Ordinary rejection notifies contention. Rx owns local interframe history and applies the acceptance result before MAC delivery. | Separate member rejection from the complete aggregate outcome for contention and history. | +| [DATA rate selection](../../src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc) and [rate control](../../src/inet/linklayer/ieee80211/mac/contract/IRateControl.h) | Current rate control selects a rate by recipient address. It takes no aggregate size. | Select one mode before mode-dependent spacing. Reuse the current recipient-based decision. | +| [AARF feedback](../../src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc) | Each callback can change the selected rate and probe state. The callback contains no actual transmission mode. | Separate callbacks for one aggregate can describe a probe at a rate that no member used. Restrict the first transmit profile to fixed rates. | +| [PHY receiver](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc) and [error model](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc) | The receiver obtains a whole-data success decision. The WLAN error model supports whole-Packet corruption only. | Consume cached delimiter and MPDU outcomes from the PHY plan. | +| [HT PHY header](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg) | The modeled HT header has no aggregation field. | Consume the PHY plan's HT aggregation property for both singleton and multiple-member aggregates. | +| [Frame sequence](../../src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc) and [acknowledgment handler](../../src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc) | `NORMAL_ACK` selects an ACK wait. An explicit BAR creates the current Block Ack wait. | Add an aggregate exchange with a Block Ack wait and no synthetic BAR. | +| [Reorder service](../../src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc) and [Rx history](../../src/inet/linklayer/ieee80211/mac/Rx.h) | Rx supplies local interframe evidence. HCF passes its result to the reorder service through one boolean. | Supply aggregate position, delimiter continuity, and previous PPDU facts for aggregate release rules. | +| [HCF transfer completion](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) | Lifetime expiry and exchange completion invoke `completeTransfers()`. An accepted agreement with transmitted DATA retires when its peer/TID has no queued or numbered DATA. | Name this finite-batch recovery trigger. Preserve transmitted-member records for aggregate submissions. | +| [QosAckHandler](../../src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc) and [BAR policy](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc) | Current BAR selection uses `BLOCK_ACK_NOT_YET_REQUESTED` members. It does not select failed implicit waits automatically. | Define an explicit probe trigger that also works while fresh DATA remains queued. | + +Prior local edits affect `AGENTS.md`, the replacement plan, its summary, the agreement lifecycle plan, and the EDCA lifetime plan. The S5 summary and planning history also exist as untracked files. This plan does not alter those files. The source revision above remains the implementation reference; local prose does not establish additional runtime support. + +## 3. Scope and acceptance criteria + +### First supported profile + +The first aggregate transmit profile targets packet-level `Ieee80211Radio` with scalar or dimensional reception, HT mixed format, 20 MHz, and long GI. Aggregate transmission requires a fixed DATA rate and no HCF rate-control submodule. The transmitter must select an actual HT mode. An `ac` interface can qualify only when its actual transmission uses this HT profile and valid peer HT state. This does not add VHT PPDU support. + +Reception also covers HT20 short GI when local HT capability advertises that receive mode. The PHY contract requires coverage of the advertised MCS and NSS values within the effective radio limits. Interface presence alone cannot establish that coverage. The MAC supplies its exact capability inputs to the receiver for validation before the MIB update. This receive requirement does not extend the new aggregate transmit policy to short GI. [PHY receive contract](ieee80211-ampdu-phy-reception.md#42-optional-role-and-shared-implementation). + +Each aggregate contains unfragmented, individually addressed QoS Data MPDUs for one RA, TA, and TID. An accepted immediate Compressed agreement must exist. Multiple peers and agreements can coexist; each aggregate remains within one agreement. The first transmit policy forms ordinary MSDU members. New selection of nested A-MSDUs remains separate work; current individual A-MSDU operation remains available. + +The recipient must preserve the current negotiated A-MSDU checks and deaggregation when it accepts an otherwise valid received member. The new transmitter must not use nested A-MSDU selection to bypass an absent peer length limit. An A-MSDU retry from S5 can retain the individual path. + +A-MPDU transmission uses a new `HtMpduAggregationPolicy` through the current optional `mpduAggregationPolicy` slot. The default `BasicMpduAggregationPolicy` retains its no-aggregation behavior. The receiver recognizes supported aggregates independently of its own transmit policy. No new on-wire capability bit or Block Ack profile selector represents this local choice. + +When no aggregate qualifies, use the current individual-frame path where its conditions permit transmission. Absent peer HT state retains S5's Normal Ack fallback. An enabled policy does not grant an agreement or override a PHY limit. + +The PHY plan rejects a present HT model without the aggregate role or required receive-mode coverage before management advertises capabilities. This check applies independently of local transmit aggregation. An absent error model retains the current error-free abstraction after the required PHY checks. The PHY plan's [compatibility section](ieee80211-ampdu-phy-reception.md#5-affected-components-and-compatibility) defines custom-model migration and the supported absent-model case. + +This work remains outside this plan: + +- VHT PPDUs, HT40, HE/EHT, Multi-TID, and multi-user aggregates. +- Short-GI aggregate transmission through the new HT policy. +- Delayed Block Ack, partial-state scoreboards, protected agreements, and dynamic fragmentation. +- Group-addressed aggregates, reverse direction, and mixed control/data aggregates. +- New stop/start recovery, general TXOP repair outside this feature, adaptive aggregate transmission, and rate-control algorithms. +- Bit-level FEC simulation, time-resolved error correlation, and a performance improvement guarantee. + +Current explicit Compressed and Basic exchanges remain supported. A valid explicit BAR still receives its current response. The new feature preserves the S5 profile conflict diagnostic and finite QoS DATA lifetime. + +### Observable completion + +1. Production association establishes the HT limits and immediate agreement before aggregate transmission. +2. One HT PPDU carries at least two independently identifiable MPDUs from production queues. +3. A valid member with ACK policy bits `00` causes one Compressed Block Ack after the complete PPDU and SIFS. +4. The successful implicit exchange contains no BAR and no individual ACK for its members. +5. Partial loss produces correct receipt bits and selective retry without duplicate delivery. +6. A singleton aggregate retry still receives Block Ack; MPDU count does not change its response kind. +7. Refusal, RTS failure, expiration, and agreement removal preserve or dispose of each owned frame through its current owner. +8. Aggregate length, member length, spacing, transmit window, PPDU duration, and TXOP conditions hold at actual transmission. +9. Compatible feature-off cases preserve current sequences and random draws. Separate peers and TIDs retain independent state. +10. Absent-agreement Implicit BAR DATA causes discard without a response to that DATA. Finite and continuous traffic cases prove recovery and later setup. +11. Deterministic integration and NIST/Yans support have separate evidence gates. Completion of the documented profile requires the PHY plan's D2 gate. +12. A failed implicit response wait arms one explicit BAR probe before further DATA for that agreement. Valid zero-bit Block Ack does not arm a probe. One boolean retains the requirement; current EDCA classification derives its access category from the agreement's TID. +13. Admission checks the L-SIG length condition for HT mixed format. The separate 10 ms HT duration limit also applies. +14. Production transmission calculates each member's FCS after its final header fields, including changes for selective retry. +15. A correctly received aggregate member prevents EIFS, an access delay after specified reception errors. This also applies to valid overheard members. +16. Agreement removal during SIFS cancels an unsent aggregate before queued-source commitment, new sequence assignment, or DATA attempt accounting. Existing canonical members retain their assigned numbers unless current disposal rules remove them. +17. Aggregate transmit opt-in with a rate-control submodule fails initialization, even when a fixed-rate override would bypass its rate selection. +18. Present HT models without the aggregate role or required receive-mode coverage fail before capability advertisement, also with local transmit aggregation disabled. +19. Response acceptance uses the active receive interval and observable frame fields. Retired local callbacks cannot complete another wait. +20. An absent error model preserves current PHY checks and adds no error draws. PHY D1 preserves default NIST and configured Yans operation. +21. The modeled HT-SIG header preserves HT Length and Aggregation through serialization and independent decode. +22. PREAMBLE queries add no early HEADER or DATA calculation. Failed-header delivery follows the radio's actual reception mode. +23. Aggregate results preserve applicable receive metadata and use the PHY plan's explicit probability contract. +24. Advertised HT20 short-GI aggregates reach MAC validation without an extension of the new long-GI transmit policy. +25. Invalid encoded HT-SIG prevents member outcomes, receipt updates, delivery, and responses through the current header-failure paths. +26. Insufficient modeled PSDU content causes structural DATA refusal without range draws. The valid HEADER result retains its decoded Aggregation property. +27. RTS selection uses the complete PSDU length, including delimiters and padding. Batch reduction reevaluates that decision before complete-cost admission. +28. Numbered, unsent MPDUs remain eligible without another sequence assignment. Aggregate RTS failure produces one contention failure without member retry increments or RTS-triggered member disposal. + +## 4. Proposed design and reasons + +### 4.1 Requirements that control the design + +IEEE Std 802.11-2024 controls the supported HT profile. [Appendix A](#appendix-a-standard-requirements-and-evidence) retains the detailed clauses, canonical corpus IDs, PDF pages, and extraction limits. + +The design preserves these conditions: + +- The actual HT aggregation property distinguishes a singleton A-MPDU from a standalone MPDU. +- The receive contract covers advertised HT20 long-GI and short-GI modes independently of the local transmit policy. +- HT-SIG CRC and field validation precede acceptance of Aggregation, HT Length, and member outcomes. +- Each accepted member needs valid delimiter context, a successful PHY outcome, and a valid FCS. +- One eligible aggregate requests one Compressed Block Ack after the complete PPDU and SIFS. +- Clause 10.25.6.6.2.1 requires release when an applicable condition in (a)(2)(i–iv) holds. Conservative gap retention cannot replace that behavior. +- Table 19-25 specifies an HT `aPPDUMaxTime` of 10 ms. Clause 10.13 applies that bound independently of TXOP admission. +- Clause 10.27.4 also requires the calculated HT mixed-format `L_LENGTH` to remain at or below 4095 octets. +- Clause 10.3.2.3.7 prohibits EIFS when at least one member of an A-MPDU arrives correctly. +- Clause 10.3.5 requires RTS/CTS when the complete PSDU exceeds the applicable RTS threshold in this HT profile. +- Clause 10.23.2.12.1 distinguishes ordinary frame retry accounting from Block Ack operation. It separately requires frame retry increments after internal collisions. + +The source assessment confirmed the commented A-MPDU call at `OriginatorQosMacDataService.cc:309–313`. PDF inspection confirmed the mandatory release text and the 10 ms table value. + +### 4.2 Eligibility and limits + +HCF decides whether the proposed exchange qualifies under the active profile, agreement, and actual PHY mode. The HT policy selects eligible members within those supplied limits. Management and the MIB remain the owners of advertised and negotiated capabilities. The policy does not read unrelated module parameters or invent peer capability state. + +Add minimum MPDU start spacing to local HT capability state and its directional receiver limits. Preserve its encoded value from 0 through 7. Extend equality checks, management conversion, full association, simplified management, and cleanup through the current capability path. Keep the current zero-spacing default. An asymmetric test must prove that the peer receiver's value controls local transmission. + +The aggregate must satisfy all applicable limits: + +- Each member's complete MPDU length fits the HT delimiter limit of 4095 octets. +- The complete PSDU fits the peer maximum and the HT maximum of 65535 octets. +- Member sequence numbers fit the active transmit window and negotiated buffer capacity. +- Alignment and zero-length delimiters meet the peer spacing requirement at the retained DATA mode. +- The selected mode's airtime satisfies the L-SIG length condition, HT PPDU duration bound, and applicable TXOP rule. + +The L-SIG field is the legacy signal field in the HT mixed-format preamble. Its `L_LENGTH` value tells non-HT stations when the PPDU ends. + +The mode API owns airtime. Extend the HT mode contract to expose both duration conditions for the aggregate admission check. Clause 10.27.4 requires `L_LENGTH = ceil((TXTIME - SignalExtension - 20 us) / 4 us) * 3 - 3` to remain at or below 4095. For the supported long-GI profile, this limits `TXTIME - SignalExtension` to 5484 microseconds. The signal extension is zero in 5 GHz and six microseconds in 2.4 GHz when applicable. Table 19-25's separate 10 ms limit also applies. + +In a hypothetical 5 GHz case, an aggregate needs 6 ms and otherwise fits the peer and TXOP limits. Its calculated `L_LENGTH` is 4482, so HCF must reduce the batch or defer it. The 10 ms check alone would accept an illegal HT mixed-format transmission. These are HT requirements; no VHT duration limit supplies either check. The current `getMpduMaxLength()` value does not define the member delimiter limit. + +### 4.3 Preparation, commitment, and ownership + +Extend the current reversible preparation mechanism to retain a batch of candidate MPDUs. New candidates remain in their queues until commitment. `InProgressFrames` retains committed candidates, including transmitted retries and numbered, unsent MPDUs. Preparation owns previews and copied membership keys; it borrows committed canonical members. It stores no second acknowledgment map or lifetime table. + +Retain current EDCAF arbitration and the first eligible peer/TID choice. The HT policy uses the selection order below within one agreement and its current transmit window. This order is an INET policy, not an HT requirement. Current acknowledgment state distinguishes transmitted retries from numbered, unsent candidates. + +| Selection order and source state | Preparation and commitment | +| --- | --- | +| 1. Eligible transmitted retries, in cyclic sequence order. | Borrow canonical members from `InProgressFrames`. Preserve their sequence numbers and original deadlines. Derive the Retry bit from current acknowledgment state. | +| 2. Eligible numbered, unsent MPDUs, in cyclic sequence order. | Borrow canonical members from `InProgressFrames`. Preserve their assigned numbers, recovery state, and deadlines. Keep Retry clear before their first DATA submission. | +| 3. Eligible new members, in queue order. | Retain source MSDUs in their queues. Previews use consecutive candidate numbers without counter advancement. Commitment verifies that reference before actual assignment. | + +The current internal collision handler can extract and number one frame before any DATA transmission. Preserve that single-frame recovery path, including its counter update and terminal disposal. It must not prepare or commit an aggregate for the EDCAF that loses the collision. A frame that remains after collision recovery retains `FRAME_NOT_YET_TRANSMITTED` in the acknowledgment handler. Aggregate preparation accepts it through the second row above without another allocation. + +In a hypothetical case, an internal collision assigns sequence number 100 to one frame but transmits no DATA. Later aggregate preparation selects that retained frame and a queued new member. Only the new member receives sequence number 101 at DATA submission. Member 100 retains its deadline and collision recovery state. Its first DATA transmission keeps Retry clear. + +HCF selects one actual DATA mode through the current fixed-rate DATA rules. Existing interface-wide or per-receiver rate settings supply that choice. HCF supplies the selected member's recipient and header context, without a delimiter envelope or a size-aware rate-control API. The service then calculates delimiter spacing and exact aggregate length at that retained mode. HCF can reduce the proposed batch without a second rate decision. A reduced batch requires another length and airtime calculation, not another rate selection. + +HCF rejects aggregate transmit opt-in during initialization when its optional rate-control submodule exists. This includes a controller whose rate selection a fixed-rate override would bypass, because HCF still sends feedback to that controller. AARF can change its probe state after the first member callback. A later callback for the same PPDU can then describe a probe at a rate that no transmission used. Fixed-rate aggregate transmission avoids that false result without a new feedback API or changes to existing rate algorithms. + +In a hypothetical AARF case, nine successes precede a two-member aggregate at rate R. The increase threshold is ten, and a faster rate exists. The first member succeeds, so its callback selects the faster rate and sets probe state. The second member fails at R, but its callback treats that failure as a failed probe at the faster rate. The fixed-rate profile rejects this controller configuration before any aggregate attempt. + +For a hypothetical peer with nonzero minimum spacing, a faster selected mode can require more zero-length delimiters between short MPDUs. HCF therefore obtains the mode before the service calculates that padding. If the padded batch exceeds a limit, HCF removes candidates and checks the smaller batch at the same mode. The selector receives no claim that the preliminary candidate size equals the final aggregate size. + +A new batch normally starts with two members. A batch with a transmitted retry can contain one member and retain aggregate response semantics. A numbered, unsent singleton can use the current individual-frame path. If no batch qualifies, the current individual-frame path can proceed under its own conditions. + +Here, DATA submission means the actual call that sends the aggregate from Tx to the radio. Acceptance of a delayed request by Tx does not commit source MSDUs. At actual submission, HCF checks agreement state, mode permissions, candidate membership, and complete-cost admission. The service checks queue identities, sequence references, and deadlines through their current owners. One synchronous commit assigns sequence numbers only to queued new members and transfers them to `InProgressFrames`. The commit preserves all previously assigned numbers and original deadlines. + +HCF sets each member's Retry bit, ACK policy, and common Duration/ID before Tx finalizes the member. Tx applies the local TA and configured FCS mode through a shared MPDU finalizer. For computed FCS, Tx calculates each member's FCS over that member's final header and payload. Header or trailer edits use copy-on-write before the new envelope takes references. The aggregate builder then combines those complete MPDUs with delimiters and padding. Tx sends that envelope without an outer MAC header, outer FCS, or another ordinary MPDU finalization pass. + +`InProgressFrames` owns canonical committed MPDUs until acknowledgment, expiration, or terminal disposal. The aggregate envelope owns delimiter chunks and references to immutable member content. Envelope destruction releases only its own content references. It does not destroy the canonical MPDUs. Region tags retain the correct member offsets and lengths. Aggregate names do not encode protocol identity. + +Extend the aggregate Tx contract with a start callback to HCF and a cancellation result. For delayed submission, Tx invokes that callback at its existing `endIfsTimer` boundary, before it copies or sends the PSDU. With zero IFS, Tx invokes the same callback synchronously. HCF validates the batch before the service commits it and Tx finalizes its members. Tx then submits the complete envelope to the radio in that synchronous call chain. Tx reports actual submission once so HCF can record each transmitted member. + +The frame sequence context owns the preparation until submission or cancellation. Agreement retirement marks an unsent preparation as cancelled. Tx clears its pending callback before it reports refusal to the frame sequence handler. Refusal ends the pending aggregate step without DATA failure feedback or an implicit-response probe. Reset must cancel a pending aggregate request through Tx before it releases the context. Ordinary synchronous calls consume no simulation time; this extension adds no timer, zero-delay event, or generation counter. + +Tx reports `hasTransmission()` while the aggregate request awaits its start or physical completion. It derives that result from the retained request or transmitted envelope. This preserves the current deferral of radio configuration commands. Commitment also preserves current queue notifications. + +In a hypothetical case, an originator inactivity deadline falls between CTS reception and the prepared aggregate's DATA start. The [current inactivity handler](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) retires that agreement through `beginTeardown()`. At the existing Tx timer event, HCF refuses the cancelled preparation. No aggregate reaches the radio, and no new source receives a sequence number. Independent expiry or teardown can still dispose of frames through their current owners. Section 4.7 defines aggregate RTS failure without commitment or member retry accounting. + +### 4.4 PHY representation and partial reception + +The [PHY plan](ieee80211-ampdu-phy-reception.md#4-proposed-design-and-reasons) owns the HT aggregation property and cached delimiter and MPDU outcomes. Its D1 gate supplies deterministic reception through the real radio and receiver. This feature consumes that result without a second error draw. Member count and transmit-only tags cannot replace the decoded HT property. + +PHY P1 also owns the modeled HT-SIG fields and serializer. Its wire fixture must prove HT Length and Aggregation through golden bytes and independent decode. A1 consumes that representation for singleton and multiple-member aggregates. The work covers the modeled HT-SIG header without a new waveform model. [HT-SIG representation](ieee80211-ampdu-phy-reception.md#41-representation-and-bounded-ranges). + +The radio already declares a 48-bit HT header. PHY P0 records this length and the current mode/PSDU duration path. P1 preserves the declared length through serialization and independent decode. Ordinary HT controls must preserve airtime under identical mode and PSDU inputs. Byte or fingerprint changes require separate evidence; they establish no necessary airtime change. + +PHY P1 defines one stateless HT-SIG validator; P2 applies it at the receiver's HEADER boundary. Invalid CRC or unsupported fields prevent delimiter and member draws. MAC receives no accepted Aggregation property from that header. Current header-failure paths prevent receipt updates, delivery, and responses. Encoded-header rejection needs separate evidence from probabilistic header loss. [HT-SIG receive validation](ieee80211-ampdu-phy-reception.md#ht-sig-receive-validation). + +The PHY separately bounds valid HT Length against the available modeled PSDU before range calculation or length-based decapsulation. Insufficient content denotes malformed model input. HEADER retains success and its decoded Aggregation property; DATA refuses the content without range outcomes or draws. The radio skips length-based trailer removal and supplies a failure container. MAC refuses that content before header or trailer peeks. This case establishes bounds safety, with no claim about physical carrier loss or acceptance of a received prefix. + +PHY P2 defines PREAMBLE, HEADER, DATA, and WHOLE decisions at their actual radio boundaries. PREAMBLE must not construct HEADER or DATA. A2 accepts the mode-specific failed-header paths and applicable receive metadata. The PHY plan owns the aggregate probability fields; MAC must not present an aggregate estimate as a member estimate. [Decision and metadata contracts](ieee80211-ampdu-phy-reception.md#43-decision-ownership-and-random-draws). + +PHY P0 must close the audit of aggregate metadata consumers before P2 implements its probability contract. The proposed `NaN` fields follow `ErrorRateInd`'s documented contract for unknown probabilities. A reachable incompatible reader blocks that implementation until the PHY plan records its resolution. Omission requires evidence that all affected readers accept an absent tag. Ordinary MPDU metadata retains its current behavior. + +The PHY plan keeps its optional WLAN contract separate from generic `IErrorModel`. Its shared WLAN implementation reuses the current per-length success calculation for NIST, Yans, and compatible custom subclasses. A present custom HT model outside that base also needs the aggregate role, even for ordinary MPDU operation. Its mode-support query must cover advertised HT20 long-GI and short-GI reception without a probability call or random draw. Non-HT custom models retain ordinary MPDU reception without that role. + +BerTable remains outside this HT aggregate profile and receives no aggregate role. Its current initializer already rejects HT operation strings before the proposed role check. The PHY plan owns its named compatibility decision, migration text, non-HT parent control, and expected HT refusal case. That current refusal supplies no evidence for the new custom-model diagnostic. [BerTable compatibility](ieee80211-ampdu-phy-reception.md#5-affected-components-and-compatibility). + +An absent error model follows the current error-free abstraction after PHY acquisition, possibility, attempt, and SNIR checks. The receiver records successful physical ranges without error-model calls or random draws only after those checks succeed. MAC delimiter, FCS, address, and agreement checks still control acceptance. An absent model needs no substitute role or module. + +D1 permits MAC integration and deterministic exchange checks. It includes the shared NIST/Yans implementation before the early HT check becomes active. D2 completes the broader NIST/Yans and compatibility evidence. A7 requires D2 before the feature claims the full NIST/Yans profile. The abstraction uses common PHY acquisition and header success with independent range outcomes under scalar SNIR reduction. It makes no claim about correlated FEC errors or interference within each member. + +### 4.5 Receive validation and one response per PPDU + +The MAC and Rx recognize aggregate context before the ordinary header and trailer peeks. MAC rejects a delivered failure container after common header failure before those peeks. That failure does not establish a decoded aggregation property. The shared decoder from the PHY plan bounds every delimiter, MPDU length, alignment interval, and zero-length delimiter against the PSDU. Each iteration must advance. Invalid input must cause a bounded refusal or resynchronization, never an out-of-range read. + +Separate-part PREAMBLE or HEADER failure aborts the radio's attempted reception before any container reaches MAC. Existing radio state notifications let HCF invalidate the relevant physical history. Whole-signal mode can supply a failure container that MAC must reject. Both paths prevent acceptance, responses, and range draws. Do not add a failure event solely to produce identical container delivery. + +Delimiter recovery scans valid alignment positions for a valid signature and CRC. It cannot use an unchecked length or the transmitter's private member list to bypass corruption. Record any broken delimiter chain even when a later member becomes recoverable. This distinction controls the reorder release rules. + +The decoder creates a separate Packet for each recoverable complete MPDU. Before it deletes the envelope, it projects the typed range outcome onto that member. It calls `setBitError(true)` on a failed member Packet. The [Rx FCS predicate](../../src/inet/linklayer/ieee80211/mac/Rx.cc) rejects that flag before declared-correct, declared-incorrect, and computed FCS branches. + +This uses the public Packet API without a false checksum or shared chunk mutation. Partial DATA failure must not mark the entire envelope incorrect or copy one global error flag onto every member. A failed delimiter still prevents member acceptance until valid bounded resynchronization. + +Rx validates each complete member through its current FCS predicate. A successful FCS check cannot override the member's PHY error flag. An invalid or unaddressed member cannot update receipt state, refresh inactivity, or trigger a response. Retain the applicable received NAV behavior for valid overheard members. + +The aggregate Rx entry must separate member checks from ordinary corrupt-frame callbacks. Rx records member rejection without an EIFS notification for each failed member. After all member checks, Rx reports one aggregate outcome for contention. If any member passes delimiter, PHY, and FCS checks, Clause 10.3.2.3.7 prohibits EIFS. This decision also counts valid overheard members and valid DATA without an agreement. With no correctly received member, Rx invokes the existing corruption procedure once for that PPDU. + +A partial member failure must not invoke `Hcf::corruptedFrameReceived()` or erase valid aggregate history. HCF receives the aggregate context with its member failures and delimiter facts. Common PHY failure still invalidates history through the current owner. In the main scenario, failed MPDU 101 causes neither EIFS nor loss of valid history for 100, 102, and 103. A separate all-member-failure case must exercise the existing corruption procedure without a Block Ack response. + +HCF processes valid members in receive order with explicit aggregate context. That context identifies PPDU membership, member position, delimiter continuity, and the valid member. The ordinary `NORMAL_ACK` path must not send individual ACKs for these members. The agreement handler records the valid DATA under the agreement even though its policy bits equal `00`. + +Validate the shared address, TID, policy, and Duration/ID conditions before agreement or delivery updates. A malformed context cannot produce success. For a valid addressed implicit aggregate under an active Basic agreement, report the profile conflict before receipt or reorder updates. Do not send a Basic response or silently reinterpret the implicit request. An absent agreement follows the separate recovery policy below. + +After the entire PPDU, HCF requests one immediate Compressed Block Ack when at least one valid addressed member qualifies. The response procedure uses the updated recipient scoreboard and its actual window start. Delivery does not erase receipt evidence. If all members fail, the recipient sends no Block Ack. + +For valid addressed Implicit BAR DATA without an agreement, the recipient discards the DATA. It sends no Block Ack or individual ACK. That DATA also triggers no DELBA, receipt state, reorder state, or upper delivery. Clause 11.5.4 explicitly names Block Ack-policy DATA in its absent-agreement DELBA rule. This first profile does not extend that rule to Implicit BAR DATA. + +Silence leaves the originator without immediate agreement-loss information. Finite batches already have a recovery trigger in [HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc). The lifetime timer expires DATA and invokes `completeTransfers()` when no exchange runs. Exchange completion also expires DATA and invokes that check. An accepted agreement with transmitted DATA retires when its peer/TID has no queued or numbered DATA. + +That trigger works with inactivity timeout zero. The new aggregate submission must retain each actual member's `recordTransmittedData()` update. This preserves the agreement's first-transmission condition and teardown boundary. HCF then uses its current originator-role DELBA procedure. Later DATA uses the current Normal Ack and ADDBA procedures. + +Continuous arrivals can prevent the empty-transfer condition. A failed implicit response therefore arms the explicit probe in [Section 4.7](#47-originator-exchange-timing-and-recovery). The recipient retains silent discard for implicit DATA. A later explicit BAR without an agreement retains S5's recipient-role DELBA response. + +In a hypothetical case, the recipient loses its agreement while fresh DATA continues to arrive at the originator. The recipient silently discards an implicit aggregate. The failed response wait arms one probe, which takes priority over further DATA for that agreement. The recipient receives the explicit BAR and sends DELBA. The originator retires its stale agreement and can establish a later agreement without queue drain. + +With an active agreement, valid Implicit BAR DATA refreshes recipient inactivity under Clause 11.5.4. A duplicate can refresh inactivity without a second delivery. Invalid members cannot refresh it. Preserve the current S5 behavior for an explicit BAR without an agreement. + +### 4.6 Reorder release and physical history + +Replace the single interframe boolean at the aggregate call boundary with the facts that the release decision needs. Extend Rx's local reception and transmission observations for real PPDU intervals. Rx will retain prior aggregate delimiter facts and completed Block Ack history. HCF will pass the copied result to the reorder service. The reorder service remains the owner of delivery decisions. This proposal uses no radio-medium timing query. + +Implement the applicable conditions in 10.25.6.6.2.1(a)(2)(i–iv), as well as the current individual-MPDU condition (v). The checks must distinguish a first member, a later member, and a standalone MPDU. A zero-length spacing delimiter does not become a DATA member. The receipt bit immediately before the scoreboard start and the required delimiter chain remain separate conditions. + +These are mandatory conditional actions within the supported HT-immediate profile. The clause states that the record `shall be modified as follows` before its release instructions. The [history table](#release-history-required-by-the-supported-profile) maps the fields to those conditions. Retain each history field only for a reachable release condition. A deferred condition needs evidence that the declared profile cannot reach it. + +Clear the relevant history after unrelated or abandoned receptions. Clear it after PHY acquisition or header failure. Receptions that overlap and incompatible local transmissions also invalidate the relevant history. A failed member alone does not erase valid PPDU history. Preserve its failure and delimiter facts for the applicable release check. + +A completed Block Ack can establish the history that the aggregate rules permit. No RIFS transmit scheduler enters this change. Reuse the current HT/BSS RIFS permission checks when a received sequence requires them. + +Tests must distinguish receipt from delivery. A positive Block Ack bit proves reception; it does not prove immediate upper delivery. No test may impose unconditional gap retention where an applicable aggregate release rule requires delivery. + +### 4.7 Originator exchange, timing, and recovery + +Add an aggregate transmit step followed by a Block Ack receive step. The exchange context identifies the peer, TID, transmitted member keys, and expected response kind. It reuses the current frame sequence timer. It does not construct an unsent BAR to obtain a timeout or response mode. + +The RXSTART deadline follows Clause 10.23.2.2: SIFS plus slot time plus the PHY receive-start delay, measured from PHY transmission completion. After an eligible reception starts, the step waits for its reception end and validates the response. Reuse the current HCF start/end reception mechanism. Do not replace it with an arbitrary whole-response delay. + +After actual aggregate submission, record every transmitted canonical member's allocation index through the current agreement owner. Queue commitment alone does not establish transmission. This preserves first-DATA tracking and the monotonic teardown boundary. + +All members use wire ACK policy bits `00`. The aggregate context determines that these bits mean Implicit BAR. The acknowledgment handler sets a Block Ack wait for each submitted member. Ordinary standalone MPDUs retain their current policy interpretation. + +Factor response-rate selection and response construction so explicit and implicit callers share the legal Compressed response rules. The implicit caller supplies the actual aggregate PPDU mode and valid response context. It must not pass a fabricated BAR or the mode of an unrelated member. Prediction and actual response selection must agree on legal format, rate, and airtime. + +The initial profile has no STBC or HT Control conditions that permit an HT/VHT control response. Block Ack therefore uses non-HT OFDM or ERP-OFDM under Clause 10.6.6.1. Choose the highest permitted BSS basic rate at or below the actual HT non-HT reference rate. If none qualifies, use the highest permitted mandatory rate at or below that same reference rate. Clause 10.6.6.5.2 applies this upper limit to both choices. Retain S5's explicit fallback for unknown BSS rate state. + +An alternate must preserve the primary response duration and modulation class under Clause 10.6.6.5.4. In a hypothetical HT MCS 0 case, no BSS basic rate qualifies. The available mandatory OFDM rates are 6, 12, and 24 Mbps. The non-HT reference rate is 6 Mbps, so the recipient selects 6 Mbps. A 24 Mbps fallback would exceed the reference rate. + +For aggregate protection, use legal non-HT RTS and CTS. A forced incompatible control format must prevent aggregate submission with a clear diagnostic. It must not create a new control-format violation. This restriction applies to the new profile; general control-response repair remains separate work. + +Clause 10.3.5 requires RTS/CTS when the complete PSDU exceeds `dot11RTSThreshold` in this HT profile. The complete length includes every member, delimiter, alignment interval, and zero-length spacing delimiter. A single member's length cannot supply that decision. Equality does not require protection under that clause, but the current RTS policy also selects protection at equality. Retain that permitted policy choice and ordinary-frame behavior. + +After mode-dependent spacing, preparation constructs an aggregate preview with the exact PSDU length. HCF passes that preview and a valid selected-member header to the current RTS policy. The preview supplies length; the separate header supplies frame kind and receiver address. The preview uses copied member content before finalization and carries no outer MAC header. It establishes no commitment or transmission attempt and never reaches the radio. Every batch reduction rebuilds that preview and reevaluates protection before complete-cost admission at the same retained DATA mode. + +In a hypothetical case, one retained retry and one new preview each contain a complete 1500-octet MPDU. Zero extra spacing gives a complete PSDU length of 3008 octets. With an RTS threshold of 2346 octets, HCF requires RTS/CTS although neither member exceeds that threshold. If CTS does not arrive, HCF records one contention failure. It discards the unused previews without commitment of the queued source. The retained member keeps its retry state and deadline. + +HCF checks the complete aggregate exchange before transmission: optional RTS/CTS, required interframe spaces, DATA PPDU, and the expected Block Ack. The retained mode and exact delimiter padding control DATA airtime. All members receive the same Duration/ID value, referenced to PPDU end. Recipient response transmission retains the current local NAV choice. + +A zero TXOP limit permits one A-MPDU DATA PPDU with its required protection and acknowledgment under Clause 10.23.2.9. It does not permit a second DATA PPDU. For a positive limit, every multi-MPDU aggregate must fit, even when all members are retries. The same-size retry exception can apply to a singleton aggregate only when no other DATA or management frame enters that TXOP. An initial singleton ordinary MSDU under the agreement can use its specified exception. A first-attempt A-MSDU cannot use that exception. + +Validate Block Ack addresses, profile, TID, length, fragment field, and bitmap before success. Reuse S5 modulo-4096 mapping and its rule that positive bits apply only to transmitted MPDUs. A partial bitmap releases only positively acknowledged canonical members. Absent or invalid responses end the batch wait without false success or inactivity refresh. They arm the probe before DATA retry. + +Response reception must satisfy the active frame sequence's receive interval and expected peer, TID, and profile. A response outside that interval cannot complete the wait. A callback from a retired local context cannot complete a later wait. The supported peer sends each immediate response at the required SIFS boundary. No additional exchange identifier exists in the supported Compressed Block Ack format. + +In a hypothetical injected case, an old probe response enters a later active interval with identical addresses, TID, SSN, and bitmap. Observable fields cannot distinguish that frame from a valid response to the later exchange. Apply normal response validation and transmitted-only bitmap updates; do not promise rejection based on its hidden origin. This profile adds no simulator-only correlation tag or replay detector. + +Use DATA retransmission for unacknowledged members, as permitted by 10.25.6.7.1. A singleton A-MPDU can carry the last retry member. An absent or invalid implicit response first causes one explicit Compressed BAR probe. A valid Block Ack with zero positive bits completes the response exchange and does not arm that probe. + +The required probe is an INET policy choice beyond the standard's requirements. Clause 10.25.6.7.1 also permits direct DATA retransmission after a lost Block Ack. This version retains one probe after the first failed implicit response to expose a stale agreement under continuous traffic. Each submitted probe adds BAR airtime, channel access delay, and a Block Ack response exchange or timeout. A healthy agreement incurs that cost too. Transfer completion can retire an empty agreement before probe submission. + +When the implicit wait ends without a valid matched Block Ack, HCF sets one pending-probe flag on the current originator agreement. It ends the sequence and returns to contention. The BAR policy prioritizes that peer/TID's probe before further DATA for the same agreement. Other eligible peers and TIDs can continue. + +Represent the pending state with one boolean in the current originator agreement. HCF derives its EDCAF from the agreement's TID through current EDCA classification. BAR selection compares that derived category with the candidate EDCAF. Only that EDCAF can submit or count the probe. The fixed TID mapping needs no retained category or second mapping table. + +The pending flag survives member expiry and admission refusal until submission or agreement retirement. This state is necessary because the next contention grant occurs after the failure. The flag belongs to the current agreement; add no separate recovery map, failure counter, or timer. Empty-transfer completion takes priority over an unsent probe. + +The probe uses the agreement's current transmit-window start and the actual explicit Compressed BAR exchange. It bypasses ordinary BAR threshold deferral. The acknowledgment handler attaches retained transmitted members within its bitmap range to that BAR wait. Selection must not depend only on `BLOCK_ACK_NOT_YET_REQUESTED`. + +Actual probe submission consumes the pending flag. A valid probe response applies the ordinary transmitted-only bitmap rules and permits eligible DATA retry. A failed probe returns to DATA retry through current control recovery if the agreement remains active. It does not arm another probe by itself. For each agreement, at most one submitted probe occurs between DATA attempts. + +A valid recipient-role DELBA uses the current lifecycle handler to retire the agreement. Reset and teardown clear an unsent probe with its agreement. An accepted probe retains its physical completion context under S5's retirement rules. [Appendix C](#appendix-c-recovery-transitions-and-acceptance-cases) defines the transitions and direct acceptance cases. + +Recovery counters and rate-control feedback describe different outcomes. A valid matched Block Ack completes one successful response exchange, even when its bitmap has no positive bits. This follows the response-based success definition in Clause 10.23.2.2. Unacknowledged members still need retry. An absent or invalid response produces one contention failure, not one failure per member. Preserve the current HCF choice to end the sequence and return to contention after response failure. + +Extend the recovery owner's API to separate per-member attempt counts from access-category contention updates. Do not call `dataFrameTransmissionFailed()` once per member: that method also changes station counters and CW. Use current member keys and count storage where their meanings fit. Keep Block Ack DATA retries subject to the current lifetime bound; add no new member retry limit. + +For an active agreement, aggregate RTS failure before DATA submission applies one ordinary control contention failure through the current recovery owner. This rule covers batches of new members, committed members, and both together. It adds no member retry increment, DATA attempt, DATA outcome, or pending probe. Queued sources remain queued; committed members retain their numbers, acknowledgment state, recovery state, and deadlines. Preparation releases its previews. Expiry and agreement retirement remain independent disposal causes. + +An aggregate RTS failure does not invoke the current per-frame RTS retry limit or discard a representative member. At the access-category retry boundary, the recovery owner applies the current contention reset without member disposal. This aggregate policy extends the current uncommitted-preview path to every aggregate composition. Clause 10.23.2.12.1 requires associated-RTS frame retry increments for DATA outside a Block Ack agreement; it does not require them here. Current individual-frame RTS recovery and internal collision recovery retain their existing counter and disposal rules. Current lifecycle resolution suppresses obsolete completion feedback. + +Record each attempted member's outcome through the current recovery and observation owners. Positive bitmap members report success; other attempted members report failure and retain eligibility until expiry or other current terminal disposal. Report terminal disposal once. The first aggregate transmit profile has no rate-control submodule, so these outcomes cause no `IRateControl::frameTransmitted()` calls. Current individual-frame configurations with rate control retain their existing callbacks. Adaptive aggregate feedback requires a separate design that preserves the actual PPDU mode across all member results. + +A probe adds no DATA attempt or duplicate per-member feedback. Its own contention outcome follows ordinary BAR accounting. + +### 4.8 Reuse and justified additions + +These choices apply [minimal design](../../doc/project/rule/quality.md#qr-design-minimal), [reuse](../../doc/project/rule/architecture.md#ar-ext-reuse), and [contract purity](../../doc/project/rule/architecture.md#ar-org-contract-purity). + +| Addition or extension | Required owner and consumers | Why current state is insufficient | +| --- | --- | --- | +| `HtMpduAggregationPolicy` and supplied selection limits. | QoS data service and replaceable policy. | The default policy deliberately selects no aggregate. An explicit HT policy preserves that default. | +| Batch preparation and aggregate transmit step. | Data service, `InProgressFrames`, and frame sequence context. | One-MPDU preparation cannot retain independent members and one PPDU response wait. | +| Aggregate start callback and cancellation result. | Tx calls HCF through the retained frame sequence context at the existing IFS boundary. | Current Tx copies an MPDU before SIFS and cannot revalidate an aggregate at its actual start. | +| Shared MPDU finalizer and aggregate Tx entry. | Tx finalizes members before the builder constructs the envelope. | Ordinary Tx processing assumes one header and one FCS. It cannot finalize a delimiter envelope. | +| Complete aggregate outcome for contention. | Rx supplies one outcome after member validation; HCF retains member history. | Ordinary rejection would invoke EIFS and clear history for an individual failed member. | +| HT aggregation property and receive indication, owned by the PHY plan. | WLAN PHY supplies values to MAC receive entry and HCF. | Policy bits and member count cannot distinguish standalone DATA from a singleton aggregate. | +| Cached aggregate reception, owned by the PHY plan. | WLAN receiver and current medium cache supply the result to MAC. | Whole-Packet corruption cannot report partial reception or delimiter continuity. | +| Aggregate reorder context and history extension. | HCF and reorder service. | A single interframe boolean cannot represent the additional mandatory release conditions. | +| Shared Compressed response source context. | Rate selection, recipient Block Ack procedure, and receive step. | Current public calls require an explicit BAR, which an implicit exchange does not transmit. | +| Aggregate preview for RTS selection. | Preparation supplies exact PSDU length; the current RTS policy uses its separate member-header argument. | One member's length can omit required protection. The existing callable contract needs no outer aggregate MAC header or second threshold selector. | +| One pending-probe flag in the current originator agreement. | HCF and the BAR policy retain a probe requirement across contention. | Continuous arrivals can prevent transfer completion. Member expiry must not erase the probe requirement while fresh DATA remains queued. | + +Keep new interfaces pure. Put reusable calculations in implementations. Add no parallel agreement store, acknowledgment map, expiry timer, or generic Packet extension. The new metadata and classes belong in the WLAN MAC or PHY packages that own their facts. + +## 5. Affected components and compatibility + +| Change surface | Declarations and consumers | +| --- | --- | +| Capability propagation | MIB C++/NED, HT capability structures, management conversion, association and simplified-management consumers, serializer checks. | +| Transmit preparation | QoS data-service contract and implementation, MPDU policy C++/NED contract, new HT policy, aggregate builder, `InProgressFrames`, lifetime handoff. | +| PHY dependency | The PHY plan owns HT header/tag MSG inputs, HT-SIG serialization and validation, receiver, radio, and the error-model contract/base. It also owns the MAC coverage handoff before the MIB update, per-part decisions, receive metadata, and shared delimiter parsing. | +| MAC receive path | MAC, Rx contract/implementation, aggregate decoder, recipient QoS data service and agreement handler, reorder contract and implementation. | +| Exchanges | HCF, frame sequence declarations/steps/context, acknowledgment handler, recovery procedure, RTS policy consumers, Compressed response policy/procedure, response-rate contracts, Tx contract/implementation and callbacks. The aggregate transmit profile requires fixed DATA rates. | +| Representation and observations | Current WLAN serializers and dissector, member region tags, semantic outcome signals, protocol-test observation boundary. | +| User documentation | `WHATSNEW`, user guide, migration guide, a focused example, and scoped Wi-Fi evidence. | + +The proposed transmit configuration is: + +```ini +**.mac.hcf.isBlockAckSupported = true +**.mac.hcf.blockAckVariant = "compressed" +**.mac.hcf.rateControl.typename = "" +**.mac.hcf.rateSelection.dataFrameBitrate = 6.5Mbps +**.mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +**.mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +**.mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +``` + +`HtMpduAggregationPolicy` does not exist yet. The fixed rate above selects HT MCS 0 with one spatial stream under the required HT20 long-GI configuration. These settings also require compatible modes, usable peer HT state, and accepted agreements. The example must configure those conditions explicitly. The initial policy adds no local count or byte cap. It derives those limits from the agreement, peer, and PHY. + +Custom data services, MPDU policies, Tx/Rx modules, response policies, and response-rate selectors can need interface updates. The migration guide must list each changed method and distinguish unsupported capability from an empty valid result. It must cover aggregate start, cancellation, member finalization, and the complete receive outcome. The DATA rate-control API remains unchanged; aggregate transmit opt-in requires its HCF submodule to be absent. Current `BasicMpduAggregationPolicy` configurations retain their rate-control behavior. The PHY plan owns error-model migration and preserves operation without an error model. + +Custom RTS policies retain the current callable signature but must accept an aggregate preview with a separate valid member header. The supplied Packet represents the complete PSDU for length checks. It has no outer MAC header. The migration guide must explain that distinction and the aggregate RTS recovery rule. Current individual-frame recovery remains the compatibility control. + +Use current owner-emitted simulation signals for actual transitions. Aggregate-level observations count one PPDU; member-level observations identify each MPDU. Document that boundary wherever a recorder or protocol assertion consumes it. No synthetic per-member PHY transmission event may imply multiple PPDUs. + +The current [seal registry](../../doc/project/audit/seal-list.md) seals `src/inet/common/packet/`. This design uses its current public API and proposes no edit there. Recheck seals before implementation. General MAC stop/start recovery remains a current limitation; ordinary agreement reset and object destruction must still dispose of the new transient state safely. + +## 6. Implementation steps and dependencies + +Each source step below proposes one commit with its tests and migration text. This plan defines the MAC feature PR. The PHY plan defines separate receiver implementation and broader evidence deliveries. Contract changes and their required consumers remain buildable together. A later split requires separate commit entries before implementation. + +The PHY plan defines two gates. D1 proves cached aggregate reception with deterministic probabilities through the production receiver and includes the shared NIST/Yans implementation. D1 also preserves default HT initialization and compatible ordinary reception. D2 completes the broader NIST/Yans and PHY compatibility evidence. A2 accepts D1; A6 can close deterministic integration before D2. A7 requires both A6 and D2. + +| Step and purpose | Responsible components and proposed change | Expected result and verification | Dependency | +| --- | --- | --- | --- | +| A0: Establish the merged implementation base. | Pin the integrated master base. Compare the MAC contracts before implementation. | Confirm the commented call, default policy, owners, and resolved choices. Record the feature-off baseline before source changes. Revise changed contracts or owners. | The merge prerequisite is satisfied. Contract checks and feature-off evidence remain required. | +| A1: Preserve capabilities and construct legal aggregates. | Extend MIB/management spacing state. Consume PHY P1 representation, HT-SIG serialization and validation, and bounded parsing. Implement aggregate construction calculations for spacing, serialization, and region-tag rules. | Capability and representation checks prove directional limits, valid aggregate bytes, and singleton identity after byte conversion. The PHY plan owns the shared wire fixture and HT-SIG round trips. | A0; PHY P1. | +| A2: Accept deterministic PHY reception. | MAC/Rx consumers adopt the PHY D1 indication and aggregation property. Accept the four per-part decisions, header rejection, structural refusal, actual failure paths, and receive metadata contract. | Valid long-GI and short-GI aggregates supply copied PHY outcomes without a second error draw. Failure containers stop before MAC header or trailer peeks. Incomplete advertised receive-mode support fails before the MIB update. An absent model follows its supported error-free case. MAC receipt and response acceptance belong to A4 and A5. | A1; PHY D1. | +| A3: Prepare and retain independent members. | Add the HT policy, dynamic member selection, and batch preparation. Require fixed DATA rates and reject aggregate transmit opt-in with a rate-control submodule. Select one mode before spacing and batch reduction. Apply A1's construction calculations at that mode. Include numbered, unsent candidates through current acknowledgment state. Preserve single-frame internal collision recovery and current queue/lifetime handoff. | Unsupported rate-control combinations fail before DATA attempts. Refusal preserves source ownership and sequence state. Numbered, unsent members retain their assigned numbers and first-DATA Retry value. Commitment retains independent deadlines. | A1. | +| A4: Receive aggregates and apply mandatory release. | Project range outcomes onto separate member Packet flags before Rx validation. Integrate one contention outcome, receipt updates, reorder context, and history. | Physical reception proves FCS rejection, correct EIFS behavior, delimiter continuity, mandatory release, and exactly-once delivery. Silent discard creates no receive state. | A1, A2. | +| A5: Complete the implicit exchange and probe recovery. | Add aggregate steps, Tx start/cancellation, member finalization, Block Ack wait, response calculation, admission, and transmitted-member records. Select RTS from the complete aggregate preview after each reduction. Apply one contention outcome after aggregate RTS failure without member retry accounting. Derive the probe EDCAF from TID. Apply the observable response acceptance rules. | Exchanges prove computed FCS, SIFS cancellation, selective retry, response validation, probe priority, unchanged deadlines, and both HT duration conditions. Threshold cases prove PSDU-based protection. Mixed-batch RTS failure preserves member state at the contention retry boundary. A short-GI aggregate updates MAC receipt and receives one Compressed response after SIFS. At most one probe occurs between DATA attempts. | A2, A3, A4. | +| A6: Verify negotiation, recovery, and compatibility. | Add infrastructure and compatibility coverage. Verify absent-agreement recovery for finite batches and continuous arrivals with zero and nonzero inactivity values. Verify DATA progress for a healthy agreement under controlled response loss. | Real management establishes limits. Tests prove transfer completion, probe-driven retirement without queue drain, later setup, continued DATA transmission, and historical compatibility. | A5. | +| A7: Document and verify the supported profile. | Add the example, user and migration text, release note, and execution record. Reconcile relevant expected-failure declarations under separate authorization. | A6 and PHY D2 evidence support the documented profile. Required publication checks pass before any separately authorized publication. | A6; PHY D2. | + +A1 and A3 can supply reviewed preparation changes before the complete feature exists. A5 and A6 establish deterministic integration after A4. They do not establish NIST/Yans support. The final feature claim requires A7. Dependencies describe delivery order; they assign no concurrent agents. + +The following subjects identify the future commits. Each entry uses the purpose, components, behavior, and dependencies in its step above. Section 5 identifies the affected source files. Section 7 defines the test scenarios. Each behavior commit must add the named reviewer document beside its protocol tests under `tests/protocol/wifi/`. + +| Commit ID | Proposed subject | Tests and reviewer document | +| --- | --- | --- | +| A0 | `plan: change: pin the merged aggregate implementation base` | Record source comparisons and feature-off evidence. No protocol behavior changes, so no new protocol test or reviewer document applies. | +| A1 | `ieee80211: add: construct aggregates within peer limits` | Add capability and wire tests plus `ampdu-capabilities.md`. Verify spacing, lengths, and byte conversion under directional peer limits. | +| A2 | `ieee80211: add: accept copied aggregate PHY outcomes` | Add physical receive tests plus `ampdu-phy-acceptance.md`. Verify cached outcomes, failed headers, and absent-model behavior. | +| A3 | `ieee80211: add: retain independent members before aggregate submission` | Add preparation tests plus `ampdu-preparation.md`. Verify refusal, cancellation, source ownership, sequence numbers, and deadlines. | +| A4 | `ieee80211: add: release accepted aggregate members once` | Add receive and reorder tests plus `ampdu-member-release.md`. Verify selective loss, delimiter recovery, EIFS, and Rx history. | +| A5 | `ieee80211: add: complete implicit Compressed Block Ack exchanges` | Add exchange, admission, and recovery tests plus `ampdu-implicit-exchange.md`. Verify one response, selective retry, protection, and probe bounds. | +| A6 | `tests: add: verify aggregate negotiation and recovery` | Add management and compatibility cases plus `ampdu-compatibility.md`. Verify agreement loss, finite batches, continuous arrivals, and later setup. | +| A7 | `doc: add: describe the supported aggregate profile` | Add the example and user, migration, and release text. Record A6 and PHY D2 evidence. This commit adds no protocol behavior. | + +For each proposed commit, its parent includes only the earlier entries whose dependencies it requires. Those effects remain assumptions until implementation supplies evidence. For example, A4 assumes A2 supplies copied PHY outcomes. A4 must prove that one failed member cannot prevent correct members from their required release. Each reviewer document must explain that commit's concrete scenario, controlling standard clauses, assertions, and verification commands. The proposed tests establish no executed result. + +## 7. Verification methods and expected results + +Each future A1 through A6 behavior commit must include its direct protocol tests and a reviewer document. The reviewer document must state the trigger, expected exchange, failure conditions, exact commands, and evidence limits. These tasks apply to each behavior commit within a short series. A later test commit does not satisfy this requirement. A7 collects the final profile evidence; it does not defer these earlier obligations. See [TR-PLAN-PROTOCOL](../../doc/project/rule/testing.md#tr-plan-protocol) and [PR-SERIES-PLAN](../../doc/project/rule/pull-request.md#pr-series-plan). + +### Proposed direct checks + +The seven fixtures below belong to this feature plan and remain proposed. The [PHY plan](ieee80211-ampdu-phy-reception.md#7-verification-methods-and-expected-results) separately lists its unit, module, and protocol fixtures. Those fixtures check wire fields, bounded inspection, deaggregation, cached reception, ordinary controls, and the current MAC rejection boundary. The [PHY execution record](ieee80211-ampdu-phy-execution.md) states observed PHY results and their limits. That evidence does not establish MAC feature completion. + +Fixed causal cases use seed 0. Injected agreements or deterministic error models establish only the stated boundary. Infrastructure cases separately prove production negotiation. [Appendix B](#appendix-b-verification-commands-and-campaigns) contains commands, campaign bounds, and evidence requirements. + +| Proposed fixture and category | Trigger and production boundary | Required observation | +| --- | --- | --- | +| `Ieee80211AmpduCapabilities_1.test`, module. | Full and simplified management with asymmetric receive limits. Include an absent aggregate role, a role without advertised short-GI support, and an absent error model. Use exact MAC inputs with a one-antenna limit and catalog modes outside that limit. | Exponents 0–3 and spacing values 0–7 reach the correct directional consumer; reset removes old peer limits. Absent roles fail before advertisement; incomplete coverage fails before the MIB changes local HT capability. Unsupported unadvertised modes do not cause rejection. An absent model retains valid HT initialization and negotiation. Compatible models retain advertised HT20 short-GI support. | +| `Ieee80211AmpduPreparation_1.test`, module. | Production queues, HT policy, fixed DATA rates, batch preparation, and commitment. Include internal collision survivors and aggregate transmit opt-in with AARF. | Source identities, sequence wrap, independent deadlines, count/window/byte limits, and refusal before extraction. Numbered, unsent members keep their allocation and first-DATA Retry value. Internal collision terminal disposal still affects its selected frame only. Batch reduction retains one rate decision. AARF fails initialization with and without fixed-rate overrides. | +| `Ieee80211AmpduReordering_1.test`, module. | Physical reception, member Packet flags, Rx FCS modes, and copied PPDU history. | Separate cases cover each reachable condition in 10.25.6.6.2.1(a)(2)(i–iv), retained condition (v), delimiter chains, and receipt evidence before the window. Declared-correct and computed FCS cannot accept a PHY-failed member. Partial loss preserves valid history and suppresses EIFS. Required release cannot become unconditional gap retention. | +| `Ieee80211AmpduExchange_1.test`, module. | Production HCF, frame sequences, Tx/Rx, PHY, and recipient response. | Exactly one response after PPDU end and SIFS; no per-member ACK or successful-path BAR; singleton aggregate response remains Block Ack. Successful initial transmission and selective retry verify each member's final header and computed FCS. Without a qualifying basic rate, the recipient selects a mandatory fallback at or below the HT reference rate. | +| `Ieee80211AmpduRecovery_1.test`, module. | Aggregate RTS failure, bitmap results, implicit response loss, probe refusal/results, expiry, DELBA, and reset. Cases cover new, committed, and mixed batches. Other cases cover absent or healthy agreements under finite or continuous traffic. | One contention outcome after RTS failure, unchanged member counters, no RTS-triggered member disposal, and no DATA outcome or probe. Other cases prove selective recovery, unchanged deadlines, safe disposal, derived probe ownership, and at most one probe between DATA attempts. No case permits duplicate DATA feedback. Finite and continuous traffic permit later setup. Healthy agreements retain DATA progress. | +| `Ieee80211AmpduAdmission_1.test`, module. | Actual aggregate transmission under zero and positive TXOP limits, with and without RTS/CTS. Include threshold boundaries and batch reduction. | Complete-cost admission, separate L-SIG and 10 ms checks, one retained mode, and correct common Duration/ID. Complete PSDU length controls RTS selection after spacing and each reduction. Originator inactivity expiry during SIFS cancels DATA before new commitment or DATA attempt accounting. | +| `N_AmpduImplicitBa.test`, protocol. | Production association and application traffic through AP and STA paths. | Negotiated limits, aggregate contents, implicit response, selective retry, and exactly-once upper delivery. | + +A2 accepts copied PHY indications from valid long-GI and short-GI aggregates through the real radio. Its checks stop at that indication and refusal boundary. A4 supplies member validation and MAC receipt. A5 owns the complete short-GI receive exchange in `Ieee80211AmpduExchange_1.test`, after A4. A2 requires neither MAC receipt updates nor a Block Ack response for closure. + +For A5, a fixture peer supplies legal aggregate bytes at an advertised HT20 short-GI mode through the real radio and MAC. Verify valid member receipt and one Compressed response after SIFS. Keep the local aggregate transmit policy at long GI. This case proves receive coverage; it does not establish short-GI selection from production transmit queues. A6 separately verifies the production capability negotiation. + +The PHY fixture separately supplies invalid encoded HT-SIG with successful header probabilities and without an error model. A2 verifies HEADER failure and refusal of any delivered container before MAC header or trailer peeks. Invalid CRC and unsupported field combinations use the current header-failure paths. A separate malformed-input case retains HEADER success but refuses insufficient modeled PSDU content at DATA. A4 and A5 verify no receipt update, delivery, or response after either refusal. + +The current [N_Ampdu](../../tests/protocol/wifi/11n/N_Ampdu.test) presence assertion needs the Compressed profile and supported HT configuration. Retain its aggregate observation and add completion evidence; do not replace it with a declaration. The current [Ac_Ampdu](../../tests/protocol/wifi/11ac/Ac_Ampdu.test) requirement remains deferred for actual VHT PPDUs. An inherited HT transmission from an `ac` interface cannot close that VHT requirement. + +Retain the current Compressed admission, exchange, input, interframe, and originator fixtures. Retain Block Ack scheduling, lifetime, lifecycle, association reset, fragment retirement, Basic, and disabled-feature checks. Map each changed contract to an explicit fixture before execution. A fixture with injected HCF responses does not prove PHY reception or association. + +The receive cases include partial loss, all-member failure, common header failure, and an overheard aggregate while local DATA remains queued. At least one correct member must suppress EIFS regardless of its address or agreement state. A delivered container with no correct member must invoke the existing corruption procedure once. Separate-part common header failure instead preserves the radio's abort and HCF history invalidation. Member rejection must not clear valid aggregate history through the ordinary HCF corruption callback. + +The computed-FCS exchange starts with valid production DATA and verifies a successful aggregate before it injects a selective member loss. Its retry changes the Retry bit and can change Duration/ID. Every transmitted member must pass the receiver's computed-FCS check after those edits. No outer FCS may replace a member FCS. + +Drive internal collisions through production EDCAF arbitration before aggregate preparation. Verify one numbered, unsent survivor without an aggregate commitment or DATA attempt. Later aggregate submission must preserve its number, deadline, and collision recovery state. Its first DATA transmission keeps Retry clear. A separate retry-limit case verifies current collision disposal before aggregate selection. + +For aggregate RTS failure, cover batches of new members, committed members, and both together. Include a committed candidate that remains `FRAME_NOT_YET_TRANSMITTED` and a transmitted retry. Withhold CTS through the access-category retry boundary. Verify one contention update per failed RTS and the current boundary reset. Verify unchanged member counters, numbers, acknowledgment states, and deadlines. No member disposal, DATA outcome, or pending probe may result from that failure alone. + +Absent-agreement recovery is an A6 acceptance gate. Cover finite batches and continuous arrivals with zero and nonzero negotiated inactivity values. For the finite case, force probe failure so DATA expiry reaches transfer completion. For continuous traffic, keep fresh DATA queued and provide a legal probe admission opportunity. Observe actual BAR, recipient DELBA, retirement, and later successful setup without queue drain. + +A valid zero-bit Block Ack must cause retry without a probe. Probe failure must permit a DATA attempt before another probe. Refusal must preserve the pending flag and DATA deadlines. Response tests cover frames outside an active receive interval, wrong peer/TID/profile, retired local callbacks, and an indistinguishable valid frame inside a later interval. The last case follows ordinary validation; no hidden exchange identity controls acceptance. [Appendix C](#appendix-c-recovery-transitions-and-acceptance-cases) maps the recovery cases to their expected results. + +A6 must also prove DATA progress for a healthy agreement under deterministic response loss. Keep fresh DATA queued with inactivity timeout zero. Appendix C separates full, partial, and failed probe cases and defines the required DATA observations. A5 checks the state transitions and counters for these cases. + +### Evidence status + +The [PHY execution record](ieee80211-ampdu-phy-execution.md) states the final-commit checks and their limits. Those checks cover the PHY implementation and the current MAC rejection boundary. They do not establish MAC member acceptance, implicit Block Ack, or the proposed recovery procedure. + +The source assessment uses the pinned master base and the PHY contracts in this branch. The diagrams describe proposed MAC behavior. Diagram rendering remains unverified. + +## 8. Risks, unresolved decisions, and implementation scope + +The proposed transmit scope is HT20 with long GI. Reception also covers advertised HT20 short-GI modes. Other decisions include fixed DATA rates, explicit transmit opt-in, full-state receipt records, and cached partial PHY outcomes. Recovery uses DATA retry after one response-failure probe. These are model and implementation choices. They do not redefine IEEE ACK policy bits or the mandatory release conditions within the supported path. + +A threshold for consecutive failures remains optional follow-up work because it adds history and reset rules. Probe-cost measurements can guide a later trigger change. + +The source and standard checks resolve the main exchange choices: + +| Decision | Confirmed requirement or source behavior | Proposed implementation | +| --- | --- | --- | +| Implicit response format and rate. | 10.6.6.1 requires non-HT format for this profile. 10.6.6.5.2 explicitly covers Implicit BAR. | Factor the S5 selector around actual PPDU mode and addresses. Preserve primary/alternate rules. | +| Absent agreement after aggregate reception. | 11.5.4 explicitly covers Block Ack-policy DATA. This profile does not extend that DELBA rule to implicit DATA. | Discard implicit DATA silently. Current completion handles a drained transfer; the new probe handles continuous arrivals. A6 proves later setup. | +| Aggregate TXOP exceptions. | 10.23.2.9 excludes every multi-MPDU aggregate from the listed overrun exceptions. | Require full fit for multiple members. Check singleton composition and TXOP history before an exception. | +| Response-failure probe. | 10.25.6.7.1 permits BAR or direct DATA retransmission after response loss. Current BAR selection omits failed implicit waits. Continuous arrivals can prevent transfer completion. | Require one probe as INET policy before further DATA. Consume it on submission; probe failure returns to DATA retry. A6 checks a healthy agreement under controlled response loss. | +| Aggregate recovery counters. | 10.23.2.2 defines response success independently of bitmap delivery. Clause 10.23.2.12.1 separates ordinary associated-RTS retry increments from Block Ack operation. | Apply one contention outcome after aggregate RTS failure without member retry increments or disposal. Preserve current individual-frame and internal collision recovery. | +| Aggregate RTS selection. | 10.3.5 applies the threshold to the complete PSDU. The current policy accepts a Packet and a separate MAC header. | Supply an exact aggregate preview and valid member header. Reevaluate protection after each reduction. Retain the current permitted equality policy. | +| Aggregate rate control. | AARF can change rate and probe state after one member callback without knowledge of the actual PPDU mode. | Require fixed DATA rates and no HCF rate-control submodule for aggregate transmission. Leave adaptive aggregate feedback for a separate design. | +| Response correlation. | Clauses 9.3.1.8.1 and 9.3.1.8.2 define no identifier for an individual exchange. | Validate the active receive interval and observable fields. Reject retired local callbacks. Apply ordinary rules to an indistinguishable valid frame. | +| HT error-model compatibility. | HT length exponent zero advertises 8191 octets. Short-GI capability covers HT PPDU reception. MAC supplies effective limits to the MIB at link-layer initialization. BerTable already rejects HT operation strings. | Check role presence at PHY initialization. Validate exact MAC inputs before the MIB update. Preserve absent-model operation. Include shared NIST/Yans support for advertised HT20 receive modes in D1. Document BerTable's HT exclusion and compare non-HT parent controls. | +| HT-SIG receive validation and content bounds. | Clause 19.3.21 distinguishes invalid HT-SIG CRC from physical carrier loss after a valid header. Current radio validation omits the CRC check. | P1 defines a shared stateless header validator. P2 applies it through current HEADER failure paths. A separate DATA bounds check refuses insufficient modeled content without a carrier-loss claim. | +| HT mixed-format duration. | 10.27.4 limits calculated `L_LENGTH` to 4095. The separate 10 ms bound also applies. | Check both conditions through the HT mode contract before commitment and actual submission. | +| Partial aggregate reception. | 10.3.2.3.7 prohibits EIFS when any member arrives correctly. Ordinary MAC rejection also clears HCF history. | Separate member rejection from one complete receive outcome. Preserve valid aggregate history. | +| Deferred aggregate submission. | Current Tx copies and finalizes one MPDU before its IFS timer expires. | Use the existing timer boundary for HCF validation, commitment, member finalization, and actual aggregate submission. | + +Silent absent-agreement discard, one probe after a failed implicit wait, and the PHY independence assumption are model choices. The standard evidence does not establish measured error accuracy or performance. A materially different owner, PHY abstraction, or supported profile requires a plan revision before its implementation. + +The PHY prerequisite contains the partial reception risk. D1 must pass before the MAC exchange can claim deterministic selective recovery. It includes the NIST/Yans implementation and basic compatibility checks. D2 must pass before the full NIST/Yans profile forms part of the feature claim. A whole-PPDU failure plus a MAC-injected bitmap does not establish this feature. A second risk is incorrect aggregate history: a valid later delimiter must not repair a broken continuity condition silently. + +The current short-GI, VHT negotiation, control-response, channel-width, LDPC, and Minstrel topic branches are not assumed prerequisites. The new transmit profile retains its stated limits. The short-GI receive checks use current mode calculations; they establish no new short-GI transmit or duration-repair claim. Reconcile shared interfaces against their actual heads before any later integration. This plan does not authorize a merge or a rebase of those branches. + +The authorized deliverable is this document revision and the linked PHY plan. Implementation, expectation changes, protected-source edits, commits, and publication retain their separate task scope. The current design needs no protected Packet API edit. If that changes, update the design and obtain the permission that the seal owner requires. + +## Appendix A. Standard requirements and evidence + +IEEE Std 802.11-2024 is the normative reference for this profile. Its local source is `80211ax-2024.pdf`; the filename does not identify the document revision correctly. The corpus document ID is `ieee80211-2024`. Physical PDF pages below differ from printed page numbers. + +| Requirement | Normative source | Physical PDF pages and corpus locator | +| --- | --- | --- | +| ACK policy bits `00` mean Implicit BAR inside the applicable aggregate context. | Clause 9.2.4.5.4; Table 9-13. | 676–679; `ieee80211-2024:clause:9.2.4.5.4`, `ieee80211-2024:table:9-13`. | +| The modeled HT-SIG header preserves PSDU length, Aggregation, field positions, and the specified CRC. | Clause 19.3.9.4.3; Table 19-11; Figure 19-6; Clause 19.3.9.4.4. | 3431–3434; `ieee80211-2024:clause:19.3.9.4.3`, `ieee80211-2024:table:19-11`, `ieee80211-2024:figure:19-6`, `ieee80211-2024:clause:19.3.9.4.4`. | +| Invalid HT-SIG CRC prevents PHY-RXSTART. Unsupported field combinations also require rejection through the PHY receive procedure. | Clause 19.3.21. | 3486–3487; `ieee80211-2024:clause:19.3.21`; PDF fallback for the receive validation text. | +| Signal loss after a valid header produces CarrierLost. A short modeled PSDU fixture does not establish that physical behavior. | Clause 19.3.21. | 3486–3487; `ieee80211-2024:clause:19.3.21`; PDF fallback for the carrier-loss distinction. | +| Compressed Block Ack carries addresses, TID, sequence control, and bitmap, without an identifier for an individual exchange. | Clauses 9.3.1.8.1 and 9.3.1.8.2. | 722–723; `ieee80211-2024:clause:9.3.1.8.1`, `ieee80211-2024:clause:9.3.1.8.2`. | +| Each member has a delimiter and its own MPDU/FCS. Nonfinal HT subframes align to four octets; the final subframe has no alignment padding. | Clause 9.7.1; Figures 9-1326 through 9-1331; Table 9-659. | 1840–1843; `ieee80211-2024:clause:9.7`, `ieee80211-2024:figure:9-1326`, `ieee80211-2024:table:9-659`. | +| An HT delimiter uses the 12-bit low length field. Its CRC covers the previous 16 bits; the signature is `0x4E`. | Clause 9.7.1; Table 9-659. | 1841–1842; `ieee80211-2024:table:9-659`. | +| Members share the applicable address, ACK policy, and Duration/ID conditions. | Clause 9.7.2; Clause 10.12.1. | 1843 and 1976; PDF fallback for the exact subclauses. | +| The receiver advertises maximum length and minimum MPDU start spacing. Maximum HT length is `2^(13 + exponent) - 1` octets. | Table 9-225; Clauses 10.12.2 and 10.12.3. | 1126, 1976–1979; `ieee80211-2024:table:9-225`, `ieee80211-2024:clause:10.12.3`; PDF fallback for 10.12.2. | +| Short-GI capability describes reception of HT PPDUs, without a separate restriction to standalone MPDUs. | Table 9-224. | 1124–1125; `ieee80211-2024:table:9-224`. | +| Additional spacing uses zero-length delimiters. The selected PHY data rate controls the required spacing. | Clause 10.12.3, Equation 10-12. | 1977–1979; `ieee80211-2024:clause:10.12.3`. | +| This HT profile requires RTS/CTS when the complete PSDU exceeds the RTS threshold. Additional protection remains permitted. | Clause 10.3.5. | 1930; `ieee80211-2024:clause:10.3.5`. | +| Associated-RTS frame retry increments apply to DATA outside a Block Ack agreement. Internal collisions separately increment the affected frame retry counts. | Clause 10.23.2.12.1. | 2016–2017; `ieee80211-2024:clause:10.23.2.12.1`. | +| HT PPDU duration must satisfy its PHY limit; TXOP conditions apply separately. | Clause 10.13; Table 19-25; Clause 10.23.2.9. | 1982–1983, 3493–3494, and 2012–2014; `ieee80211-2024:clause:10.13`, `ieee80211-2024:table:19-25`, `ieee80211-2024:clause:10.23.2.9`. | +| HT mixed format also requires calculated `L_LENGTH` at or below 4095, with the signal extension excluded from that calculation. | Clause 10.27.4; Equation 10-18. | 2091–2092; `ieee80211-2024:clause:10.27.4`. | +| One or more correctly received aggregate members prohibit EIFS. | Clause 10.3.2.3.7. | 1891–1892; `ieee80211-2024:clause:10.3.2.3.7`. | +| This profile's Block Ack uses non-HT OFDM or ERP-OFDM. Both basic-rate selection and mandatory-rate fallback remain at or below the HT non-HT reference rate. An alternate preserves the required duration and modulation class. | Clauses 10.6.6.1, 10.6.6.5.2, and 10.6.6.5.4. | 1942–1947 and 1950; canonical clause IDs use `ieee80211-2024:clause:`. | +| The recipient updates its scoreboard before one Block Ack response after SIFS. At least one valid member must exist. | Clauses 10.25.6.3, 10.25.6.5, and 10.25.6.7.1. | 2060–2064 and 2068–2070; canonical clause IDs use `ieee80211-2024:clause:`. | +| Mandatory aggregate release depends on delimiter validity and previous aggregate or Block Ack history. | Clause 10.25.6.6.2.1(a)(2)(i–iv). | 2065–2066; `ieee80211-2024:clause:10.25.6.6.2.1`. | +| A lost Block Ack permits DATA retransmission. Received bits update only the applicable transmitted MPDUs. | Clauses 10.25.6.7.1 and 10.25.6.8. | 2068–2072; canonical clause IDs use `ieee80211-2024:clause:`. | +| EDCA distinguishes a received response from absent reception; contention state belongs to the exchange. | Clause 10.23.2.2. | 1999–2001; `ieee80211-2024:clause:10.23.2.2`. | +| Valid Implicit BAR DATA refreshes recipient inactivity. The explicit absent-agreement DELBA rule names Block Ack-policy DATA. | Clause 11.5.4. | 2566; `ieee80211-2024:clause:11.5.4`. | + +The corpus was rebuilt with the tracked standards launcher. Lint reports 76 heading ambiguities and 425 unresolved references across the corpus. This is not a clean global corpus. Structural retrieval lacks separate nodes for 9.7.1, 10.12.1, and 10.12.2. PDF text supplied their relevant requirements. Visual inspection of Table 9-13 confirmed the two distinct meanings of bits `00`. + +The retrieved references from 10.25.6.5 resolve, but the extractor omits some inline clause references from its reference list. Direct retrieval separately confirmed 10.25.6.3, 10.25.6.4, and 10.25.6.7.1. Partial-state and DMG/TDD requirements do not enter this profile. No absent corpus edge establishes a protocol exception. + +The review assessment also inspected physical PDF page 2065 and Table 19-25 on physical PDF page 3493. The release instruction is mandatory when its conditions hold. The table specifies `aPPDUMaxTime = 10 ms`. The Clause 10.13 reference to Table 19-25 resolves. Its unresolved 10.12.2 corpus edge does not alter the directly checked HT duration requirement. + +The latest review retrieved normative Clauses 10.27.4 and 10.3.2.3.7 through their canonical nodes. Visual inspection of physical PDF page 2092 confirmed the ceiling operation in Equation 10-18 and the 4095-octet prohibition. Clause 10.27.4 references to 6.7.4, 6.7.5, and Table 19-25 resolve. The extracted 10.3.2.3.7 reference to 10.3.7 also resolves. Its EIFS prohibition required no PDF fallback. These retrieved clauses add requirements that the separate 10 ms table value does not replace. + +The response review retrieved normative Clauses 9.3.1.8.1 and 9.3.1.8.2, plus Table 9-225. Their corpus text establishes the frame fields and the minimum advertised HT aggregate length. The references from 9.3.1.8.2 to Figures 9-54 and 9-48, and Clause 10.25.6.5, resolve. Table 9-225's reference to 10.12.3 also resolves. These checks required no additional PDF inspection. + +The serialization correction retrieved normative Clauses 19.3.9.4.3 and 19.3.9.4.4, Table 19-11, and Figure 19-6. Their corpus locators and physical pages appear in the table above. The clause references to Table 19-11 and Figure 19-8 resolve. The extracted text establishes the fields and CRC without additional PDF inspection. P1 must verify the actual encoded bytes against those requirements. + +The receive-contract review retrieved normative Table 9-224 and Clause 19.3.21. Table 9-224 defines short GI as a receive capability. Its reference to 19.1.4 resolves; its reference to 10.11 remains unresolved. Clause 19.3.21 references Figures 19-25 through 19-27, which resolve. The extractor places later receive text under Figure 19-27. PDF text inspection of physical page 3486 confirmed the invalid-CRC rule without reliance on that extraction boundary. + +The RTS and recovery correction retrieved normative Clauses 10.3.5, 10.23.2.12.1, and 10.23.2.2 through their canonical nodes. Their physical PDF pages appear above. The extracted references to 26.2.1, 10.27, and 10.3.2.11 resolve. Clause 10.23.2.2's extracted references also resolve. No additional PDF inspection was necessary for these text requirements. The aggregate RTS rule and retained internal collision path remain distinct implementation decisions within that evidence. + +### Release history required by the supported profile + +The table maps Clause 10.25.6.6.2.1(a)(2) to the required history. Here, `WinStartR` is the receipt window start; `WinStartB` is the reorder window start. The prior receipt bit is the bit for `WinStartR - 1`. Release across a gap also requires `SN > WinStartB` and the clause's other profile conditions. + +| Condition | Required facts | Reachable in this profile? | +| --- | --- | --- | +| (i) | The received MPDU is a later aggregate member. The prior receipt bit is set. Delimiters from the MPDU at that sequence number to this member are valid. | Yes. A later member in the same aggregate supplies the proposed case. | +| (ii) | The first aggregate member follows an aggregate after SIFS/RIFS, or a Block Ack transmission after SIFS. The prior receipt bit is set. All delimiters after the MPDU at that sequence number in the prior aggregate are valid. | Yes. A first aggregate member can follow the prior Block Ack after SIFS within a positive TXOP. | +| (iii) | The received MPDU follows an aggregate after SIFS/RIFS, or a Block Ack transmission after SIFS. The prior receipt bit is set. All delimiters after the MPDU at that sequence number in the prior aggregate are valid. | Yes. Individual fallback can follow Block Ack after SIFS within a positive TXOP. | +| (iv) | The first aggregate member follows an MPDU after SIFS/RIFS, or an ACK transmission after SIFS. The prior receipt bit is set. | Yes. An aggregate can follow individual Block Ack-policy DATA after SIFS. | +| (v), retained from S5 | The individual MPDU follows an MPDU after SIFS/RIFS, or an ACK transmission after SIFS. The prior receipt bit is set. | Yes. The retained individual-MPDU path supplies a SIFS case. | + +These are proposed reachability cases, not executed evidence. A4 must verify each case through physical reception. No condition is deferred in this revision. Any later deferral must identify the excluded path and its source or fixture evidence. + +HCF supplies member position, valid delimiter history, prior event kind, and physical intervals. The reorder service owns the release decision. These facts explain the history extension; they do not require a second receipt map or timer. + +## Appendix B. Verification commands and campaigns + +The bounded timing campaign uses both 2.4 GHz and 5 GHz, HT MCS 0 and 7, long GI, and a legal fixed non-HT response. It covers no protection and RTS/CTS under both current protection mechanisms. Test one case immediately below, at, and above each applicable admission bound. Derive boundary values from the selected mode. Separate clock tolerance from protocol airtime; do not add the HT signal extension twice. + +Set the RTS threshold immediately below, equal to, and above the same complete PSDU length. Verify required protection above the threshold and the current policy's permitted protection at equality. Include two individually short members whose aggregate exceeds the threshold. Add nonzero spacing that crosses the threshold. Reduce a batch across that threshold and verify renewed protection selection, complete-cost admission, and one retained DATA mode. + +Include the HT mixed-format boundary at `TXTIME - SignalExtension = 5484 us` where the selected mode can reach it. Verify that HCF refuses a 6 ms 5 GHz candidate despite its duration below 10 ms. Test the separate 10 ms predicate directly because legal HT mixed-format transmission reaches the L-SIG bound first. + +Place the originator inactivity deadline after CTS reception but before the delayed aggregate's SIFS timer expires. Let the production inactivity handler retire the agreement. Verify that the start callback refuses the cancelled preparation. No DATA submission, source commitment, sequence assignment, member failure feedback, or response-failure probe may result from that cancelled request. Independently scheduled expiry and teardown retain their current disposal effects. A separate reset case verifies cancellation before context release and no callback after release. + +Use two peers and two TIDs in isolation controls. Exercise sequence numbers around 4095/0 and buffer sizes 1, 2, and 64. Cover aggregate completion before, at, and after DATA expiry where those orders are reachable. Use deterministic probabilities for the essential selective-loss cases. The PHY plan owns NIST/Yans seeds 0, 1, and 2. Its campaign checks invariants and reproducibility, not statistical goodput. + +The current checkout provides the module, protocol, and fingerprint runners. Their CLI and selection rules were inspected. The commands below are planned; new fixture selectors cannot run until those fixtures exist. Use the checkout's normal OMNeT++ environment. A zero-case selection is `NOT_RUN`. + +```sh +make MODE=debug -j$(nproc) + +opp_run_module_tests -m debug \ + -f 'Ieee80211Ampdu(Capabilities|Preparation|Reordering|Exchange|Recovery|Admission)_1\.test$' + +opp_run_module_tests -m debug \ + -f 'Ieee80211(CompressedBlockAck.*|BlockAck.*|Edca.*Lifetime.*)_1\.test$' + +opp_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' \ + -f '/(N_AmpduImplicitBa|N_Ampdu|N_BlockAck|Ac_BlockAck|Ac_BlockAckFallback)\.test$' \ + --log-file /tmp/inet-ampdu-protocol.log + +opp_run_fingerprint_tests -m debug -r 0 \ + -w '^(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless/qos|adhoc/qos))$' \ + --ingredients tplx --ingredients '~tNl' --ingredients '~tND' +``` + +Preview each new selector with the runner's `--dry-run` option. Reconcile selected files and executed configurations. Do not run protocol runners concurrently against shared generated directories. Preserve logs and generated artifacts before a later run can replace them. + +Run the full Wi-Fi protocol suite once after the focused cases pass. Separate observed passes, declared expected failures, unexpected failures, and setup errors. Expected-failure declarations for unrelated unsupported features remain visible. A changed marker or expected result follows [baseline rules](../../doc/project/rule/testing.md#tr-baseline-deliberate); source approval alone does not authorize that change. + +For each result, record HEAD, local source diff, environment, library mode, exact command, selected files, configuration, run, seed, verdict, and artifact path. Compare affected behavior with the pinned parent under identical inputs. Fingerprint differences require causal explanation and direct correctness evidence. Graphical `tyf` coverage needs its own environment and must not receive an inferred pass. + +Before publication, follow the [gate procedure](../../doc/project/guide/run-the-gates.md). It requires debug and release compilation, scoped correctness evidence, and the applicable architecture, naming, interface, seal, and series checks. Use the actual implementation base for range checks. No publication command forms part of this plan request. + +## Appendix C. Recovery transitions and acceptance cases + +The pending probe is one boolean in the originator agreement. It exists because response failure and the next admission occur at different events. Member expiry can occur between those events. Current EDCA classification derives the responsible access category from the agreement's TID. HCF coordinates the state with the current BAR policy and acknowledgment handler. It creates no retained category, second mapping table, separate agreement store, or periodic recovery task. + +| Trigger | Required state change and result | +| --- | --- | +| An implicit wait ends without a valid matched Block Ack. | Set the agreement's pending-probe flag. End the sequence with one contention failure. Defer further DATA for that peer/TID until the probe attempt or retirement. | +| A valid matched implicit Block Ack has no positive bits. | Leave the probe flag clear. Complete one successful response exchange. Retain unresolved members for DATA retry. | +| Probe admission refuses transmission. | Retain the pending flag. Preserve all DATA deadlines and sequence state. Other eligible traffic remains available. | +| The radio accepts the explicit probe. | Consume the pending flag. Only the EDCAF derived from the agreement's TID submits and counts the attempt. Retain its physical context and mark applicable transmitted members for that actual BAR wait. Use the current agreement window start. | +| A valid matched Block Ack answers the probe. | Apply transmitted-only bitmap updates. Release acknowledged canonical members. Permit unresolved or new DATA under normal eligibility. Do not repeat DATA feedback. | +| The probe ends without a valid matched response and its agreement remains active. | Apply ordinary BAR control recovery. Return unresolved members to DATA retry. Do not re-arm a probe until another implicit DATA wait fails. | +| A valid recipient-role DELBA arrives. | Use current lifecycle retirement and Action invalidation. Clear unsent probe state. Preserve any accepted physical completion context. | +| A finite transfer becomes empty at a safe boundary. | Run current transfer completion before an unsent probe. Retire the accepted agreement and send originator-role DELBA independently of inactivity timeout. | +| Reset or teardown retires the agreement. | Remove its pending flag with that agreement. Another peer/TID retains its state. | + +Continuous-traffic recovery assumes a responsive peer and a legal BAR admission opportunity. A6 must keep fresh DATA queued while the probe occurs. A BAR received without a recipient agreement triggers S5's DELBA path. The originator then retires its stale agreement before later setup. + +The finite-batch case must reach the current completion trigger even if probes fail. Preserve actual-member transmission records so the completion predicate can succeed. Withhold probe recovery responses until DATA expires, then permit normal teardown completion. This is a deterministic fixture condition, not a claim that radio loss always ends within a fixed time. + +Healthy-agreement recovery is a separate A6 case. Production management establishes accepted agreements at both peers. Fresh DATA remains queued with inactivity timeout zero. Deterministic PHY outcomes cause selected implicit Block Ack losses. The partial-bitmap case also causes selected DATA reception failures. Each case provides legal access for the probe and subsequent DATA. + +| Probe result under a healthy agreement | Required observation | +| --- | --- | +| A valid probe Block Ack acknowledges all retained transmitted members. | Release those members. Observe a new DATA transmission before another probe. | +| A valid probe Block Ack acknowledges some retained transmitted members. | Release only those members. Retain unresolved members for retry. Observe the next eligible DATA transmission before another probe. | +| The probe response is absent or invalid. | Observe an eligible DATA transmission before another probe while the agreement remains active. | + +In every case, the agreement remains active while fresh DATA remains queued. Each required DATA observation belongs to the tested agreement. Probes preserve DATA deadlines and add no DATA attempt or duplicate member feedback. Once normal responses resume, the recipient delivers each accepted MSDU once. The cases prove DATA progress under controlled access and loss conditions. They establish no general fairness or goodput bound. + +Expire the members from the failed DATA attempt before probe admission while fresh DATA remains queued. The probe must still transmit. This case proves that member expiry does not erase the agreement's retained probe requirement. A second access category must neither submit that probe nor receive its contention outcome. + +The probe waits for Block Ack. DELBA follows the current Action path and can arrive after that wait. It still retires the agreement. Current lifecycle resolution suppresses obsolete probe completion feedback. + +The probe is a submitted control exchange, not a synthetic input for implicit response calculations. It adds no DATA transmission attempt. Its positive bitmap cannot acknowledge an unsent or already discarded member. A result from a retired local context cannot resolve another exchange. Received frames follow the active receive interval and observable validation rules in [Section 4.7](#47-originator-exchange-timing-and-recovery). No probe result restarts a DATA lifetime. diff --git a/plan/pending/ieee80211-ampdu-phy-reception.md b/plan/pending/ieee80211-ampdu-phy-reception.md new file mode 100644 index 00000000000..aff74f51461 --- /dev/null +++ b/plan/pending/ieee80211-ampdu-phy-reception.md @@ -0,0 +1,408 @@ +# Implementation plan: partial PHY reception for HT A-MPDU + +> **Kind:** procedure · **Status:** draft · **Seal:** none · **Owns:** — · **Stands on:** [S5 execution record](../done/ieee80211-compressed-blockack-s5-execution.md), [implementation plan procedure](../../doc/project/guide/write-an-implementation-plan.md), [IEEE 802.11 rules](../../doc/project/domain/ieee80211.md) + +Checkout: `/home/user/omnetpp_ws/inet-ieee80211-ampdu-implicit-blockack`, branch `work/ieee80211-ampdu-implicit-blockack`. The implementation base is `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. + +Master contains the merged explicit Compressed Block Ack implementation from PR #1345. Rx owns local interframe observations. The radio supplies metadata for Radiotap. The PHY integration must preserve both contracts. + +The feature plan defines PSDU-based RTS selection, recovery before DATA submission, and a boolean probe requirement with a derived access category. This PHY plan supplies its reception prerequisites. MAC implementation and expectation changes remain outside this PR. + +The project plan procedure remains a draft. + +Later commits supply P0 through P4 for the declared PHY profile. The final record states verification and its limits. Only the final commit receives build and test execution. Earlier commits receive structural checks only. MAC acceptance and implicit Block Ack remain pending. + +## 1. Problem and intended behavior + +The WLAN receiver currently produces a whole-data success result. That result cannot describe a failed MPDU beside valid MPDUs in the same A-MPDU. The [MAC feature plan](ieee80211-ampdu-implicit-blockack.md) needs those separate results before it can acknowledge and retry members correctly. + +This hypothetical case defines the required result. One HT PPDU carries MPDUs 100 through 103. PHY acquisition and header reception succeed, but MPDU 101 fails. The receiver must preserve valid outcomes for 100, 102, and 103. Two later reads of that reception result must return the same outcomes without additional random draws. + +The PHY reports reception facts. The MAC separately checks delimiters, FCS values, addresses, and agreement state before acceptance. This plan supplies no Block Ack response or upper delivery by itself. + +```mermaid +flowchart TD + RX["One received HT PPDU"] --> H{"Do PHY acquisition and
header reception succeed?"} + H -->|No| FAIL["Report no valid aggregate content"] + H -->|Yes| R["Obtain bounded delimiter
and MPDU ranges"] + R --> MODEL{"Is an error model present?"} + MODEL -->|Yes| E["Obtain each range probability
from the selected error model"] + MODEL -->|No| GOOD["Record physical success
Retain delimiter and FCS checks"] + E --> C["Sample when necessary
Retain immutable cached outcomes"] + GOOD --> C + C --> I["Radio supplies a typed indication
with copied range outcomes"] + I --> MAC["MAC applies delimiter, FCS,
address, and agreement checks"] +``` + +This diagram describes proposed behavior. It is not an executed trace. + +## 2. Current implementation and evidence + +The source assessment established the facts below at the pinned revision. The companion plan's [standards appendix](ieee80211-ampdu-implicit-blockack.md#appendix-a-standard-requirements-and-evidence) retains the common normative evidence. + +| Source and current behavior | Consequence | +| --- | --- | +| The PER operation in [IErrorModel](../../src/inet/physicallayer/wireless/common/contract/packetlevel/IErrorModel.h) accepts an SNIR object and signal part. It has no arbitrary range-length argument. | An aggregate calculation needs an explicit callable range boundary. A generic whole-DATA call cannot score each member. | +| [Ieee80211ErrorModelBase](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h) declares protected header and DATA success calculations with a bit length. | The shared WLAN implementation can reuse those calculations for range probabilities. | +| [NIST](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h) and [Yans](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.h) derive from that WLAN base. | One base implementation can support both models and compatible custom subclasses. | +| [BerTable](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211BerTableErrorModel.h) derives directly from the generic base. Its [initializer](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211BerTableErrorModel.cc) rejects HT operation strings at the local stage. | BerTable remains outside the HT aggregate profile. Its current HT refusal must remain distinct from the new role check. | +| [Ieee80211Receiver](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc) delegates reception results to the common receiver path. | Aggregate reception needs its own outcomes before whole-DATA failure can discard valid members. | +| [Ieee80211PhyHeader.msg](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg) has no modeled HT aggregation field. | A PHY property must distinguish an A-MPDU from a standalone MPDU. This also applies to a singleton aggregate. | +| The HT methods in [Ieee80211PhyHeaderSerializer](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc) write no fields and reconstruct an empty header. | P1 must extend the typed HT-SIG header and serializer together. A new MSG field alone cannot survive conversion to bytes. | +| [Ieee80211Radio](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc) validates some PHY headers but performs no HT CRC check. | Correct serialization alone cannot establish rejection of an invalid received HT-SIG. P2 needs receive validation before range outcomes. | +| [MpduDeaggregation](../../src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc) creates separate member Packets, but assumes valid lengths and uses the aggregate name. | Shared bounded parsing must replace unchecked range discovery. Feature A4 projects the PHY outcome onto each member Packet. | +| [CommunicationCacheBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/CommunicationCacheBase.cc) stores decision pointers per receiver, transmission, and signal part. It deletes replaced decisions and retained decisions at entry destruction. | The cache can own a derived WLAN decision. Separate signal-part slots need explicit sample ownership. | +| [RadioMedium](../../src/inet/physicallayer/wireless/common/medium/RadioMedium.cc) caches decision lookups, but directly computes `isReceptionSuccessful()`. Its result path requests a WHOLE decision. | Aggregate queries must share cached samples across both paths and signal parts. Generic cache presence alone does not prevent repeated draws. | +| Separate-part reception in [Radio](../../src/inet/physicallayer/wireless/common/radio/packetlevel/Radio.cc) queries PREAMBLE before HEADER. A failed PREAMBLE or HEADER removes the attempted reception. | Preserve the per-part checks without early HEADER or DATA calculation. That aborted attempt supplies no result to MAC. | +| [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) attaches error-rate metadata through the old whole-PSDU calculation. [ReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc) supplies power, SNIR, and reception times. | Aggregate result construction needs an explicit probability contract. It must preserve the other applicable receive metadata. | +| [Rx](../../src/inet/linklayer/ieee80211/mac/Rx.cc) rejects a Packet bit-error flag before its FCS-mode branches. | A failed member can use its own Packet flag without a false checksum or shared chunk mutation. | +| [Ieee80211Receiver](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc) sets its mode set at local initialization. [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) resolves its error model at that stage. | The receiver can check role presence at the physical-layer stage. That stage does not supply the exact limits for capability advertisement. | +| [Ieee80211Mac](../../src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc) calculates effective channel widths and NSS at link-layer initialization, before its MIB update. | Exact coverage validation belongs at that handoff, with the same mode set and effective limits. | +| [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) permits reception without an error model after its SNIR check. The [VHT/HT fixture](../../tests/module/Ieee80211VhtHtSupport_1.test) uses this path with transmit aggregation disabled. | An absent error model is a supported case. The new interface check must preserve it. | +| [Ieee80211Radio](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned) selects NIST by default for scalar and dimensional reception. | D1 needs the shared implementation before it enables the HT interface check. Otherwise, ordinary default HT configurations fail initialization. | +| [Ieee80211Mib](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc) derives local HT capability from the mode set and effective radio limits. This includes short GI. | Receive support must cover those advertised modes before capability publication. HT length exponent zero still advertises 8191 octets. | + +The feature plan and its summary describe the proposed MAC implementation. The execution record supplies results for the completed PHY series. + +## 3. Scope and acceptance criteria + +The scope is packet-level `Ieee80211Radio` with scalar or dimensional reception and HT mixed format at 20 MHz. Reception covers long GI and short GI at the advertised MCS and NSS values. The MAC feature's aggregate transmit policy remains limited to long GI. D1 supplies receiver integration, the shared NIST/Yans implementation, and basic compatibility checks. Deterministic probabilities establish its partial-reception evidence. D2 completes the NIST/Yans and compatibility evidence under the documented scalar SNIR abstraction. + +The plan owns PHY representation, shared bounded delimiter parsing, the optional aggregate error-model role, cached decisions, and receive indications. The MAC feature plan owns member selection, negotiated spacing, canonical MPDU ownership, release rules, responses, and recovery. + +The required results are: + +1. A decoded HT aggregation property survives radio encapsulation and decapsulation. +2. A singleton aggregate retains aggregate identity. +3. Header failure prevents valid aggregate content. +4. A failed delimiter or member does not automatically discard every later recoverable member. +5. Each outcome belongs to a bounded byte range in the actual PSDU. +6. Repeated decision and result access causes no additional random draw. +7. After successful header reception, whole-signal and separate-part reception produce consistent aggregate outcomes. +8. Compatible ordinary MPDU reception preserves its calculations and random draw sequence. +9. A present HT model without the aggregate role or required receive-mode coverage fails before capability advertisement, independently of local transmit aggregation. +10. NIST and Yans share the WLAN base implementation without duplicate aggregate algorithms. +11. Spacing delimiters receive outcomes, but alignment padding consumes no separate random draw. +12. The indication permits a failed member Packet to retain its error without an incorrect whole envelope. +13. The WHOLE summary does not replace the MAC's member validation or its complete receive outcome for contention. +14. Non-HT custom models retain ordinary MPDU reception without the optional aggregate role. +15. An absent error model retains PHY acquisition and SNIR checks without new error draws or an interface requirement. +16. D1 preserves default NIST initialization, configured Yans initialization, and compatible ordinary MPDU reception before D2 extends the evidence. +17. PREAMBLE queries preserve per-part acquisition checks without HEADER or DATA draws. +18. HT-SIG serialization preserves length and aggregation for standalone, singleton, and multiple-member cases. +19. Aggregate error-rate metadata follows the explicit probability contract in Section 4.3. +20. Advertised HT20 short-GI reception has aggregate support even when the local transmit policy uses long GI or selects no aggregate. +21. Invalid HT-SIG CRC or unsupported fields cause HEADER failure before delimiter or member draws. +22. A valid header with insufficient modeled PSDU content causes bounded DATA refusal without range draws. Its HEADER result remains valid. + +This plan excludes correlated FEC errors, bit-level decoding, interference reconstruction within each member, and statistical accuracy or goodput claims. It cannot represent errors confined to alignment padding. It adds no MAC recovery timer, agreement store, or sealed Packet API change. + +## 4. Proposed design and reasons + +### 4.1 Representation and bounded ranges + +Extend the modeled HT PHY header with its aggregation property. The radio derives the receive indication from that property. A transmit-only tag or the member count cannot substitute for decoded state. + +HT-SIG is the HT signal field that carries PSDU length and other PHY control values. The packet-level model represents its fields without a waveform model. + +P1 owns the HT-SIG representation and [HT PHY header serializer](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc). Extend the typed header and its serializer together. The existing `lengthField` remains the sole PSDU length value. The serializer maps that value to HT Length. New typed fields represent the other HT-SIG values, including Aggregation. The radio obtains mode-dependent values from the selected mode. + +IEEE Std 802.11-2024, Table 19-11 and Figure 19-6, define the fields and their positions. Clause 19.3.9.4.4 defines the HT-SIG CRC. Serialization must preserve the standard's field, reserved, CRC, and tail conditions. The encoded header must match the mode's 48-bit HT-SIG length. P1 must check golden bytes and independent reconstruction of standalone, singleton, and multiple-member cases. Full preamble waveform representation and VHT headers remain outside this profile. + +Current [radio encapsulation](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc) already assigns 48 bits from the selected HT header mode. The [HT length calculation](../../src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc) supplies that value despite the empty serializer. P1 preserves this declared length and completes byte conversion within it. Fresh decode must consume those same 48 bits. The [transmitter](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc) calculates airtime from the selected mode and PSDU length. P1 preserves those duration inputs and calculations for ordinary HT MPDUs. + +In a hypothetical singleton case, the radio sets Aggregation to one and HT Length to the complete PSDU length. Conversion to bytes must preserve both values. A fresh decode must recover them without tags, names, or access to the original typed header. The recipient must therefore retain aggregate identity after that conversion. A typed-object copy alone cannot establish this result. + +#### HT-SIG receive validation + +The WLAN PHY owns one stateless HT-SIG validator beside the header representation and serializer. The serializer preserves the received CRC; decode must not replace it with a newly calculated CRC. The radio calculates the transmit CRC after all HT-SIG fields reach their final values. P1 defines this validator and its wire checks. P2 calls it at the receiver's HEADER decision boundary. + +At that boundary, the receiver checks CRC, supported field combinations, and agreement between encoded mode fields and the actual PHY mode. Invalid CRC or unsupported fields cause HEADER failure without delimiter or member draws. The radio reuses the validator before decapsulation relies on HT Length. On failure, decapsulation must not use the untrusted length for PHY trailer removal. + +An invalid header supplies no accepted Aggregation property. Separate-part reception uses the current abort; whole-signal reception uses the current failure-container path. MAC rejects that container before any MAC header or trailer peek. These header checks apply Clause 19.3.21 within the packet-level model without a new radio event or PHY primitive. + +A separate bounds check compares a valid header's HT Length with the available modeled PSDU before range calculation or length-based decapsulation. Insufficient content denotes malformed model input. The HEADER decision retains success and its decoded Aggregation property. The DATA decision reports failure with no range outcomes or range draws. The radio uses the current failure-container path without length-based trailer removal. MAC rejects that refused content before header or trailer peeks. + +This structural refusal does not represent physical signal loss. Clause 19.3.21 distinguishes invalid HT-SIG CRC from carrier loss after a valid header. The short-content fixture proves bounds safety only. It establishes no carrier-loss behavior or acceptance of a physically received prefix. Physical carrier-loss behavior remains outside this plan. + +Bad encoded bytes and probabilistic header loss remain separate test causes. In a hypothetical case, the error model reports header success, but the transmitted HT-SIG has an invalid CRC. The receiver rejects that header before any member draw or Block Ack response. Repeat the case without an error model. A successful probability result cannot override invalid encoded content. + +Extend the current aggregate decoder with bounded delimiter inspection that the WLAN receiver and MAC can reuse. Check every length, alignment interval, zero-length delimiter, CRC, and signature against the available PSDU. Each scan must advance. A malformed length cannot produce an accepted range or an out-of-bounds read. + +The shared parser supplies range facts, not delivery permission. The MAC must apply the received delimiter outcome before it trusts a boundary. A later recovered delimiter does not erase an earlier continuity break. The parser must not use names or a transmitter-private membership list to bypass corruption. + +The indication identifies each scored range by kind, byte offset, byte length, and outcome. It also retains common header success. A spacing delimiter has a four-octet delimiter range and no MPDU range. These values define the member-error projection before P2 starts. + +#### Scored ranges + +These choices define the packet-level error abstraction. They do not claim that the standard specifies independent error draws. + +| PSDU content | Probability and outcome rule | +| --- | --- | +| Each four-octet delimiter. | Score 32 bits once. This includes every zero-length spacing delimiter. Its failure breaks the applicable delimiter-continuity chain. | +| Each nonzero MPDU range. | Score its complete header, payload, and FCS once. Its outcome belongs to that member only. | +| Alignment padding. | Do not score it. It contributes airtime but has no error outcome or separate error draw. | + +Evaluate common acquisition and header success first. Header failure prevents all delimiter and member draws. After header success, process the bounded transmitted ranges in wire order: delimiter, member if present, padding, and the next delimiter. Probabilities zero and one give fixed outcomes without an RNG draw. An intermediate probability consumes one draw. + +A failed delimiter does not stop the physical calculation for known later ranges. MAC delimiter validation and bounded resynchronization still control which members it can accept. The range indication cannot authorize a jump through a failed delimiter. + +The wire fixture covers HT-SIG serialization, validation, delimiters, member limits, alignment, and bounded recovery. Feature A1 owns capability propagation and aggregate construction calculations for spacing, serialization, and member layout. Feature A3 owns dynamic member selection, spacing at the retained DATA mode, batch reduction, and canonical member ownership. + +### 4.2 Optional role and shared implementation + +Add a paired pure C++/NED WLAN contract for aggregate error probabilities. Its caller supplies the selected mode, modeled SNIR, and bounded delimiter or member ranges. The role distinguishes supported probabilities, invalid input, and unsupported aggregate reception. A pure mode-support query lets the receiver check required coverage before capability publication. That query performs no probability calculation or random draw. + +An implementation must support every advertised HT20 receive mode, whether it uses long GI or short GI. The MAC supplies its mode set, effective channel widths, and NSS limit to the receiver's coverage operation before the MIB update. These are the same values that the MAC supplies to `updateLocalHtCapabilities()`. Validation follows that operation's current MCS and GI rules. It adds no second capability store or independent filter. Unsupported input denotes modes or reception detail outside this contract. + +Table 9-224 defines short-GI support for reception of HT PPDUs. It supplies no separate short-GI permission for standalone MPDUs and aggregates. A model cannot advertise short-GI reception and refuse an otherwise valid short-GI aggregate. HT40 and VHT aggregate reception remain outside this plan. A later extension of advertised receive modes requires receiver support for those modes before advertisement. + +Keep the generic `IErrorModel` API free of WLAN dependencies. The receiver calls aggregate probability operations only for aggregate reception. When an error model is present, HT operation requires the role because advertised HT capability includes aggregate reception. Non-HT custom models retain their ordinary MPDU path without the new role. Custom HT models need the role even when local transmit aggregation remains disabled. + +This plan adds no aggregate role to `Ieee80211BerTableErrorModel`. Its current HT refusal precedes the new role check. The plan claims no loss of supported HT reception through BerTable. P0 records that boundary and a non-HT parent control. P2 compares the control and verifies the expected HT refusal. Tests for the new role diagnostic use the custom model, whose local initialization permits HT operation. + +Place the reusable probability calculation in `Ieee80211ErrorModelBase`. That implementation calls the current protected `getDataSuccessRate()` for each range length at the selected scalar SNIR. The pure contract contains no default method body. NIST, Yans, and compatible custom subclasses inherit the shared implementation. P2 includes this implementation before it enables the HT compatibility check. P3 extends its evidence rather than its interface availability. + +An absent error model retains the current error-free abstraction after PHY acquisition, possibility, attempt, and SNIR checks. After those checks succeed, the receiver records successful physical outcomes for bounded delimiter and member ranges without error-model calls or random draws. Delimiter CRC, bounds, and member FCS checks still control MAC acceptance. A failed common PHY check still prevents valid aggregate content. This case adds no substitute error-model module or support flag. + +The deterministic test model implements the same role with probabilities of zero or one. Thus, tests control error conditions while the production receiver still creates the reception outcomes. Models outside the WLAN base can supply their own implementation. + +The new role has concrete substitutes: the deterministic model and the shared WLAN implementation for NIST/Yans. A new callable boundary is necessary because the public generic API cannot accept an arbitrary member length. No synthetic reception object or temporary PHY length change supplies that absent argument. + +### 4.3 Decision ownership and random draws + +The current [medium lookup](../../src/inet/physicallayer/wireless/common/medium/RadioMedium.cc) computes a decision on a cache miss. The [cache implementation](../../src/inet/physicallayer/wireless/common/base/packetlevel/CommunicationCacheBase.cc) retains the decision's dynamic type. Its key contains the receiver, transmission, and signal part. A result lookup can request WHOLE after a DATA lookup. The direct success-query path can also bypass the decision cache. + +P0 must confirm these paths against the merged implementation base. Record cache insertion, replacement, entry destruction, and result consumption. The current `receivePacket()` consumes the cached result but leaves its decision entries. The sample guarantee applies for as long as those entries exist. The copied receive indication remains valid after cache cleanup. + +For aggregates, retain a PREAMBLE decision in its current cache slot. Its calculation preserves current per-part acquisition, possibility, attempt, and SNIR checks. It does not request HEADER or DATA. Shared WLAN NIST/Yans calculations add no PREAMBLE error draw. Direct PREAMBLE queries reuse that decision. + +HEADER first checks the cached PREAMBLE result before its own calculation. It retains common header success once the header decision boundary permits that calculation. An immutable WLAN decision in DATA retains the bounded delimiter and member outcomes, or structural refusal without ranges. A DATA lookup first obtains the cached HEADER result. A failed prerequisite or structural refusal prevents range draws. A WHOLE lookup derives its summary from these entries at the valid result boundary. + +| Query | Required calculation and retained result | +| --- | --- | +| PREAMBLE | Apply current per-part checks. Retain its decision without HEADER or DATA calculation. | +| HEADER | Reuse PREAMBLE. Retain one header outcome at the header boundary. | +| DATA | Reuse HEADER. Refuse insufficient modeled content before range draws. Otherwise, retain each bounded delimiter and member outcome once after successful header reception. | +| WHOLE | Reuse prerequisite decisions. Derive the summary without another error draw. | + +The WHOLE slot owns a separate summary decision. It does not own the DATA pointer. Aggregate result construction reads the DATA outcomes and copies them into the WLAN indication. This design uses the current cache and adds no second outcome store. + +The WLAN receiver must route aggregate `computeIsReceptionSuccessful()` queries through those cached decisions. Its aggregate `computeReceptionDecision()` override constructs misses through calculation helpers. Construction must not call the cache-backed query recursively. Ordinary MPDU branches retain their current calculation and draw order. + +For aggregate DATA, bypass the inherited whole-PSDU error draw. WHOLE success requires common PHY success, valid PSDU bounds, and success for every scored range. This is a PPDU summary only. Aggregate result construction preserves the envelope's intact content after partial DATA failure. It must not invoke whole-Packet corruption or mark the envelope incorrect for one failed member. + +The WHOLE summary does not decide whether the MAC invokes EIFS. Feature A4 applies delimiter, member PHY, and FCS checks before it reports one aggregate outcome for contention. A correct member suppresses EIFS even when the WHOLE summary reports failure. The feature plan owns that rule and the preservation of valid HCF history after partial loss. [Aggregate receive outcome](ieee80211-ampdu-implicit-blockack.md#45-receive-validation-and-one-response-per-ppdu). + +The radio converts retained facts into a typed WLAN indication with copied values. It retains no pointer to a reception or transmission whose lifetime can end first. Feature A4 maps a failed complete member to that member's `Packet::setBitError(true)` before the normal Rx FCS predicate. + +In separate-part reception, a failed PREAMBLE or HEADER makes `Radio::continueReception()` abort the attempted reception. That attempt supplies no result or failure container to MAC. Existing radio state notifications let HCF invalidate its physical history. If whole-signal reception supplies a container after header failure, MAC rejects it before format-specific header or trailer peeks. Its indication reports failure without a decoded aggregation property. Both paths prevent aggregate acceptance, responses, range draws, and stale history without a new notification event. + +#### Aggregate error-rate metadata + +The current [result path](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) obtains `ErrorRateInd` through a whole-PSDU probability calculation. That estimate includes content outside the new scored ranges. It does not describe the selected range model. With a present error model, the aggregate result retains `ErrorRateInd` with `packetErrorRate`, `bitErrorRate`, and `symbolErrorRate` set to `NaN`. The typed WLAN indication separately supplies the retained outcomes. This first profile exposes no aggregate probability estimate. + +The [tag contract](../../src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg) permits `NaN` for unknown probabilities. P0 must trace aggregate results through direct readers, [result filters](../../src/inet/common/ResultFilters.cc), and the [configured radio recorders](../../src/inet/physicallayer/wireless/common/radio/packetlevel/Radio.ned). The [network configurator](../../src/inet/networklayer/configurator/base/L3NetworkConfiguratorBase.cc) requires the tag for an ordinary probe; this read alone establishes no aggregate path. P0 must verify behavior with unknown values and absent tags for reachable consumers. A reachable incompatibility blocks P2's metadata implementation until the plan records its resolution. Tag omission requires evidence that all affected consumers accept absence. + +The aggregate result path must not call the old WHOLE probability operations to populate those fields. It also must not substitute a realized success or failure for a probability. Without an error model, preserve the common base's existing tag presence and exact values. Preserve applicable power, SNIR, reception times, transmission identity, mode, and channel metadata in both cases. Ordinary MPDU reception retains its current metadata calculations and values. + +In a hypothetical partial reception with a present model, MPDU 101 fails beside valid MPDUs 100, 102, and 103. The typed indication reports those distinct outcomes. The aggregate's probability fields remain `NaN` because the first profile supplies no aggregate estimate. A member must not inherit an aggregate probability as its own probability. The receiver needs no probability cache or additional error-model call for this metadata policy. + +### 4.4 Model detail and diagnostics + +The first abstraction has common PHY acquisition and header success, followed by independent delimiter and MPDU outcomes. Each range uses the current scalar SNIR reduction. The calculation adds no separate PHY header to every member. + +This abstraction does not model correlated FEC errors or variation of interference within a member. It cannot represent errors confined to alignment padding. Padding still contributes to PSDU length and PPDU airtime. More detailed error models remain separate work. NIST/Yans integration reuses current calculations; it does not establish new PHY accuracy. + +The WLAN receiver owns the HT compatibility operations. At `INITSTAGE_PHYSICAL_LAYER`, it rejects a present HT model if the aggregate role is absent. This stage follows local error-model initialization. Exact coverage validation occurs later, before the MAC updates local HT capability at `INITSTAGE_LINK_LAYER`. Both checks precede management capability advertisements and apply independently of the local transmit policy. Exponent zero cannot disable advertised A-MPDU reception because it represents 8191 octets under Table 9-225. + +The MAC calculates effective channel widths from transmitter and receiver support. It calculates the NSS limit from antenna count and its mode set. Before `updateLocalHtCapabilities()`, the MAC calls the receiver's pure coverage operation with those exact values and the same mode set. The receiver checks the HT20 MCS and GI combinations that those inputs advertise. Failure prevents the MIB update and capability publication. No probability call or random draw occurs. + +In a hypothetical configuration, one antenna limits advertisement to one spatial stream. The mode catalog also contains modes for more spatial streams. A model with complete support for the advertised modes must pass validation. Unsupported modes outside those effective limits must not cause rejection. This case prevents an incorrect coverage requirement for the full catalog. + +After initialization, `setModeSet()` checks role presence before it accepts an HT set. That setter alone cannot establish exact advertised-mode coverage. The current capability-update caller exists only in MAC initialization. Any later capability-update path must validate its exact inputs before the MIB changes local HT capability. This plan adds no runtime capability-update mechanism or second capability store. + +The check must not clear short-GI capability to obtain model compatibility. A present model must not substitute uniform success or one whole-PSDU result for the required aggregate outcomes. An absent model follows the explicit error-free case in Section 4.2. No new capability bit or mutable support flag represents this local compatibility condition. + +In a hypothetical configuration, a custom model supports ordinary HT MPDUs but lacks the aggregate role. The default transmit policy still selects no A-MPDU. Initialization must fail before any capability advertisement or association, rather than at the first received aggregate. The custom model needs an aggregate implementation before this HT configuration can run. The same model can retain its current non-HT MPDU operation without that role. + +In another hypothetical case, a custom model supplies the role but supports only long GI. The effective limits permit advertised HT20 short-GI reception. The mode-support query must cause initialization failure before the MIB update or advertisement. With complete support, a peer's short-GI aggregate must produce a valid PHY indication. Feature A5 separately verifies MAC receipt and the response. The local transmitter can retain long GI throughout this case. + +### 4.5 Reuse and justified additions + +| Addition | Responsible component and consumer | Reuse decision | +| --- | --- | --- | +| HT-SIG fields, serializer, validator, and typed indication. | WLAN radio and receiver validate header fields before MAC/Rx receives aggregate content. | Current PHY state cannot distinguish singleton aggregates. Its empty HT serializer cannot preserve the new property through bytes. Current radio validation omits HT CRC. | +| Optional aggregate error-model role. | WLAN receiver checks receive-mode support before it calls deterministic or shared WLAN probability implementations. | Generic `IErrorModel` has no range-length argument. Interface presence alone cannot establish coverage of advertised receive modes. | +| Shared probability implementation. | `Ieee80211ErrorModelBase` serves NIST, Yans, and compatible custom subclasses. | Current protected per-length calculations already supply the required arithmetic. | +| Immutable aggregate decision. | Current medium cache owns outcomes for receiver and radio consumers. | Recomputed random decisions would change repeated observations. No second cache is necessary. | +| Aggregate error-rate metadata. | WLAN receiver preserves applicable receive tags and reports unspecified probability fields through `NaN`. | The old whole-PSDU estimate describes a different calculation. This policy needs no new probability store. | +| Bounded delimiter inspection. | Current decoder supplies common range rules to PHY and MAC. | Unchecked lengths cannot establish valid ranges or delimiter continuity. | +| Early HT compatibility checks. | WLAN receiver checks role presence. MAC supplies exact advertised-mode inputs for receiver coverage validation before the MIB update. | PHY initialization lacks the effective advertisement limits. A runtime aggregate diagnostic occurs after capability advertisement. Exponent zero cannot represent absent aggregate reception. | +| Explicit case without an error model. | WLAN receiver preserves current PHY checks and supplies bounded physical outcomes. | The current error-free abstraction needs no new role, module, or random draw. | + +These choices follow [minimal design](../../doc/project/rule/quality.md#qr-design-minimal), [reuse](../../doc/project/rule/architecture.md#ar-ext-reuse), and [contract purity](../../doc/project/rule/architecture.md#ar-org-contract-purity). + +## 5. Affected components and compatibility + +Changes affect MSG inputs for the HT PHY header and WLAN receive tag. They also affect the HT header serializer, receiver, radio, WLAN error-model contracts/base, NIST/Yans declarations, and aggregate decoder. The MAC adds the receiver coverage call before its current MIB update. P2 owns that required consumer with the receiver operation. Generated files follow the normal build procedure. Tests exercise the real medium and receiver. + +HT-SIG serialization completes byte conversion for ordinary HT MPDUs as well as aggregates within the current 48-bit header length. P0 records that declared length and the current mode/PSDU duration path. P1 verifies 48-bit serialization and decode; ordinary HT controls verify unchanged airtime under identical mode and PSDU inputs. Record byte changes separately from reception calculations, frame sequences, and random draws. Any fingerprint change needs its own causal explanation. Any affected recorded expectation follows the current baseline rules and authorization scope. + +D1 changes no default transmit aggregation policy. It includes the shared NIST/Yans implementation before the early HT check becomes active. D1 must retain default HT initialization and compatible ordinary MPDU reception. Its deterministic test model establishes partial-reception integration. Those deterministic results alone do not establish the full NIST/Yans profile; D2 completes that evidence. + +The early diagnostic changes compatibility for present custom HT models without the role or required receive-mode coverage. This includes configurations that previously used only ordinary MPDUs. Those configurations require model migration. Compatible ordinary MPDU calculations retain their current behavior and draws. Non-HT custom configurations need no aggregate implementation. Invalid encoded HT-SIG content now causes reception failure; valid ordinary HT content retains its current calculations. + +BerTable receives no HT support in this delivery. Its current initializer rejects `opMode="n"` and the HT operation strings that the radio supplies before the new role check. The migration guide lists this current limit and directs supported HT configurations to NIST or Yans. It must distinguish the current initializer error from the new diagnostic for an otherwise valid custom HT model. Non-HT BerTable controls compare initialization and reception with the pinned parent; they add no aggregate-role requirement. A failure in the parent remains a prior limitation, with no inferred compatibility pass. + +An absent error model remains valid for ordinary HT operation, including current `Ieee80211UnitDiskRadio` configurations. The receiver preserves their current PHY checks without error draws. The aggregate profile remains scalar or dimensional reception under the declared HT conditions. This compatibility requirement adds no aggregate support claim for unit-disk reception. + +The migration guide must identify the optional role, mode-support query, shared base, typed indication, range semantics, and early HT diagnostics. It must explain the MAC-to-receiver coverage handoff before the MIB update. It must distinguish absent, compatible, and incompatible models, such as a present role with incomplete receive-mode coverage. It must separate HT20 short-GI reception from the long-GI transmit policy. The current [seal registry](../../doc/project/audit/seal-list.md) protects `src/inet/common/packet/`. This plan uses its public API. + +The guide must explain HT-SIG byte conversion, header validation, per-part failure delivery, and the aggregate probability fields. It must distinguish structural PSDU refusal from header failure and physical carrier loss. + +General MAC stop/start recovery remains outside this plan. Current agreement reset and object destruction must safely release new transient metadata through their current owners. + +## 6. Implementation steps and dependencies + +Each step forms a buildable commit or short series with its required consumers and tests. D1 and D2 are separately reviewable deliveries. D1 supplies receiver integration without an intermediate loss of default HT operation. D2 completes the NIST/Yans and compatibility evidence for the declared profile. + +| Step and purpose | Responsible component and proposed change | Expected result and verification | Dependency | +| --- | --- | --- | --- | +| P0: Establish the merged base and cache contract. | Pin the integrated master base. Trace all signal parts, direct success queries, result construction, failure delivery, and cache lifetime. Audit header length, the duration path, BerTable controls, and aggregate metadata consumers. | Record keys, insertion, replacement, destruction, result consumption, and shared-sample paths. Record the current 48-bit header length, duration inputs, and BerTable parent controls. Close the audit of aggregate metadata consumers before P2 implements its contract. Confirm error-model APIs and the feature-off baseline. | The merge prerequisite is satisfied on master. | +| P1: Define HT-SIG representation. | Extend typed HT-SIG state and its serializer together. Define the shared stateless validator. Derive mode fields and aggregate identity during radio encapsulation. | Golden bytes and independent decode preserve HT Length and Aggregation within the current 48-bit header length. Ordinary HT controls preserve airtime for identical mode and PSDU inputs. Wire checks distinguish invalid headers from insufficient modeled PSDU content. | P0 and VHT-PRE. Independent RANGE and RANGE-USE commits supply bounded parsing. | +| P2: Deliver receiver integration and basic compatibility. | Add the optional role, mode-support query, and shared WLAN implementation for NIST/Yans. Preserve reception without an error model. Include the role check and MAC coverage handoff with their required implementations. Apply HT-SIG validation at HEADER and structural content bounds at DATA. Add the deterministic model, per-part decisions, WHOLE summary, cache-backed queries, copied indication, and defined metadata. | Actual reception proves partial loss, delimiter failure, per-part failure delivery, query consistency, cache lifetime, and metadata without duplicate draws or probability calls. Invalid headers and structural refusal prevent range draws through distinct decisions. Advertised HT20 short-GI aggregates produce valid PHY indications. Default NIST, configured Yans, absent-model operation, and non-HT BerTable controls match their documented parent behavior. Incompatible present models fail before advertisement; BerTable's current HT refusal remains distinct. This closes D1 without MAC receipt or response acceptance. | P1. Close the metadata audit before its dependent implementation. Cache reuse, scored ranges, and member-error projection must be defined first. | +| P3: Verify NIST/Yans aggregate outcomes. | Exercise the shared implementation from P2 through the real receiver. Compare range probabilities with the current per-length calculations. | Deterministic reference checks and fixed-seed runs prove cache use, repeatability, and supported model selection. No additional interface implementation is necessary to restore default HT initialization. | D1. Feature A3–A6 need not wait. | +| P4: Close PHY compatibility evidence. | Complete migration text, affected receiver controls, and the execution record. Compare feature-off calculations and draws with the pinned parent. | NIST/Yans and unsupported-model cases match the documented boundary. Scoped checks close D2. Feature A7 requires D2. | P3. | + +### Commit boundaries + +The table assigns each commit its own purpose and verification. Each entry assumes only its stated prerequisites. These entries describe expected results. The final execution record states observed results. + +| ID and subject | Purpose, owner, and affected files | Dependency, expected result, and verification | +| --- | --- | --- | +| PLANS: `plan: add: define HT A-MPDU reception stages` | The three plan documents define PHY and MAC owners, limits, and artifact assignments. | This is the first commit. It changes no runtime behavior, so no protocol fixture or reviewer document applies. Check links and commit assignments. | +| VHT-PRE: `ieee80211: fix: create VHT headers for VHT signal modes` | The VHT mode factory must create the existing VHT header rather than an HT header. Change `Ieee80211VhtMode.h` and its release note. | After PLANS, two production-radio runs must observe a VHT header for long and short GI. Include `WifiVhtHeaderIdentity.test` and `vht-header-identity.md` from the protocol table below. | +| RANGE: `ieee80211: add: inspect bounded MPDU aggregate ranges` | `MpduAggregationUtils` supplies a pure bounded layout query. Add the helper and `tests/unit/Ieee80211MpduLayout_1.test`. | After PLANS, direct cases verify bounds, alignment, truncation, and typed/raw recovery. Existing callers remain unchanged. This pure query adds no protocol transition, so unit checks apply without a protocol reviewer document. | +| RANGE-USE: `ieee80211: fix: recover bounded MPDUs during deaggregation` | `MpduDeaggregation` replaces unchecked member extraction with RANGE results. Add the direct protocol fixture and reviewer document. | After RANGE, complete and malformed aggregates must return only complete bounded members in typed and byte representations. Include `WifiMpduDeaggregation.test` and `mpdu-deaggregation.md`. Extraction grants no MAC acceptance. | +| P1: `ieee80211: add: preserve HT-SIG fields through byte conversion` | The HT MSG header, field helpers, serializer, and radio preserve HT-SIG. Retain the wire unit fixture and production wire protocol fixture. | After VHT-PRE, independent octets verify field positions, CRC, and singleton identity. RANGE is not a prerequisite for serialization. Include `WifiHtAmpduWire.test` and `ampdu-ht-sig-wire.md`. | +| PROB: `ieee80211: add: expose pure HT A-MPDU range probabilities` | The optional role and WLAN error-model base supply pure support queries and probabilities. NIST/Yans NED types declare the role. | After PLANS, the direct probability matrix checks the declared range contract without random draws. No receiver uses the role yet. Pure calculations add no protocol transition, so direct unit checks apply without a protocol reviewer document. | +| P2: `ieee80211: add+change: retain partial HT A-MPDU PHY outcomes` | The receiver, decision values, indication, MAC validation, and Rx integrate cached partial reception. | After RANGE, RANGE-USE, P1, and PROB, production reception must retain a failed middle member beside successful neighbors. Include the module fixtures and `WifiHtAmpduPartialReception.test` with `ampdu-partial-reception.md`. | +| P3: `tests: add: verify seeded NIST and Yans aggregate reception` | The receiver fixture consolidates direct probability checks and adds seeded real-model observations. | After P2, independent generators verify probabilities, samples, draw counts, and cache reuse. This test-only change adds no protocol behavior, so no separate protocol reviewer document applies. | +| P4: `tests: add: verify ordinary PHY compatibility with the parent` | The ordinary compatibility fixture records unchanged reception properties. Migration text states the model abstraction. | After P3, run the 576 ordinary controls. A fresh parent comparison requires separate execution. This test-only change adds no protocol behavior, so no separate protocol reviewer document applies. | +| RECORD: `plan: change: record HT A-MPDU PHY completion on master` | The final plan record states scope, exact verification, and limits. | After P4, verify the final source tree and run the explicit final selectors. Documentation changes add no protocol behavior, so no separate protocol fixture or reviewer document applies. | + +VHT-PRE, RANGE, and PROB can stand as separate prerequisites. RANGE-USE introduces the inspector's first production caller. P1 preserves wire fields independently of delimiter inspection. P2 combines their completed contracts before it supplies copied outcomes to Rx. + +```mermaid +flowchart TD + M["Integrated master
P0: Pin the base"] --> V["VHT-PRE: VHT header identity"] + V --> P1["P1: HT-SIG representation"] + M --> R["RANGE: Bounded inspector"] + R --> U["RANGE-USE: Deaggregation"] + M --> B["PROB: Pure range probabilities"] + P1 --> P2 + U --> P2 + B --> P2 + P1 --> A1["Feature A1 and A3
Capabilities and preparation"] + P2["P2 / D1: Receiver integration
Shared models and basic compatibility"] --> A4 + A1 --> A4["Feature A2 and A4 to A6
Reception, mandatory release, and exchanges"] + P2 --> P3["P3 and P4 / D2
NIST/Yans and PHY compatibility evidence"] + A4 --> A7["Feature A7
Document and verify the supported profile"] + P3 --> A7 +``` + +D1 includes the shared NIST/Yans implementation and basic compatibility checks. D2 can follow deterministic MAC integration because it extends the evidence. That exchange still needs MAC receive step A4 and all applicable release conditions. Completion of the documented profile requires D2 and feature A7. + +## 7. Verification methods and expected results + +### Protocol artifacts assigned to the behavior commits + +VHT-PRE, RANGE-USE, P1, and P2 each include their protocol fixture and reviewer document. The documents explain the requirement, setup, stimulus, assertions, expected parent result, and coverage limits. + +| Commit ID and proposed subject | Purpose, owner, and parent comparison | Exact protocol artifacts and verification | +| --- | --- | --- | +| VHT-PRE: `ieee80211: fix: create VHT headers for VHT signal modes` | The VHT factory creates an HT object on the parent. Later HT validation would then receive a VHT mode without an HT MCS. Existing module controls check integrated VHT delivery but do not directly assert the header class. | Add `tests/protocol/wifi/common/WifiVhtHeaderIdentity.test` and `tests/protocol/wifi/vht-header-identity.md` in VHT-PRE. At 1 ms, the driver sends an ordinary PSDU through a production scalar radio. Two seed-zero runs select VHT20 MCS 0. Long GI uses one stream; short GI uses two streams from the existing catalog. Observe `transmissionStarted`. Require VHT class, exclude HT class, and compare the selected header length before the tester event. The parent fails the class check. Select `WifiVhtHeaderIdentity\.test$`. Typed transmission only; no serializer or delivery claim applies. | +| RANGE-USE: `ieee80211: fix: recover bounded MPDUs during deaggregation` | The parent has RANGE but retains unchecked extraction. An overlong delimiter can cause an out-of-bounds extraction instead of recovery. No existing protocol fixture directly tests this production extraction boundary. | Add `tests/protocol/wifi/common/WifiMpduDeaggregation.test` and `tests/protocol/wifi/mpdu-deaggregation.md`. Four seed-zero runs cover complete and overlong layouts as typed and byte representations. The driver calls production deaggregation, verifies exact returned members, and deletes all outputs. A valid first member precedes a malformed 128-octet segment. A later 20-octet member begins at offset 212. The parent cannot complete the required recovery. Select `WifiMpduDeaggregation\.test$`. This tests extraction, not radio reception or MAC acceptance. | +| P1: `ieee80211: add: preserve HT-SIG fields through byte conversion` | The radio and serializer must preserve singleton aggregation through byte conversion. The parent declares a 48-bit HT header but supplies no HT-SIG octets. After P1, a transmitted singleton must match the independent six-octet vector. This entry depends on P0 and VHT-PRE. | Add `tests/protocol/wifi/common/WifiHtAmpduWire.test` and `tests/protocol/wifi/ampdu-ht-sig-wire.md` in P1. Two seed-zero runs submit a 104-octet singleton PSDU through the production radio. Typed and raw bytes must preserve MCS 0, 20 MHz, long GI, Aggregation, Length, and CRC. Select `WifiHtAmpduWire\.test$` with the debug protocol runner. A missing transmission or different byte vector fails. No existing protocol case establishes this path. | +| P2: `ieee80211: add+change: retain partial HT A-MPDU PHY outcomes` | The receiver must retain separate outcomes, and Rx must reject containers before standalone checks. This entry assumes P1 wire representation and the preceding probability-role commit. The parent has no copied per-range indication. After P2, the middle failed MPDU must retain correct neighbors at the Rx drop. | Add `tests/protocol/wifi/common/WifiHtAmpduPartialReception.test` and `tests/protocol/wifi/ampdu-partial-reception.md` in P2. Eight seed-zero runs cover both GIs, typed and raw bytes, and whole and separate reception parts. Production radios receive three members. Only the 101-octet middle member fails. The protocol tester requires the verified drop followed by an accepted CTS NAV update. Select `WifiHtAmpduPartialReception\.test$` with the debug protocol runner. No existing protocol case establishes these copied outcomes. | + +These fixtures add protocol observations to the unit and module coverage. VHT-PRE checks transmitted header identity. RANGE-USE checks production extraction. P1 checks HT-SIG bytes. P2 checks physical reception and the current MAC rejection boundary. None establishes member delivery or implicit Block Ack. + +Build the current debug library before the explicit protocol selectors. The execution record states observed results separately from these expected results. + +The PHY plan owns its unit, module, and protocol fixtures. RANGE supplies `Ieee80211MpduLayout_1.test`. P1 supplies wire checks. P2 supplies receiver and MAC rejection checks. P4 supplies ordinary controls whose source can compile on the parent without new PHY interfaces. The feature plan owns seven proposed fixtures; its MAC features remain separate. + +| Proposed fixture | Boundary and required observations | +| --- | --- | +| `Ieee80211MpduLayout_1.test`, unit. | Direct layout checks cover delimiter lengths, alignment, truncation, final padding, typed error flags, and bounded typed/byte recovery. This fixture calls the pure inspector before production deaggregation uses it. | +| `Ieee80211AmpduMacBoundary_1.test`, module. | Production radio reception reaches default Rx rejection before standalone FCS and MAC header checks. Sixteen cases cover DCF/HCF, typed/raw bytes, scalar/dimensional radios, and both reception-part modes. | +| `Ieee80211AmpduWire_1.test`, unit/serializer. | HT-SIG golden bytes and independent decode preserve length, Aggregation, CRC, and the current 48-bit header length. Cover standalone, singleton, and multiple-member cases. Validator cases reject invalid CRC and unsupported field combinations. The field-limit case refuses an HT Length above 65535 octets. The receiver fixture separately checks insufficient PSDU content. | +| `Ieee80211AmpduPhyReception_1.test`, module. | Actual reception proves all-good, first/middle/last member loss, regular/spacing delimiter failure, all-member failure, per-part header failure, and query consistency. It distinguishes HT-SIG rejection from structural DATA refusal before range draws. It also verifies copied indication lifetime, metadata with reachable consumers, and HT20 short-GI indications without MAC response acceptance. D1 checks NIST/Yans initialization, ordinary reception, absent-model operation, BerTable controls, and exact coverage at the MAC handoff. Absent roles and incomplete advertised coverage fail; unsupported unadvertised modes do not cause failure. D2 extends NIST/Yans aggregate evidence. | +| `Ieee80211AmpduPhyCompatibility_1.test`, module. | The same source runs on the pinned parent and feature branch. NIST, Yans, absent-model, and non-HT custom controls expose exact outcomes, duration, metadata, draw counts, and subsequent random values. Three noise levels and seeds 0, 1, and 2 cover scalar and dimensional reception with whole and separate signal parts. | + +Use deterministic probabilities and seed 0 for the causal cases. Cover whole-signal and separate-part reception with scalar and dimensional models. Verify indication values after the producer's source objects leave their normal lifetime. + +Add a fixed probability between zero and one for cache checks. Record random draw counts before and after repeated reads. Those reads must add no draws. Count error-model calls to distinguish cached access from repeated probability calculations. + +Exercise PREAMBLE, HEADER, DATA, and WHOLE lookups, direct success queries, result creation, and repeated access in each supported reception configuration. At the production PREAMBLE boundary, verify no HEADER or DATA calculation or draw. Repeat that query without a new draw. Query WHOLE before DATA in a separate case at the valid result boundary. Both later lookup orders must use the same retained outcomes and add no duplicate draws. Verify result consumption and cache cleanup against P0's recorded ownership. + +Force PREAMBLE and HEADER failure separately through the production radio in separate-part mode. Verify that the failed attempt supplies no container to MAC. Verify no later range draws, response, accepted member, or retained physical history. In whole-signal mode, verify rejection of any delivered failure container before aggregate or ordinary MAC header parsing. These checks preserve the existing event boundaries and require no synthetic failure delivery. + +With a present model, check all three `ErrorRateInd` fields for `NaN` after aggregate result construction and repeated reads. Count the old WHOLE probability calls; aggregate metadata must add none. Check power, SNIR, reception times, transmission identity, mode, and channel at their existing boundaries. Without a model, compare tag presence and exact values with the current common-base behavior. Ordinary MPDU controls must retain their existing metadata. Member results must never present an aggregate probability as a member estimate. + +P0 records each metadata consumer, its aggregate reachability, and its behavior with unknown probabilities or an absent tag. The PHY module fixture exercises the reachable consumers that P0 identifies, with the configured filters and recorders. An unresolved incompatibility in P0 blocks P2's metadata implementation. A fixture failure blocks D1 until its documented resolution passes. Verify safe behavior with unknown values before D1 closes. Do not replace `NaN` with an absent tag without evidence that affected readers accept absence. + +P1's controls compare ordinary HT header length and PPDU duration with the pinned parent under identical mode and PSDU inputs. Verify the 48-bit header length before byte conversion and after independent decode. Verify unchanged PPDU airtime separately from changed bytes or fingerprint values. The wire fixture supplies representation evidence; the PHY module fixture verifies actual transmission duration. P1 adds ordinary HT duration controls to the PHY module fixture. P2 extends that fixture with partial-reception cases. + +The wire fixture must decode fresh HT-SIG bytes without access to the original typed header. Check Aggregation zero for standalone MPDUs and one for singleton or multiple-member aggregates. Check HT Length against the complete PSDU length. Golden bytes must also establish the reserved, CRC, tail, and field-position conditions in the cited standard. Compare the encoded length with the mode's HT-SIG length. + +Corrupt an encoded HT-SIG bit without repair of its CRC. Separately encode an unsupported field combination with a valid CRC. Run both cases through the production receiver with successful header probabilities and without an error model. Verify HEADER failure, no accepted Aggregation property, and no delimiter or member draws. Preserve the current separate-part abort and whole-signal failure-container paths. + +Separately supply a valid HT-SIG whose HT Length exceeds the available modeled PSDU. Verify HEADER success and preservation of the decoded Aggregation property. Verify DATA refusal with no range outcomes or draws. Verify bounded decapsulation and MAC refusal before header or trailer peeks in both reception modes. Repeat this case without an error model. This malformed-input check establishes no physical carrier-loss result. + +Vary alignment padding without a separate outcome or draw. Add zero-length spacing delimiters and verify their extra scored ranges. A failed spacing delimiter must preserve the continuity break after later recovery. The feature's A4 fixture separately proves rejection under declared-correct and computed FCS modes. + +Feature A4 also verifies EIFS suppression after partial reception and one corruption notification after total failure. Feature A5 owns production member finalization, computed-FCS retry checks, and aggregate cancellation during SIFS. The PHY wire fixture does not establish those MAC transmission results. [Feature acceptance fixtures](ieee80211-ampdu-implicit-blockack.md#proposed-direct-checks). + +P3 also compares range probabilities against each model's current per-length calculation. NIST/Yans runs use seeds 0, 1, and 2 to check invariants and reproducibility. These runs establish no statistical goodput or error-accuracy claim. Repeated cache access must leave later random draws unchanged. + +Feature-off controls compare compatible ordinary MPDU results and subsequent draws with the pinned parent under identical inputs. A non-HT custom model without the role must retain ordinary MPDU reception. Its HT configuration must fail initialization before any capability advertisement, also with the default transmit policy. After initialization, a change to an HT mode set without the required role must fail before receiver acceptance. The feature capability fixture separately proves the absence of management advertisements and association after the initialization failure. + +Use a custom model that exposes the role but supports only long GI. Use exact MAC inputs that permit short-GI advertisement. Verify initialization failure before the MIB changes local HT capability. The mode-support query must add no probability call or random draw. Also verify success with one antenna and unsupported modes outside the effective NSS limit. These checks must use the production MAC handoff and its exact mode set, channel widths, and NSS limit. + +With complete coverage, receive a valid HT20 short-GI aggregate through the production radio. D1 and feature A2 verify the copied PHY indication and member outcomes. Feature A5 separately verifies MAC receipt and one Compressed response after SIFS. The local aggregate transmit policy remains at long GI. Any later capability-update caller must use the same validation boundary; this plan adds no such runtime caller. + +D1 also checks ordinary HT reception with an absent error model and with each shared NIST/Yans implementation. Compare results and subsequent draws with the pinned parent. Retain the current VHT/HT fixture's absent-model configuration. Within the aggregate profile, an absent model produces successful physical ranges only after all common PHY checks succeed. Failed acquisition or SNIR checks still prevent valid aggregate content. Delimiter and FCS rejection remain active without a model, and no error draw occurs. + +The PHY module fixture includes a non-HT BerTable control and an expected HT refusal case. Supply a valid table file so that a file error cannot mask the operation-mode refusal. Compare the non-HT control with P0's parent evidence. Record any parent failure separately from feature regressions. Verify that BerTable's current error at local initialization precedes the new role check. Keep the custom HT model case as the direct proof of that new check. + +The planned commands use the current runners. They cannot establish a result before the fixtures exist: + +```sh +make MODE=debug -j$(nproc) + +opp_run_unit_tests -m debug -f 'Ieee80211AmpduWire_1\.test$' + +opp_run_module_tests -m debug -f 'Ieee80211AmpduPhyReception_1\.test$' +``` + +Preview each selector with `--dry-run`. Record selected files, configurations, seeds, exact commands, HEAD, local source diff, library mode, verdicts, and artifacts. A zero-case selection is `NOT_RUN`. Compare the actual changed receiver dependencies before any broader campaign. + +Use the [gate procedure](../../doc/project/guide/run-the-gates.md) before publication under separate authorization. Coordinate final Wi-Fi coverage with the feature plan so one unchanged revision does not need duplicate broad runs. Release compilation remains required; debug behavioral evidence does not establish release behavioral coverage. + +The [execution record](ieee80211-ampdu-phy-execution.md) records final-commit verification and its limits. Proposed MAC tests supply no executed evidence. + +## 8. Risks, decisions, and implementation scope + +The main risks are accidental whole-PSDU rejection, repeated random draws, unbounded delimiter parsing, and confusion between singleton aggregates and standalone MPDUs. D1 addresses those risks before MAC exchange integration. + +The scalar SNIR and independent-range assumptions are explicit model choices. Mandatory MAC release conditions remain in the feature plan. This split does not make those conditions optional. + +P1 owns HT-SIG representation and validation so that aggregate identity survives byte conversion without acceptance of invalid headers. P2 applies the validator through current HEADER failure paths. A separate DATA bounds check refuses malformed modeled content without a carrier-loss claim. The first profile reports aggregate outcomes through typed metadata and leaves probability estimates unspecified when an error model is present. PREAMBLE caching and current failed-header aborts preserve actual radio boundaries. These choices add no probability store, failure event, or expanded waveform model. + +Feature step A2 accepts D1. This plan owns the PHY implementation work, including the shared NIST/Yans implementation in P2. Broader NIST/Yans evidence can follow deterministic MAC integration. The documented profile requires D2; default HT operation must remain valid at D1. + +Both plans use the integrated master base stated above. Master supplies the explicit Compressed Block Ack lifecycle, local interframe observations, and reversible DATA preparation. + +The merge prerequisite is satisfied. P0 pins the integrated master base. A changed owner, result contract, or model-detail assumption requires a plan revision before the dependent implementation. From b6e769ebfdd8cf0b8b99bf6dcd65879728f3eabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 29 Oct 2026 19:46:15 +0100 Subject: [PATCH 02/24] ieee80211: fix: create VHT headers for VHT signal modes Before this commit, a VHT signal mode creates an HT header. The transmission has the wrong typed header and can enter later HT validation without an HT MCS. After this commit, the VHT signal mode creates the existing VHT header. Its selected length remains unchanged. This prerequisite adds no VHT serializer or aggregate profile. WifiVhtHeaderIdentity.test sends an ordinary PSDU through the production radio and checks the header at transmission start. On the parent, the factory supplies an HT object, so the assertion that requires Ieee80211VhtPhyHeader fails. With this commit, the factory supplies a VHT object. The VHT assertion succeeds, the HT exclusion succeeds, and the header length matches the selected mode. The driver then emits its required completion event. Both guard intervals use these assertions. Protocol verification: tests/protocol/wifi/vht-header-identity.md Protocol comparison: tests/protocol/wifi/vht-header-identity-comparison.md Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: src.ieee80211 | behavior.change.fix | test whatsnew | ieee80211-ampdu-phy-reception --- .../ieee80211-vht-phy-header-identity.txt | 3 + .../ieee80211/mode/Ieee80211VhtMode.h | 2 +- .../wifi/common/WifiVhtHeaderIdentity.test | 137 ++++++++++++++++++ .../wifi/vht-header-identity-comparison.md | 44 ++++++ tests/protocol/wifi/vht-header-identity.md | 62 ++++++++ 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt create mode 100644 tests/protocol/wifi/common/WifiVhtHeaderIdentity.test create mode 100644 tests/protocol/wifi/vht-header-identity-comparison.md create mode 100644 tests/protocol/wifi/vht-header-identity.md diff --git a/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt b/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt new file mode 100644 index 00000000000..0e27285065e --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt @@ -0,0 +1,3 @@ +VHT PHY header identity + +VHT signal modes now create the existing VHT PHY header type. This corrects their former use of the HT header type. diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index 0579be2a6ac..91c9055b31f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -100,7 +100,7 @@ class INET_API Ieee80211VhtSignalMode : public IIeee80211HeaderMode, public Ieee virtual const Ieee80211OfdmModulation *getModulation() const override { return modulation; } virtual const Ieee80211VhtCode *getCode() const { return code; } - virtual Ptr createHeader() const override { return makeShared(); } + virtual Ptr createHeader() const override { return makeShared(); } }; /* diff --git a/tests/protocol/wifi/common/WifiVhtHeaderIdentity.test b/tests/protocol/wifi/common/WifiVhtHeaderIdentity.test new file mode 100644 index 00000000000..d967c53390e --- /dev/null +++ b/tests/protocol/wifi/common/WifiVhtHeaderIdentity.test @@ -0,0 +1,137 @@ +%description: +Check the VHT header class at actual transmission start through a production scalar radio. +An ordinary PSDU uses VHT MCS 0 and 20 MHz with an existing catalog mode for each GI. +Two seed-zero runs select long GI and short GI without byte conversion. + +%extraargs: -r 0..1 + +%file: WifiVhtHeaderIdentity.cc +#include "ProtocolTest.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::protocoltest; + +class WifiVhtHeaderIdentityDriver : public cSimpleModule, public cListener +{ + const IIeee80211Mode *selected = nullptr; + bool checked = false; + protected: + void initialize() override { + getModuleByPath("^.sender.radio")->subscribe(transmissionStartedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); + } + void handleMessage(cMessage *message) override { + delete message; + auto modes = Ieee80211ModeSet::getModeSet("ac"); + bool shortGi = par("shortGi").boolValue(); + int numSpatialStreams = shortGi ? 2 : 1; + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getVhtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20) && + mode->getDataMode()->getNumberOfSpatialStreams() == numSpatialStreams && mode->isVhtShortGuardInterval() == shortGi) { + selected = mode; + break; + } + } + ASSERT(selected != nullptr); + auto packet = new Packet("ordinary-psdu", makeShared(std::vector(100, 0))); + packet->addTag()->setMode(selected); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + send(packet, "radioOut"); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + ASSERT(signal == transmissionStartedSignal && !checked && selected != nullptr); + const auto packet = check_and_cast(object)->getPacket(); + const auto header = packet->peekAtFront(); + ASSERT(dynamicPtrCast(header) != nullptr); + ASSERT(dynamicPtrCast(header) == nullptr); + ASSERT(header->getChunkLength() == selected->getHeaderMode()->getLength()); + checked = true; + emit(registerSignal("testVhtHeaderIdentity"), 1L); + } + using cListener::finish; + void finish() override { ASSERT(checked); } +}; +Define_Module(WifiVhtHeaderIdentityDriver); + +Define_ProtocolTest(wifi_vht_header_identity) { + return ProtocolTest("wifi_vht_header_identity") + .once(on("driver").signal("testVhtHeaderIdentity").assertValue(1).within(0.01)); +} + +%file: test.ned +import inet.protocoltest.ProtocolTester; +import inet.mobility.static.StationaryMobility; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple WifiVhtHeaderIdentityDriver { + parameters: + @class(::WifiVhtHeaderIdentityDriver); + bool shortGi; + @signal[testVhtHeaderIdentity](type=long); + gates: output radioOut; +} +module WifiVhtHeaderIdentitySender { + parameters: @networkNode; + gates: input upperLayerIn; input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + connections allowunconnected: + upperLayerIn --> radio.upperLayerIn; + radioIn --> radio.radioIn; +} +network Test { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + tester: ProtocolTester; + driver: WifiVhtHeaderIdentityDriver; + sender: WifiVhtHeaderIdentitySender; + connections: driver.radioOut --> sender.upperLayerIn; +} + +%inifile: omnetpp.ini +[General] +network = Test +sim-time-limit = 2ms +seed-set = 0 +*.tester.testName = "wifi_vht_header_identity" +*.tester.stateSignals = "testVhtHeaderIdentity" +*.driver.shortGi = ${shortGi=false,true} +*.sender.radio.opMode = "ac" +*.sender.radio.bandName = "5 GHz (mapped 20 MHz)" +*.sender.radio.channelNumber = 0 +*.sender.radio.centerFrequency = 5.18GHz +*.sender.radio.bandwidth = 20MHz +*.sender.radio.transmitter.bitrate = -1bps +*.sender.radio.initialRadioMode = "transmitter" +*.sender.radio.sendRawBytes = false +*.sender.radio.antenna.numAntennas = 2 +*.sender.radio.antenna.mobilityModule = "^.^.mobility" +**.vector-recording = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +PROTOCOLTEST wifi_vht_header_identity: PASS +%contains: stdout +Run statistics: total 2, successful 2 +%not-contains: stdout +PROTOCOLTEST wifi_vht_header_identity: FAIL diff --git a/tests/protocol/wifi/vht-header-identity-comparison.md b/tests/protocol/wifi/vht-header-identity-comparison.md new file mode 100644 index 00000000000..f79fc8817e2 --- /dev/null +++ b/tests/protocol/wifi/vht-header-identity-comparison.md @@ -0,0 +1,44 @@ +# VHT header identity: parent and commit comparison + +> **Kind:** report · **Status:** snapshot 2026-10-29 · **Seal:** none · **Owns:** — · **Stands on:** [Test guide](vht-header-identity.md) + +This comparison checks the header class at production transmission start. The same [fixture](common/WifiVhtHeaderIdentity.test) must fail on the parent and pass after the VHT factory correction. The [guide](vht-header-identity.md) explains the production path and its limits. + +## Inputs and expected difference + +| Input | Revision | +| --- | --- | +| Parent source | `c99358b1ba9fa881e4961bdbc43697c31ce1f3b7` | +| Behavior source | `c3be2077d0edaf630735f151dfbbc94ce09f6097` | +| Behavior commit subject | `ieee80211: fix: create VHT headers for VHT signal modes` | + +Both executions use a 100-octet ordinary PSDU, VHT20 MCS 0, typed transmission, and seed zero. Long GI uses one stream. Short GI uses two streams. On the parent, the factory returns an HT header. After the correction, the factory returns a VHT header. The fixture requires that class before it emits its success event. + +Both revisions receive identical fixture bytes. Their fixture SHA-256 is `40b018e42279105dcd1d563afe4853e1fcfff2229b215f7ccb3479f4ce9bcb88`. These source revisions precede the fixture-document amendments. The amendments must preserve each production tree and the shared test library. + +## Observed results + +Both debug builds and both fixture compilations pass. The parent fixture fails during production transmission. The VHT class assertion detects the HT object. The behavior fixture passes both GI runs. The parent failure therefore comes from the changed factory contract, not a missing API or build error. + +| Revision | Build exit | Runner exit | Observed simulation runs | +| --- | ---: | ---: | --- | +| Parent | 0 | 1 | One runtime error. One later run is skipped. | +| Behavior commit | 0 | 0 | Both runs pass. No run is skipped. | + +The parent error includes `dynamicPtrCast(header) != nullptr`. Cmdenv stops after the first error. This comparison therefore establishes the parent failure in the long-GI case. It supplies no executed parent result for short GI. + +## Commands and evidence + +The comparison directory is `/tmp/ampdu-protocol-parent-20261029/checkout`. Each execution installs the same fixture at its original path after source checkout. A fresh debug build precedes each filtered invocation. The temporary descriptor preserves the checked-in protocol settings and points to this directory. It omits an unsupported coverage-report setting. + +```sh +record=/tmp/ampdu-protocol-parent-20261029 +export MAKEFLAGS='--jobserver-style=pipe LN=ln\ -sf' +make MODE=debug -j8 +opp_run_protocol_tests --load "$record/inet-compatible.opp" -p inet -m debug --no-build --no-concurrent \ + -w '^tests/protocol/wifi$' -f 'WifiVhtHeaderIdentity\.test$' +``` + +The `vht-parent/` and `vht-current/` artifact directories contain build logs, runner logs, native results, compressed simulation output, and input hashes. Each `evidence.json` records the exact revision, production tree, command, and exit status. The fixture expects PASS on both revisions. Its parent failure must remain visible without an expected-failure declaration. + +This comparison proves header class selection for these two modes. It does not prove VHT serialization, peer reception, or MAC delivery. The guide supplies the sequence diagram; this record adds no separate interaction. diff --git a/tests/protocol/wifi/vht-header-identity.md b/tests/protocol/wifi/vht-header-identity.md new file mode 100644 index 00000000000..cb40f084d38 --- /dev/null +++ b/tests/protocol/wifi/vht-header-identity.md @@ -0,0 +1,62 @@ +# VHT-PRE: Create VHT headers for VHT signal modes + +This document belongs to **VHT-PRE**, with subject **ieee80211: fix: create VHT headers for VHT signal modes**. Its fixture is [WifiVhtHeaderIdentity.test](common/WifiVhtHeaderIdentity.test). The [PHY plan](../../../plan/pending/ieee80211-ampdu-phy-reception.md) assigns this prerequisite before HT header validation. + +The production radio must preserve the selected mode's PHY family when it creates a typed header. Before this commit, a VHT signal mode creates an `Ieee80211HtPhyHeader`. After this commit, that mode creates the existing `Ieee80211VhtPhyHeader`. This requirement concerns the local mode and header classes. It makes no claim about VHT protocol fields or their byte representation. + +## Original scenario, cause, and correction + +At 1 ms, the driver submits a 100-octet ordinary PSDU to a production scalar radio. Both mode requests select VHT MCS 0 and 20 MHz. The long-GI run selects one spatial stream. The short-GI run selects two spatial streams. Both runs use seed zero and typed transmission. + +These are existing entries in the [`ac` mode catalog](../../../src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc). That catalog has no one-stream short-GI entry for VHT MCS 0 at 20 MHz. The fixture uses two radio antennas to support both selected modes. It changes no production catalog entry. + +The radio asks its transmitter for the selected mode. The radio calls that mode's header factory, sets its length, and places the header before the PSDU. On the original parent, the VHT factory chooses the HT class. The radio therefore transmits an HT header object under a VHT mode. This mismatch exists before later HT validation changes. + +The correction changes the class that the [VHT signal mode](../../../src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h) constructs. The [production radio](../../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc) continues to set the header length from the selected mode. The [header declarations](../../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg) supply separate HT and VHT classes. These source contracts establish the class requirement without a normative IEEE claim. + +## Production observation and assertions + +The fixture uses the production `Ieee80211ScalarRadio` and its scalar medium. A driver connects directly to the radio's upper input gate. The runner supplies the NED paths. The fixture declares its driver's C++ class explicitly. + +The radio uses channel zero in `5 GHz (mapped 20 MHz)` with a 5.18 GHz center frequency. The transmitter uses `bitrate = -1bps` because every packet supplies an explicit valid mode request. This setup avoids the radio's default 11 Mbps bitrate, which the `ac` mode set does not support. + +The driver observes `transmissionStarted`, which confirms an actual transmission. Within that callback, it borrows the transmission's `Packet` and reads its front header. It retains no packet or header pointer after the callback. + +The callback requires all three conditions: + +- The header has type `Ieee80211VhtPhyHeader`. +- The header does not have type `Ieee80211HtPhyHeader`. +- The header length equals the selected mode's header length. + +Only after those assertions pass does the driver emit `testVhtHeaderIdentity = 1`. The protocol tester requires that event. The driver's final assertion also requires an observation. A missing transmission therefore cannot pass through an empty observation. + +```mermaid +sequenceDiagram + participant Driver + participant Radio as Production scalar radio + participant Factory as VHT signal mode + participant Tester as Protocol tester + Driver->>Radio: Ordinary PSDU and VHT mode request at 1 ms + Radio->>Factory: createHeader() + Factory-->>Radio: Typed PHY header + Radio->>Radio: Set selected mode's header length + Radio->>Driver: transmissionStarted with borrowed packet + Driver->>Driver: Require VHT class, exclude HT class, compare length + Driver->>Tester: testVhtHeaderIdentity = 1 after assertions +``` + +On the original parent, the VHT class assertion must fail because the factory creates an HT object. After the correction, both GI runs must pass. These are expected results. No expected-failure declaration weakens either assertion. + +## Verification and limits + +From the INET root, use this exact filtered debug command on the parent or behavior commit: + +```sh +opp_run_protocol_tests --load . -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiVhtHeaderIdentity\.test$' --no-concurrent +``` + +The expected output includes `PROTOCOLTEST wifi_vht_header_identity: PASS` and two successful runs. The [execution record](../../../plan/pending/ieee80211-ampdu-phy-execution.md) describes the installed runner's descriptor limitation. Use its compatible descriptor when that limitation applies. The test does not override the runner's NED paths. + +The results above define the expected observations. The [comparison record](vht-header-identity-comparison.md) reports the parent and commit executions with identical fixture bytes. The diagram agrees with the fixture's gate and callback sequence by source inspection. Mermaid rendering remains unverified. + +Coverage stops at the transmitted typed header. The fixture does not check VHT serialization, byte conversion, protocol fields, waveform construction, aggregate support, peer reception, or MAC delivery. Long GI and short GI exercise the two selected VHT modes without extra permutations. From 320a64461f205bc38144bcca5f2d0bdd4c722c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 3 Nov 2026 16:17:24 +0100 Subject: [PATCH 03/24] ieee80211: fix: recover bounded MPDUs during deaggregation Before this commit, the shared loop stops at an invalid delimiter. In a three-member aggregate, the middle delimiter declares 2000 octets where only 124 remain. The deaggregator returns the first member but loses the later complete member. After this commit, the same loop examines delimiter candidates at four-octet intervals. Explicit byte reinterpretation permits the search to cross typed MAC headers. A later valid delimiter supplies the final complete MPDU without an intermediate layout. WifiMpduDeaggregation.test from this commit requires two outputs for the malformed input: the first and final complete members. On the immediate parent, the bounds check stops extraction after the first member. The two-member count assertion therefore fails. With this commit, the search finds the final delimiter and extracts its twenty-octet member. The count, length, and byte-content assertions succeed, so the driver emits its required completion event. Complete input still returns all three members. The fixture applies these assertions to typed chunks and reconstructed bytes. The unit fixture also compares callback order and offsets across both representations. Extraction alone grants no MAC acceptance. Protocol verification: tests/protocol/wifi/mpdu-deaggregation.md Protocol comparison: tests/protocol/wifi/mpdu-deaggregation-comparison.md Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: src.ieee80211 | behavior.change.fix | test whatsnew | ieee80211-ampdu-phy-reception --- .../ieee80211-bounded-mpdu-deaggregation.txt | 2 +- .../mac/aggregation/MpduDeaggregation.cc | 11 +++- .../mac/aggregation/MpduDeaggregation.h | 1 + .../wifi/common/WifiMpduDeaggregation.test | 10 ++-- .../wifi/mpdu-deaggregation-comparison.md | 46 ++++++++++++++ tests/protocol/wifi/mpdu-deaggregation.md | 60 ++++++++++++++++--- tests/unit/Ieee80211MpduDeaggregation_1.test | 26 ++++++++ 7 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 tests/protocol/wifi/mpdu-deaggregation-comparison.md diff --git a/doc/whatsnew/incompatible/ieee80211-bounded-mpdu-deaggregation.txt b/doc/whatsnew/incompatible/ieee80211-bounded-mpdu-deaggregation.txt index b01628e00d0..c654186dc24 100644 --- a/doc/whatsnew/incompatible/ieee80211-bounded-mpdu-deaggregation.txt +++ b/doc/whatsnew/incompatible/ieee80211-bounded-mpdu-deaggregation.txt @@ -1,3 +1,3 @@ IEEE 802.11 bounded MPDU deaggregation -MPDU deaggregation extracts complete members directly from chunk traversal. It stops at an invalid or overlong delimiter before an out-of-bounds request. Extraction alone grants no PHY success, FCS acceptance, or MAC delivery permission. +MPDU deaggregation extracts complete members directly from chunk traversal. An invalid or overlong delimiter triggers a search for later valid delimiters at four-octet intervals. Recovery crosses typed MPDU headers and retains later complete members. Extraction alone grants no PHY success, FCS acceptance, or MAC delivery permission. diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc index b895cb88cb5..720cba6b5fe 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc @@ -22,12 +22,14 @@ bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, return true; int flags = Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED; Packet aggregate("mpdu-aggregate", packet->peekDataAt(offset, length, flags)); + bool complete = true; // IEEE Std 802.11-2024, 9.7.1: HT uses a 12-bit MPDU length and four-octet subframe alignment. // The final HT subframe has no alignment padding. while (aggregate.getDataLength() > b(0)) { if (aggregate.getDataLength() < B(4)) return false; B position = length - B(aggregate.getDataLength()); + // Recovery can enter a typed MPDU. Interpret its candidate delimiter bytes explicitly. const auto& delimiter = aggregate.popAtFront(B(4), Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED | Chunk::PF_ALLOW_REINTERPRETATION); @@ -35,8 +37,11 @@ bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, delimiter->getLength() >= 0 && delimiter->getLength() <= 4095 && B(delimiter->getLength()) <= aggregate.getDataLength(); visitDelimiter(delimiter, position, valid); - if (!valid) - return false; + if (!valid) { + // IEEE Std 802.11-2024, informative Annex O.2: examine candidates four octets apart. + complete = false; + continue; + } B memberLength(delimiter->getLength()); if (memberLength == B(0)) continue; @@ -51,7 +56,7 @@ bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, if (aggregate.getDataLength() == b(0)) return false; // HT has no final alignment padding. } - return true; + return complete; } std::vector *MpduDeaggregation::deaggregateFrame(Packet *aggregatedFrame) diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h index 09d46c6d90f..1c5b1d9aab8 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h @@ -24,6 +24,7 @@ class INET_API MpduDeaggregation : public IMpduDeaggregation, public cObject * Delimiter validity includes chunk flags and member bounds. It does not * establish MPDU FCS acceptance. Returns false if a delimiter, bounds, or * alignment check fails. + * After an invalid delimiter, the search continues at four-octet intervals. */ static bool visitSubframes(const Packet *packet, B offset, B length, const std::function&, B, bool)>& visitDelimiter, diff --git a/tests/protocol/wifi/common/WifiMpduDeaggregation.test b/tests/protocol/wifi/common/WifiMpduDeaggregation.test index 7e5d61bf780..9ce5548c011 100644 --- a/tests/protocol/wifi/common/WifiMpduDeaggregation.test +++ b/tests/protocol/wifi/common/WifiMpduDeaggregation.test @@ -1,6 +1,6 @@ %description: Check production MPDU extraction from complete and overlong-delimiter aggregates. -An overlong delimiter stops extraction before an out-of-bounds request. +Recovery crosses typed MAC header bytes and retains the later bounded member. Four seed-zero runs cover both layouts as typed chunks and reconstructed bytes. %extraargs: -r 0..3 @@ -54,17 +54,15 @@ class WifiMpduDeaggregationDriver : public cSimpleModule MpduDeaggregation deaggregator; // The production method owns and deletes the input aggregate. auto frames = deaggregator.deaggregateFrame(aggregate); - ASSERT(frames != nullptr && frames->size() == (recovery ? 1 : 3)); + ASSERT(frames != nullptr && frames->size() == (recovery ? 2 : 3)); ASSERT(frames->at(0)->getDataLength() == B(100)); ASSERT(frames->at(0)->peekAllAsBytes()->getBytes() == first); if (!recovery) { ASSERT(frames->at(1)->getDataLength() == B(100)); ASSERT(frames->at(1)->peekAllAsBytes()->getBytes() == middle); } - if (!recovery) { - ASSERT(frames->back()->getDataLength() == B(20)); - ASSERT(frames->back()->peekAllAsBytes()->getBytes() == last); - } + ASSERT(frames->back()->getDataLength() == B(20)); + ASSERT(frames->back()->peekAllAsBytes()->getBytes() == last); for (auto frame : *frames) delete frame; delete frames; diff --git a/tests/protocol/wifi/mpdu-deaggregation-comparison.md b/tests/protocol/wifi/mpdu-deaggregation-comparison.md new file mode 100644 index 00000000000..faaf426115f --- /dev/null +++ b/tests/protocol/wifi/mpdu-deaggregation-comparison.md @@ -0,0 +1,46 @@ +# MPDU deaggregation: parent and commit comparison + +> **Kind:** report · **Status:** snapshot 2026-10-29 · **Seal:** none · **Owns:** — · **Stands on:** [Test guide](mpdu-deaggregation.md) + +This comparison checks production extraction after a malformed delimiter. The same [fixture](common/WifiMpduDeaggregation.test) must fail on the parent and pass after bounded extraction replaces unchecked lengths. The [guide](mpdu-deaggregation.md) explains the input layout, ownership, and recovery path. + +This snapshot applies only to the source revisions below. The revised parent stops at an invalid delimiter. Its recovery failure therefore differs from the historical out-of-bounds error. No build or test verifies the revised series yet. + +## Inputs and expected difference + +| Input | Revision | +| --- | --- | +| Parent source | `dea51e29fc820034b109f57ab2b0b668b1909a88` | +| Behavior source | `adb0b5db0a1fdeeaf89e525aec98ae49a55db48c` | +| Behavior commit subject | `ieee80211: fix: recover bounded MPDUs during deaggregation` | + +Four seed-zero runs combine complete or malformed input with typed chunks or reconstructed bytes. Each aggregate contains 232 octets. The malformed case declares a 2000-octet middle member where only 124 octets remain. The parent requests that unchecked slice. After the correction, the deaggregator returns the first and last complete members. Complete input must return all three members on both revisions. + +Both revisions receive identical fixture bytes. Their fixture SHA-256 is `e10177decf8c3e81f78c40104731a151917cb56043a7be1e796822a280576c4f`. These source revisions precede the fixture-document amendments. The amendments must preserve each production tree and the shared test library. + +## Observed results + +Both debug builds and both fixture compilations pass. On the parent, the two complete-input controls pass. The typed malformed input fails during the unchecked extraction with `length is invalid`. After the correction, all four cases return the required members and pass. The controls separate malformed-input recovery from ordinary extraction. + +| Revision | Build exit | Runner exit | Observed simulation runs | +| --- | ---: | ---: | --- | +| Parent | 0 | 1 | Two controls pass. One malformed case errors. One later case is skipped. | +| Behavior commit | 0 | 0 | All four cases pass. No case is skipped. | + +Cmdenv stops after the first error. This comparison therefore supplies no executed parent result for raw-byte malformed input. The corrected commit passes both malformed representations. + +## Commands and evidence + +The comparison directory is `/tmp/ampdu-protocol-parent-20261029/checkout`. Each execution installs the same fixture at its original path after source checkout. A fresh debug build precedes each filtered invocation. The temporary descriptor preserves the checked-in protocol settings and points to this directory. It omits an unsupported coverage-report setting. + +```sh +record=/tmp/ampdu-protocol-parent-20261029 +export MAKEFLAGS='--jobserver-style=pipe LN=ln\ -sf' +make MODE=debug -j8 +opp_run_protocol_tests --load "$record/inet-compatible.opp" -p inet -m debug --no-build --no-concurrent \ + -w '^tests/protocol/wifi$' -f 'WifiMpduDeaggregation\.test$' +``` + +The `deaggregation-parent/` and `deaggregation-current/` artifact directories contain build logs, runner logs, native results, compressed simulation output, and input hashes. Each `evidence.json` records the exact revision, production tree, command, and exit status. The fixture expects PASS on both revisions. Its parent failure must remain visible without an expected-failure declaration. + +This comparison proves bounded production extraction for the selected layouts. It does not prove radio reception, member FCS acceptance, or MAC delivery. The guide supplies the sequence diagram; this record adds no separate interaction. diff --git a/tests/protocol/wifi/mpdu-deaggregation.md b/tests/protocol/wifi/mpdu-deaggregation.md index a31f67e44cb..1468c327bcf 100644 --- a/tests/protocol/wifi/mpdu-deaggregation.md +++ b/tests/protocol/wifi/mpdu-deaggregation.md @@ -1,21 +1,65 @@ -# Extract bounded MPDUs from an A-MPDU +# Recover complete MPDUs after an invalid delimiter -The production deaggregator must return complete members without an out-of-bounds request. Its [fixture](common/WifiMpduDeaggregation.test) covers typed chunks and reconstructed bytes. The [plan](../../../plan/pending/ieee80211-mpdu-deaggregation.md) assigns the shared traversal and production consumer. +This document belongs to the recovery commit, with subject **ieee80211: fix: recover bounded MPDUs during deaggregation**. Its fixture is [WifiMpduDeaggregation.test](common/WifiMpduDeaggregation.test). The [PHY plan](../../../plan/pending/ieee80211-ampdu-phy-reception.md) assigns this production consumer after bounded chunk traversal. -The aggregate contains members of 100, 100, and 20 octets. Each member follows a four-octet delimiter. These members require no alignment padding. Complete input must return all three exact members. +The deaggregator must extract only member ranges that fit inside the aggregate. Before this commit, traversal stops at an invalid delimiter. After this commit, traversal searches for a later valid delimiter. A malformed delimiter can therefore leave later complete members available for extraction. Extraction does not establish MAC acceptance. -The malformed case changes the middle declared length to 2000 octets. Only 124 octets remain after that delimiter. Before this commit, the deaggregator requests an out-of-bounds slice. After this commit, the shared traversal stops at that delimiter. The deaggregator returns only the first complete member. Delimiter recovery belongs to a separate PR. +## Original scenario, cause, and correction -The traversal pops delimiters and MPDUs from a bounded Packet view. It passes each MPDU chunk directly to the deaggregator callback. The deaggregator creates an output packet and copies its region tags. It deletes the input aggregate after traversal. The driver checks each output and deletes every output packet and the result vector. +The complete aggregate contains three members with lengths 100, 100, and 20 octets. Each member follows a four-octet typed delimiter. The first two members contain a typed `Ieee80211DataHeader` and zero-filled content. The last member contains twenty zero octets. These lengths require no alignment padding, so the aggregate contains 232 octets. -Four seed-zero runs cover complete and malformed input in both representations. The driver emits the required test event only after all assertions and cleanup. Extraction does not establish PHY success, FCS acceptance, or MAC delivery. +The recovery case changes only the middle delimiter's declared length, from 100 to 2000 octets. Its actual member still contains 100 octets, with the same typed header and zero-filled content. The 128-octet suffix starts at that malformed delimiter. Within this suffix, the later member starts at offset 108. Within the complete aggregate, the later member starts at offset 212. + +Before this commit, the deaggregator returns the first member and stops at the malformed delimiter. It cannot return the later complete member. After this commit, the traversal advances four octets for each invalid candidate. It finds the final delimiter and returns its complete member. + +The shared [traversal](../../../src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc) pops chunks from a bounded Packet view. It passes each complete MPDU directly to the deaggregator callback. Recovery can cross typed MAC headers through explicit byte reinterpretation. No intermediate range list controls extraction. The fixture checks this local extraction contract. It makes no additional normative IEEE claim. + +## Setup, stimulus, and assertions + +At 1 ms, a simple driver passes an owned aggregate to the production `MpduDeaggregation::deaggregateFrame` method. This method is the production extraction boundary. It owns and deletes the input aggregate. The fixture calls the production deaggregator without a replacement traversal. + +Four runs combine the complete or recovery layout with typed chunks or reconstructed bytes. Each run uses seed zero. For byte reconstruction, the driver serializes the complete aggregate, deletes that packet, and creates a new packet from its bytes. The typed runs retain the MAC header chunks that delimiter recovery must cross. + +The driver requires these results: + +- The complete layout returns three members with lengths 100, 100, and 20 octets. +- The recovery layout returns two members with lengths 100 and 20 octets. +- Each returned member equals its expected content byte for byte. +- The final twenty-octet member remains available after the malformed middle delimiter in both representations. + +The expected first and middle content comes from separate member packets before aggregate extraction. The final content is a fixed vector of twenty zero octets. Exact lengths and content prevent delimiter bytes, padding, or an overlong slice from passing as a recovered member. + +The driver deletes every returned `Packet` and the returned vector. Only after these checks and disposal does it emit `testMpduDeaggregation = 1`. The protocol tester requires that event. The driver's final assertion also requires completion. A missing call or failed extraction therefore cannot pass through an empty observation. + +```mermaid +sequenceDiagram + participant Driver + participant Consumer as Production deaggregator + participant Traversal as Shared chunk traversal + Driver->>Consumer: Owned complete or malformed aggregate + Consumer->>Traversal: Visit bounded aggregate chunks + loop Each delimiter candidate + Traversal->>Traversal: Check delimiter and member bounds + Traversal-->>Consumer: Pass each complete MPDU chunk + Consumer->>Consumer: Create output and copy region tags + end + Consumer->>Consumer: Delete input aggregate + Consumer-->>Driver: Return owned output packets + Driver->>Driver: Check exact content and delete outputs +``` + +The complete cases must pass before and after the correction. On the new parent, the recovery cases must fail because traversal stops before the final complete member. After the correction, all four cases must pass. These statements describe expected results. No execution result or expected-failure declaration substitutes for the assertions. ## Verification and limits -Build and test execution are deferred. The expected focused command is: +From the INET root, use this exact filtered debug command on the parent or behavior commit: ```sh opp_run_protocol_tests --load . -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiMpduDeaggregation\.test$' --no-concurrent ``` -This command requires a fresh matching debug library. No execution result exists for the revised commits. The fixture does not cover radio reception, upper delivery, every malformed delimiter, or region tags. The unit fixture supplies additional alignment and bounds assertions. +The expected output includes `PROTOCOLTEST wifi_mpdu_deaggregation: PASS` and four successful runs. The [execution record](../../../plan/pending/ieee80211-ampdu-phy-execution.md) describes the installed runner's descriptor limitation. Use its compatible descriptor when that limitation applies. The runner supplies the NED paths. + +The results above define the expected observations. The [comparison record](mpdu-deaggregation-comparison.md) retains historical executions for the former implementation. Those executions do not verify this traversal or its new parent. Build and test execution are deferred for the revised series. The diagram agrees with the fixture's direct call and ownership sequence by source inspection. Mermaid rendering remains unverified. + +The fixture establishes extraction through the production consumer, including bounded recovery across typed MAC header bytes. It does not establish radio reception, delimiter acceptance by the MAC, member FCS acceptance, upper delivery, or implicit Block Ack. It does not test region tags, every malformed delimiter form, or every alignment remainder. The traversal unit fixture supplies broader bounds checks. diff --git a/tests/unit/Ieee80211MpduDeaggregation_1.test b/tests/unit/Ieee80211MpduDeaggregation_1.test index ee427b44c67..3502416f3a9 100644 --- a/tests/unit/Ieee80211MpduDeaggregation_1.test +++ b/tests/unit/Ieee80211MpduDeaggregation_1.test @@ -96,6 +96,32 @@ bool refusedEncoding = false; try { overflow.peekAllAsBytes(); } catch (const cRuntimeError&) { refusedEncoding = true; } ASSERT(refusedEncoding); +Packet recovery("recovery", makeShared(std::vector{0, 0, 0, 0})); +appendMember(recovery, 20); +auto recovered = inspect(&recovery, B(0), B(recovery.getDataLength())); +ASSERT(!recovered.first && recovered.second.size() == 3); +ASSERT(!std::get<3>(recovered.second[0]) && std::get<1>(recovered.second[1]) == B(4)); +for (int prefixLength : {0, 6}) { + Packet typed("typed-recovery"); + if (prefixLength) + typed.insertAtBack(makeShared(std::vector(prefixLength, 0))); + auto delimiter = makeShared(); + delimiter->setLength(2000); + typed.insertAtBack(delimiter); + auto member = makeShared(); + member->setSequenceNumber(SequenceNumberCyclic(0)); + typed.insertAtBack(member); + typed.insertAtBack(makeShared(std::vector(100 - member->getChunkLength().get(), 0))); + appendMember(typed, 20); + auto bytes = typed.peekAllAsBytes()->getBytes(); + Packet independent("byte-recovery", makeShared(bytes)); + auto typedResult = inspect(&typed, B(prefixLength), B(128)); + auto byteResult = inspect(&independent, B(prefixLength), B(128)); + ASSERT(!typedResult.first && typedResult == byteResult); + const auto& last = typedResult.second.back(); + ASSERT(!std::get<0>(last) && std::get<3>(last) && std::get<1>(last) == B(108) && std::get<2>(last) == B(20)); +} +ASSERT(!std::get<0>(incorrectResult.second.back()) && std::get<3>(incorrectResult.second.back())); EV << "MPDU traversal checks passed\n"; %contains: stdout From a64a3b75afce14ad3ad6907e87d8631a4897fb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 29 Oct 2026 19:47:23 +0100 Subject: [PATCH 04/24] ieee80211: add: preserve HT-SIG fields through byte conversion Before this commit, HT header conversion supplies no HT-SIG bytes. A singleton aggregate cannot retain its aggregate identity through byte conversion. The preceding VHT prerequisite already supplies the correct VHT header type. After this commit, the radio assigns HT-SIG fields and CRC from the selected mode and aggregate identity. The serializer preserves all 48 bits. Validation rejects invalid CRC, unsupported fields, and insufficient content before length-based trailer removal. Ordinary valid HT duration inputs remain unchanged. WifiHtAmpduWire.test sends a 104-octet singleton through the production radio. It requires the independent HT-SIG vector 00 68 00 0F B4 01. On the parent, the HT serializer writes zero bits for a 48-bit header. The serialization-length check fails before the expected-byte assertion can succeed. With this commit, serialization produces those six octets. Independent decode also satisfies the Length, Aggregation, MCS, guard-interval, and CRC assertions. Typed and raw-byte transmission use the same checks. The fixture reads new fields through descriptors, so it can compile with the parent's header API. MAC aggregate acceptance remains outside this commit. Protocol verification: tests/protocol/wifi/ampdu-ht-sig-wire.md Protocol comparison: tests/protocol/wifi/ampdu-ht-sig-wire-comparison.md Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-ampdu-phy-reception --- .../ieee80211-ht-sig-representation.rst | 6 + .../ieee80211-vht-phy-header-identity.txt | 6 +- .../packetlevel/Ieee80211HtSignalField.cc | 62 ++++++++ .../packetlevel/Ieee80211HtSignalField.h | 19 +++ .../packetlevel/Ieee80211PhyHeader.msg | 14 ++ .../Ieee80211PhyHeaderSerializer.cc | 20 +++ .../ieee80211/packetlevel/Ieee80211Radio.cc | 17 +++ .../wifi/ampdu-ht-sig-wire-comparison.md | 46 ++++++ tests/protocol/wifi/ampdu-ht-sig-wire.md | 51 +++++++ .../protocol/wifi/common/WifiHtAmpduWire.test | 142 ++++++++++++++++++ tests/unit/Ieee80211AmpduWire_1.test | 76 ++++++++++ 11 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst create mode 100644 src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.cc create mode 100644 src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h create mode 100644 tests/protocol/wifi/ampdu-ht-sig-wire-comparison.md create mode 100644 tests/protocol/wifi/ampdu-ht-sig-wire.md create mode 100644 tests/protocol/wifi/common/WifiHtAmpduWire.test create mode 100644 tests/unit/Ieee80211AmpduWire_1.test diff --git a/doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst b/doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst new file mode 100644 index 00000000000..6d6ee98edf5 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst @@ -0,0 +1,6 @@ +IEEE 802.11 HT-SIG representation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The modeled HT-SIG header preserves all 48 bits through byte conversion. ``lengthField`` supplies HT Length. ``aggregation`` distinguishes an A-MPDU from a standalone MPDU, including singleton aggregates. The radio derives mode fields from the selected mode and calculates the CRC after field assignment. Decode preserves the received CRC. HEADER validation rejects invalid CRC and unsupported fields before delimiter or MPDU outcomes. + +Separate-part reception retains its current abort; whole-signal reception supplies a failed container. DATA separately refuses modeled content shorter than a valid HT Length. This refusal retains the accepted header property but supplies no range outcomes. It represents malformed model input, not physical carrier loss. Decapsulation never uses an invalid header or insufficient content for length-based trailer removal. Ordinary valid HT duration inputs remain unchanged. diff --git a/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt b/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt index 0e27285065e..2e86bcdc5bb 100644 --- a/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt +++ b/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt @@ -1,3 +1,5 @@ -VHT PHY header identity +IEEE 802.11 HT-SIG bytes and aggregate boundaries -VHT signal modes now create the existing VHT PHY header type. This corrects their former use of the HT header type. +HT transmissions now preserve the complete HT-SIG header through byte conversion. Receivers check its CRC, supported fields, and length before length-based trailer removal. Invalid headers mark packets as erroneous. Delimiter inspection supplies bounded MPDU ranges, including singleton aggregates. Ordinary valid HT duration inputs remain unchanged. VHT modes now create their own header type. + +Delimiter recovery now scans across typed MPDU headers. An invalid delimiter no longer causes a chunk conversion error at those headers. Bounded recovery retains later complete MPDU ranges. diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.cc new file mode 100644 index 00000000000..1f01975c047 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.cc @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" + +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" + +namespace inet { +namespace physicallayer { + +uint64_t packIeee80211HtSignalField(const Ieee80211HtPhyHeader& header) +{ + // IEEE Std 802.11-2024, Figure 19-6: each octet holds successive wire bits, LSB first. + auto length = header.getLengthField().get(); + if (length < 0 || length > 65535 || header.getMcs() > 127 || header.getStbc() > 3 || + header.getExtensionSpatialStreams() > 3 || header.getTail() > 63) + throw cRuntimeError("HT-SIG field is outside its encoded range"); + return uint64_t(header.getMcs()) | (uint64_t(header.getChannelWidth40()) << 7) | + (uint64_t(length) << 8) | (uint64_t(header.getSmoothing()) << 24) | + (uint64_t(header.getNotSounding()) << 25) | (uint64_t(header.getReserved()) << 26) | + (uint64_t(header.getAggregation()) << 27) | (uint64_t(header.getStbc()) << 28) | + (uint64_t(header.getFecCoding()) << 30) | (uint64_t(header.getShortGi()) << 31) | + (uint64_t(header.getExtensionSpatialStreams()) << 32) | (uint64_t(header.getCrc()) << 34) | + (uint64_t(header.getTail()) << 42); +} + +uint8_t computeIeee80211HtSignalCrc(const Ieee80211HtPhyHeader& header) +{ + // IEEE Std 802.11-2024, 19.3.9.4.4: process bits 0..33, emit complemented C7 first. + auto bits = packIeee80211HtSignalField(header); + uint8_t remainder = 0xFF; + for (int bit = 0; bit < 34; bit++) { + bool feedback = ((remainder >> 7) ^ (bits >> bit)) & 1; + remainder <<= 1; + if (feedback) + remainder ^= 0x07; + } + uint8_t crc = 0; + uint8_t complemented = ~remainder; + for (int bit = 0; bit < 8; bit++) + crc |= ((complemented >> (7 - bit)) & 1) << bit; + return crc; +} + +bool isIeee80211HtSignalValid(const Ieee80211HtPhyHeader& header, const IIeee80211Mode *mode) +{ + auto htMode = dynamic_cast(mode); + if (htMode == nullptr || header.getChunkLength() != b(48) || !header.isCorrect() || + !header.isComplete() || !header.isProperlyRepresented() || + header.getLengthField() <= B(0) || header.getLengthField() > B(65535) || header.getMcs() > 31 || + header.getStbc() != 0 || header.getFecCoding() || header.getExtensionSpatialStreams() != 0 || + !header.getNotSounding() || !header.getReserved() || header.getTail() != 0) + return false; + // Packet-level profile: BCC, no STBC, no extension streams, no sounding. + auto dataMode = check_and_cast(mode->getDataMode()); + return header.getMcs() == mode->getHtMcsIndex() && + header.getChannelWidth40() == (dataMode->getBandwidth() == MHz(40)) && + header.getShortGi() == (dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) && + header.getCrc() == computeIeee80211HtSignalCrc(header); +} + +} // namespace physicallayer +} // namespace inet diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h new file mode 100644 index 00000000000..e143671561d --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_IEEE80211HTSIGNALFIELD_H +#define __INET_IEEE80211HTSIGNALFIELD_H + +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h" + +namespace inet { +namespace physicallayer { + +INET_API uint64_t packIeee80211HtSignalField(const Ieee80211HtPhyHeader& header); +INET_API uint8_t computeIeee80211HtSignalCrc(const Ieee80211HtPhyHeader& header); +INET_API bool isIeee80211HtSignalValid(const Ieee80211HtPhyHeader& header, const IIeee80211Mode *mode); + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg index 4cbde94bba5..517b1aee4b1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg @@ -138,6 +138,20 @@ class Ieee80211HtPhyPreamble extends Ieee80211PhyPreamble class Ieee80211HtPhyHeader extends Ieee80211PhyHeader { + // IEEE Std 802.11-2024, Table 19-11 and Figure 19-6. + chunkLength = b(48); + uint8_t mcs @bit(7) = 0; + bool channelWidth40 = false; + bool smoothing = true; + bool notSounding = true; + bool reserved = true; + bool aggregation = false; + uint8_t stbc @bit(2) = 0; + bool fecCoding = false; + bool shortGi = false; + uint8_t extensionSpatialStreams @bit(2) = 0; + uint8_t crc = 0; + uint8_t tail @bit(6) = 0; } // IEEE 802.11 21. High Throughput (VHT) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc index 35f4d0ee3a4..332805354fb 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc @@ -9,6 +9,7 @@ #include "inet/common/packet/serializer/ChunkSerializerRegistry.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211OfdmSignalField.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" namespace inet { @@ -195,11 +196,30 @@ const Ptr Ieee80211ErpOfdmPhyHeaderSerializer::deserializeFields(MemoryIn void Ieee80211HtPhyHeaderSerializer::serializeFields(MemoryOutputStream& stream, const Ptr& chunk) const { auto htPhyHeader = dynamicPtrCast(chunk); + auto bits = packIeee80211HtSignalField(*htPhyHeader); + for (int byte = 0; byte < 6; byte++) + stream.writeByte((bits >> (8 * byte)) & 0xFF); } const Ptr Ieee80211HtPhyHeaderSerializer::deserializeFields(MemoryInputStream& stream, const std::type_info&) const { auto htPhyHeader = makeShared(); + uint64_t bits = 0; + for (int byte = 0; byte < 6; byte++) + bits |= uint64_t(stream.readByte()) << (8 * byte); + htPhyHeader->setMcs(bits & 0x7F); + htPhyHeader->setChannelWidth40((bits >> 7) & 1); + htPhyHeader->setLengthField(B((bits >> 8) & 0xFFFF)); + htPhyHeader->setSmoothing((bits >> 24) & 1); + htPhyHeader->setNotSounding((bits >> 25) & 1); + htPhyHeader->setReserved((bits >> 26) & 1); + htPhyHeader->setAggregation((bits >> 27) & 1); + htPhyHeader->setStbc((bits >> 28) & 3); + htPhyHeader->setFecCoding((bits >> 30) & 1); + htPhyHeader->setShortGi((bits >> 31) & 1); + htPhyHeader->setExtensionSpatialStreams((bits >> 32) & 3); + htPhyHeader->setCrc((bits >> 34) & 0xFF); + htPhyHeader->setTail((bits >> 42) & 0x3F); return htPhyHeader; } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 693dc906f55..907a87b3a1f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -10,6 +10,7 @@ #include "inet/common/packet/chunk/BitCountChunk.h" #include "inet/common/ProtocolTag_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" @@ -22,6 +23,7 @@ #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211OfdmSignalField.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -266,6 +268,13 @@ void Ieee80211Radio::encapsulate(Packet *packet) const staticPtrCast(makeShared()) : mode->getHeaderMode()->createHeader(); phyHeader->setChunkLength(b(mode->getHeaderMode()->getLength())); phyHeader->setLengthField(B(packet->getDataLength())); + if (auto htHeader = dynamicPtrCast(phyHeader)) { + htHeader->setMcs(mode->getHtMcsIndex()); + htHeader->setChannelWidth40(mode->getDataMode()->getBandwidth() == MHz(40)); + htHeader->setShortGi(mode->isHtShortGuardInterval()); + htHeader->setAggregation(dynamicPtrCast(packet->peekAtFront()) != nullptr); + htHeader->setCrc(computeIeee80211HtSignalCrc(*htHeader)); + } if (auto ofdmHeader = dynamicPtrCast(phyHeader)) { auto ofdmMode = check_and_cast(mode); ofdmHeader->setRate(ofdmMode->getSignalMode()->getRate()); @@ -306,6 +315,14 @@ void Ieee80211Radio::decapsulate(Packet *packet) const { auto mode = packet->getTag()->getMode(); const auto& phyHeader = popIeee80211PhyHeaderAtFront(packet, b(-1), Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED); + if (auto htHeader = dynamicPtrCast(phyHeader)) { + bool valid = isIeee80211HtSignalValid(*htHeader, mode); + if (!valid || htHeader->getLengthField() > packet->getDataLength()) { + packet->setBitError(true); + packet->addTagIfAbsent()->setProtocol(&Protocol::ieee80211Mac); + return; + } + } if (phyHeader->isIncorrect() || phyHeader->isIncomplete() || phyHeader->isImproperlyRepresented() || !verifyFcs(phyHeader)) packet->setBitError(true); auto indication = packet->getTagForUpdate(); diff --git a/tests/protocol/wifi/ampdu-ht-sig-wire-comparison.md b/tests/protocol/wifi/ampdu-ht-sig-wire-comparison.md new file mode 100644 index 00000000000..756001fd588 --- /dev/null +++ b/tests/protocol/wifi/ampdu-ht-sig-wire-comparison.md @@ -0,0 +1,46 @@ +# HT-SIG bytes: parent and commit comparison + +> **Kind:** report · **Status:** snapshot 2026-10-29 · **Seal:** none · **Owns:** — · **Stands on:** [Test guide](ampdu-ht-sig-wire.md) + +This comparison checks HT-SIG bytes at production transmission start. The same [fixture](common/WifiHtAmpduWire.test) must fail on the parent and pass after HT-SIG serialization exists. The [guide](ampdu-ht-sig-wire.md) explains the independent vector and protocol requirement. + +## Inputs and expected difference + +| Input | Revision | +| --- | --- | +| Parent source | `adb0b5db0a1fdeeaf89e525aec98ae49a55db48c` | +| Behavior source | `24448018e20838b49376e6aee458a52fb54c8fec` | +| Behavior commit subject | `ieee80211: add: preserve HT-SIG fields through byte conversion` | + +Two seed-zero runs send a 104-octet singleton aggregate through typed or raw-byte transmission. Both select HT20 MCS 0 and long GI. Both must produce `00 68 00 0F B4 01`. The parent supplies no HT-SIG bytes. After the correction, production encapsulation and serialization preserve the singleton's Aggregation field and CRC. + +The fixture reads new decoded fields through descriptors. It therefore compiles without the parent's missing getters. Missing fields or wrong bytes still fail during execution. Both revisions receive identical fixture bytes. Their fixture SHA-256 is `9f0893450f251b318b1ae99ce0bae26a1dedf345fd69f17fe7932815f3f71d82`. + +These source revisions precede the fixture amendments. The amendments must preserve each production tree and the shared test library. + +## Observed results + +Both debug builds and both fixture compilations pass. The typed parent case requests HT-SIG bytes at transmission start. Its empty serializer triggers the serialization length check. After the correction, typed and raw-byte transmission both match the independent vector. Independent decode also passes every required field check. + +| Revision | Build exit | Runner exit | Observed simulation runs | +| --- | ---: | ---: | --- | +| Parent | 0 | 1 | One runtime error. One later run is skipped. | +| Behavior commit | 0 | 0 | Both runs pass. No run is skipped. | + +The parent error is `serialized length is incorrect: serialized=0 bit, chunk=48 bit`. This is a production serialization failure. It is not a compile failure from a missing getter. Cmdenv stops after that error, so the raw-byte parent case does not execute. + +## Commands and evidence + +The comparison directory is `/tmp/ampdu-protocol-parent-20261029/checkout`. Each execution installs the same fixture at its original path after source checkout. A fresh debug build precedes each filtered invocation. The temporary descriptor preserves the checked-in protocol settings and points to this directory. It omits an unsupported coverage-report setting. + +```sh +record=/tmp/ampdu-protocol-parent-20261029 +export MAKEFLAGS='--jobserver-style=pipe LN=ln\ -sf' +make MODE=debug -j8 +opp_run_protocol_tests --load "$record/inet-compatible.opp" -p inet -m debug --no-build --no-concurrent \ + -w '^tests/protocol/wifi$' -f 'WifiHtAmpduWire\.test$' +``` + +The `wire-parent/` and `wire-current/` artifact directories contain build logs, runner logs, native results, compressed simulation output, and input hashes. Each `evidence.json` records the exact revision, production tree, command, and exit status. The fixture expects PASS on both revisions. Its parent failure must remain visible without an expected-failure declaration. + +This comparison proves transmit representation and independent decode. It does not prove peer reception, member FCS acceptance, or implicit Block Ack. The guide supplies the sequence diagram; this record adds no separate interaction. diff --git a/tests/protocol/wifi/ampdu-ht-sig-wire.md b/tests/protocol/wifi/ampdu-ht-sig-wire.md new file mode 100644 index 00000000000..0006c12a815 --- /dev/null +++ b/tests/protocol/wifi/ampdu-ht-sig-wire.md @@ -0,0 +1,51 @@ +# PHY P1: Preserve singleton aggregation in HT-SIG bytes + +This document belongs to PHY P1, **ieee80211: add: preserve HT-SIG fields through byte conversion**. Its protocol test is [WifiHtAmpduWire.test](common/WifiHtAmpduWire.test). The [PHY plan](../../../plan/pending/ieee80211-ampdu-phy-reception.md) defines the supported profile. + +Before this commit, the HT serializer supplies no HT-SIG octets. A singleton aggregate can lose its aggregation identity after byte conversion. After this commit, the production radio supplies HT-SIG fields and CRC. The serializer preserves their six-octet representation. This protocol test observes those octets at actual transmission start. + +## Requirement and independent expectation + +IEEE Std 802.11-2024 Figure 19-6 defines HT-SIG fields. Clause 19.3.9.4.4 defines CRC over bits 0 through 33, with complemented C7 first. The canonical nodes are `ieee80211-2024:figure:19-6` and `ieee80211-2024:clause:19.3.9.4.4`. Their physical PDF pages are 3432–3434. These are normative PHY requirements. + +The test sends one four-octet delimiter followed by a 100-octet MPDU representation. HT mixed format uses MCS 0, 20 MHz, one spatial stream, and long GI. The PSDU therefore contains 104 octets. Its Aggregation field must remain true even though the aggregate contains only one member. + +The expected HT-SIG octets are `00 68 00 0F B4 01`. Polynomial long division supplies the fixed CRC value `0x6D`, independently of the production shift register. The unit fixture retains the same independent vector. The protocol fixture adds the production encapsulation and transmission path. + +## Setup, stimulus, and assertions + +The fixture contains one production scalar radio and a test driver. The driver sends the singleton aggregate at 1 ms through the radio's upper input gate. A read-only observer receives the radio's `transmissionStarted` notification. The observer borrows the transmission packet only during that callback. + +Two runs select typed transmission and raw-byte transmission. Both use seed zero and end at 2 ms. The observer requires the same six expected octets in both runs. It also decodes an independent byte packet and checks the 48-bit length, 104-octet PSDU length, MCS, Aggregation, GI, and CRC. + +The decoded-field checks use OMNeT++ descriptors. These descriptors expose generated fields by name. The fixture therefore compiles with the parent's header declaration. The assertions still require every field and the independent byte vector. A missing field fails at runtime. No conditional check accepts the parent's missing capability. + +The protocol tester requires one `testHtAmpduWire` event after all assertions pass. A missing transmission cannot pass through an empty observation. The observer creates no packet or protocol state in the production radio. + +```mermaid +sequenceDiagram + participant Driver + participant Radio as Production radio + participant Audit as Read-only observer + participant Tester as Protocol tester + Driver->>Radio: Singleton aggregate through upper input + Radio->>Radio: Encapsulate and select typed or raw bytes + Radio->>Audit: transmissionStarted + Audit->>Audit: Compare six octets with independent vector + Audit->>Tester: testHtAmpduWire = 1 +``` + +On the parent, the missing serializer cannot produce the six expected HT-SIG octets. On this commit, the byte comparison and independent decode must pass. No expected-failure marker suppresses either requirement. + +## Verification and limits + +Use the current debug library before the selected protocol run: + +```sh +make MODE=debug -j8 +opp_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiHtAmpduWire\.test$' --no-concurrent +``` + +The installed runner can require the temporary compatible descriptor described in the [execution record](../../../plan/pending/ieee80211-ampdu-phy-execution.md). The expected result is two successful runs and `PROTOCOLTEST wifi_ht_ampdu_wire: PASS`. The [comparison record](ampdu-ht-sig-wire-comparison.md) reports the parent and commit executions with identical fixture bytes. + +This test establishes transmit representation. It does not establish peer reception, member FCS acceptance, deaggregation, upper delivery, or implicit Block Ack. The wire unit fixture checks malformed fields. The separate layout fixture checks bounded ranges. The diagram follows the gate and notification paths in the fixture. Diagram rendering remains unverified when no Mermaid renderer is available. diff --git a/tests/protocol/wifi/common/WifiHtAmpduWire.test b/tests/protocol/wifi/common/WifiHtAmpduWire.test new file mode 100644 index 00000000000..15ecc986df7 --- /dev/null +++ b/tests/protocol/wifi/common/WifiHtAmpduWire.test @@ -0,0 +1,142 @@ +%description: +Check HT-SIG octets from a singleton A-MPDU through production radio encapsulation and serialization. +An independent fixed vector defines MCS 0, 20 MHz, long GI, 104 PSDU octets, aggregation, and CRC. +Typed and raw-byte transmissions must produce the same six octets. + +%extraargs: -r 0..1 + +%file: HtAmpduWire.cc +#include "ProtocolTest.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; +using namespace inet::protocoltest; + +class HtAmpduWireDriver : public cSimpleModule, public cListener +{ + bool checked = false; + protected: + void initialize() override { + getModuleByPath("^.sender.radio")->subscribe(transmissionStartedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); + } + void handleMessage(cMessage *message) override { + delete message; + auto packet = new Packet("singleton"); + auto delimiter = makeShared(); + delimiter->setLength(100); + packet->insertAtBack(delimiter); + packet->insertAtBack(makeShared(std::vector(100, 0))); + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const IIeee80211Mode *selected = nullptr; + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20) && !mode->isHtShortGuardInterval()) + selected = mode; + } + ASSERT(selected); + packet->addTag()->setMode(selected); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + send(packet, "out"); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + ASSERT(signal == transmissionStartedSignal && !checked); + auto packet = check_and_cast(object)->getPacket(); + // IEEE Std 802.11-2024, Figure 19-6 and 19.3.9.4.4. Polynomial division supplies this fixed CRC vector. + const std::vector expected{0x00, 0x68, 0x00, 0x0F, 0xB4, 0x01}; + ASSERT(packet->peekDataAt(b(0), B(6))->getBytes() == expected); + Packet independent("independent", makeShared(expected)); + auto header = independent.peekAtFront(B(6)); + ASSERT(header->getChunkLength() == b(48) && header->getLengthField() == B(104)); + // Descriptors keep this fixture source compatible with the parent header. + auto descriptor = cClassDescriptor::getDescriptorFor(header.get()); + auto objectPointer = toAnyPtr(const_cast(header.get())); + auto field = [&](const char *name) { + int index = descriptor->findField(name); + ASSERT(index >= 0); + return descriptor->getFieldValue(objectPointer, index, 0); + }; + ASSERT(field("mcs").intValue() == 0 && field("aggregation").boolValue() && !field("shortGi").boolValue()); + ASSERT(field("crc").intValue() == 0x6D); + checked = true; + emit(registerSignal("testHtAmpduWire"), 1L); + } + using cListener::finish; + void finish() override { ASSERT(checked); } +}; +Define_Module(HtAmpduWireDriver); + +Define_ProtocolTest(wifi_ht_ampdu_wire) { + return ProtocolTest("wifi_ht_ampdu_wire") + .once(on("driver").signal("testHtAmpduWire").assertValue(1).within(0.01)); +} + +%file: test.ned +import inet.protocoltest.ProtocolTester; +import inet.mobility.static.StationaryMobility; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple HtAmpduWireDriver { + parameters: + @class(::HtAmpduWireDriver); + @signal[testHtAmpduWire](type=long); + gates: output out; +} +module HtAmpduWireSender { + parameters: @networkNode; + gates: input in; input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + connections allowunconnected: + in --> radio.upperLayerIn; + radioIn --> radio.radioIn; +} +network Test { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + tester: ProtocolTester; + driver: HtAmpduWireDriver; + sender: HtAmpduWireSender; + connections: driver.out --> sender.in; +} + +%inifile: omnetpp.ini +[General] +network = Test +sim-time-limit = 2ms +seed-set = 0 +*.tester.testName = "wifi_ht_ampdu_wire" +*.tester.stateSignals = "testHtAmpduWire" +*.sender.radio.opMode = "n(mixed-2.4Ghz)" +*.sender.radio.initialRadioMode = "transmitter" +*.sender.radio.sendRawBytes = ${raw=false,true} +*.sender.radio.antenna.mobilityModule = "^.^.mobility" +**.vector-recording = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +PROTOCOLTEST wifi_ht_ampdu_wire: PASS +%contains: stdout +Run statistics: total 2, successful 2 +%not-contains: stdout +PROTOCOLTEST wifi_ht_ampdu_wire: FAIL diff --git a/tests/unit/Ieee80211AmpduWire_1.test b/tests/unit/Ieee80211AmpduWire_1.test new file mode 100644 index 00000000000..e1ce2bda073 --- /dev/null +++ b/tests/unit/Ieee80211AmpduWire_1.test @@ -0,0 +1,76 @@ +%description: +Check HT-SIG golden bytes against IEEE Std 802.11-2024, Figure 19-6 and 19.3.9.4.4. +Fresh byte-backed packets establish independent decode, including singleton identity. + +%includes: +#include "inet/common/packet/Packet.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" + +using namespace inet; +using namespace inet::physicallayer; + +%global: +static const IIeee80211Mode *findMode(int mcs, bool shortGi) +{ + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == mcs && mode->getDataMode()->getBandwidth() == MHz(20) && + mode->isHtShortGuardInterval() == shortGi) + return mode; + } + throw cRuntimeError("Missing test mode"); +} + +static void checkSignal(int length, bool aggregation, int mcs, bool shortGi, const std::vector& golden) +{ + auto header = makeShared(); + header->setLengthField(B(length)); + header->setAggregation(aggregation); + header->setMcs(mcs); + header->setShortGi(shortGi); + header->setCrc(computeIeee80211HtSignalCrc(*header)); + Packet output("output", header); + ASSERT(output.peekAllAsBytes()->getBytes() == golden); + Packet input("fresh", makeShared(golden)); + auto decoded = input.popAtFront(B(6)); + ASSERT(input.getDataLength() == B(0)); + ASSERT(decoded->getLengthField() == B(length)); + ASSERT(decoded->getAggregation() == aggregation); + ASSERT(isIeee80211HtSignalValid(*decoded, findMode(mcs, shortGi))); + auto bad = golden; + bad[0] ^= 1; + Packet corrupt("corrupt", makeShared(bad)); + ASSERT(!isIeee80211HtSignalValid(*corrupt.peekAtFront(B(6)), findMode(mcs, shortGi))); + auto unsupported = makeShared(*decoded); + unsupported->setFecCoding(true); + unsupported->setCrc(computeIeee80211HtSignalCrc(*unsupported)); + ASSERT(!isIeee80211HtSignalValid(*unsupported, findMode(mcs, shortGi))); + auto incomplete = makeShared(*decoded); + incomplete->markIncomplete(); + ASSERT(!isIeee80211HtSignalValid(*incomplete, findMode(mcs, shortGi))); + auto improper = makeShared(*decoded); + improper->markImproperlyRepresented(); + ASSERT(!isIeee80211HtSignalValid(*improper, findMode(mcs, shortGi))); + ASSERT(decoded->getChunkLength() == b(48)); +} + +%activity: +// CRC constants use polynomial long division, independent of the production shift register. +checkSignal(100, false, 0, false, {0x00, 0x64, 0x00, 0x07, 0xFC, 0x00}); +checkSignal(104, true, 0, false, {0x00, 0x68, 0x00, 0x0F, 0xB4, 0x01}); +checkSignal(218, true, 7, true, {0x07, 0xDA, 0x00, 0x8F, 0x18, 0x02}); +checkSignal(65535, true, 31, false, {0x1F, 0xFF, 0xFF, 0x0F, 0x7C, 0x00}); +auto invalidLength = makeShared(); +invalidLength->setLengthField(B(65536)); +Packet excessivePsdu("excessive-psdu", invalidLength); +bool refused = false; +try { excessivePsdu.peekAllAsBytes(); } +catch (const cRuntimeError&) { refused = true; } +ASSERT(refused); +EV << "A-MPDU wire checks passed\n"; + +%contains: stdout +A-MPDU wire checks passed From 4d9ddb1fe6c9dfa5647de4f8c158171d5fba416e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:54:11 +0200 Subject: [PATCH 05/24] physicallayer: add: sample DATA errors in existing chunks Before this commit, the public error-model interface offers whole-packet corruption only after a failed reception decision. The common corruption function repeats its samples until some error occurs. That contract cannot represent one unconditional DATA result with correct and incorrect chunks. After this commit, computeReceivedData() samples each existing DATA chunk once. It returns content of the same length and bytes. Failed chunks receive local incorrect flags; transmitted chunks remain unchanged. An entirely correct result needs no retry. The common probability calculation uses DATA BER. Models can override that calculation with their existing PHY functions. Zero and unit probabilities consume no random number. Invalid probabilities cause a runtime error. DataChunkCorruption_1.test checks correct content, local errors, unchanged bytes, duplication, extraction, and invalid probabilities without a MAC format. The receiver integration follows in the next commit. Existing whole-packet callers retain their current path. Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: src.physicallayer | behavior.add | test whatsnew | ieee80211-ampdu-phy-reception --- ...e80211-pure-ht-aggregate-probabilities.txt | 3 + .../common/base/packetlevel/ErrorModelBase.cc | 31 +++++++ .../common/base/packetlevel/ErrorModelBase.h | 4 + .../common/contract/packetlevel/IErrorModel.h | 7 ++ tests/unit/DataChunkCorruption_1.test | 83 +++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt create mode 100644 tests/unit/DataChunkCorruption_1.test diff --git a/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt b/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt new file mode 100644 index 00000000000..d5165e80e54 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt @@ -0,0 +1,3 @@ +PHY DATA chunk errors + +PHY error models now support unconditional DATA corruption through `IErrorModel::computeReceivedData()`. The operation returns existing chunk content with local error flags. Direct interface implementations must supply this operation. The common base uses DATA BER. A model can override the probability calculation through its existing PHY functions. diff --git a/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.cc b/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.cc index c360253ff80..6cdea4b5fad 100644 --- a/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.cc +++ b/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.cc @@ -56,6 +56,37 @@ bool ErrorModelBase::hasProbabilisticError(b length, double ber) const return dblrand() < 1 - std::pow((1 - ber), length.get()); } +double ErrorModelBase::computeChunkSuccessRate(const ISnir *snir, b bitLength) const +{ + double ber = computeBitErrorRate(snir, IRadioSignal::SIGNAL_PART_DATA); + if (!(ber >= 0 && ber <= 1)) + throw cRuntimeError("DATA chunk corruption requires a valid bit error rate"); + return std::pow(1 - ber, bitLength.get()); +} + +Ptr ErrorModelBase::computeReceivedData(const ISnir *snir, const Ptr& data) const +{ + Enter_Method("computeReceivedData"); + Packet source("transmitted-data", data); + Packet received("received-data"); + b offset(0); + while (auto chunk = source.peekAt(offset, b(-1), Chunk::PF_ALLOW_ALL | Chunk::PF_ALLOW_NULLPTR)) { + double probability = computeChunkSuccessRate(snir, chunk->getChunkLength()); + if (!(probability >= 0 && probability <= 1)) + throw cRuntimeError("Invalid DATA chunk success probability: %g", probability); + bool successful = probability == 1 || (probability != 0 && dblrand() > 1 - probability); + if (!successful) { + auto corruptedChunk = chunk->dupShared(); + corruptedChunk->markIncorrect(); + received.insertAtBack(corruptedChunk); + } + else + received.insertAtBack(chunk); + offset += chunk->getChunkLength(); + } + return received.peekAll(Chunk::PF_ALLOW_ALL); +} + Packet *ErrorModelBase::corruptBits(const Packet *packet, double ber, bool& isCorrupted) const { std::vector corruptedBits; diff --git a/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h b/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h index 4eb89111e72..975c7d19dd6 100644 --- a/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h +++ b/src/inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h @@ -42,6 +42,7 @@ class INET_API ErrorModelBase : public Module, public IErrorModel virtual double getScalarSnir(const ISnir *snir) const; virtual bool hasProbabilisticError(b length, double ber) const; + virtual double computeChunkSuccessRate(const ISnir *snir, b bitLength) const; virtual Packet *corruptBits(const Packet *packet, double ber, bool& isCorrupted) const; virtual Packet *corruptBytes(const Packet *packet, double ber, bool& isCorrupted) const; @@ -50,6 +51,9 @@ class INET_API ErrorModelBase : public Module, public IErrorModel virtual Packet *computeCorruptedPacket(const Packet *packet, double ber) const; virtual Packet *computeCorruptedPacket(const ISnir *snir) const override; + + public: + virtual Ptr computeReceivedData(const ISnir *snir, const Ptr& data) const override; }; } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/IErrorModel.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/IErrorModel.h index fb91a1200a9..dda750586cf 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/IErrorModel.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/IErrorModel.h @@ -26,6 +26,13 @@ class INET_API IErrorModel : public virtual IPrintableObject public: virtual Packet *computeCorruptedPacket(const ISnir *snir) const = 0; + /** + * Samples DATA errors once over the supplied chunk structure. + * The result has the same length and bytes, with incorrect chunks marked. + * The input remains unchanged. All chunks may remain correct. + */ + virtual Ptr computeReceivedData(const ISnir *snir, const Ptr& data) const = 0; + /** * Returns the packet error rate based on SNIR, modulation, FEC encoding * and any other physical layer characteristics. diff --git a/tests/unit/DataChunkCorruption_1.test b/tests/unit/DataChunkCorruption_1.test new file mode 100644 index 00000000000..72816b03044 --- /dev/null +++ b/tests/unit/DataChunkCorruption_1.test @@ -0,0 +1,83 @@ +%description: +Check unconditional DATA corruption without a MAC format or a PHY header. +All-correct and all-incorrect probabilities consume no random numbers. +Incorrect chunks remain local to the result. Existing errors remain incorrect. +The result preserves bytes, length, and errors through packet duplication and extraction. +Invalid probabilities fail without a retry loop. + +%includes: +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h" + +using namespace inet; +using namespace inet::physicallayer; + +%global: +class DataChunkModel : public ErrorModelBase +{ + public: + mutable int calls = 0; + double probability = 1; + double ber = 0; + bool useBer = false; + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return NaN; } + double computeBitErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return ber; } + double computeSymbolErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return NaN; } + double computeChunkSuccessRate(const ISnir *snir, b length) const override { + calls++; + return useBer ? ErrorModelBase::computeChunkSuccessRate(snir, length) : probability; + } +}; + +%activity: +DataChunkModel model; +Packet transmitted("data"); +transmitted.insertAtBack(makeShared(std::vector{1, 2, 3, 4})); +transmitted.insertAtBack(makeShared(B(5))); +auto original = transmitted.peekAll(); +auto correct = model.computeReceivedData(nullptr, original); +ASSERT(model.calls == 2 && correct->isCorrect()); +ASSERT(correct->getChunkLength() == B(9)); +model.probability = 0; +model.calls = 0; +auto incorrect = model.computeReceivedData(nullptr, original); +ASSERT(model.calls == 2 && incorrect->isIncorrect() && original->isCorrect()); +Packet received("data", incorrect); +ASSERT(received.hasBitError() && !received.cPacket::hasBitError()); +ASSERT(received.peekAtFront(B(4), Chunk::PF_ALLOW_ALL)->isIncorrect()); +ASSERT(received.peekDataAt(B(4), B(5), Chunk::PF_ALLOW_ALL)->isIncorrect()); +ASSERT(received.peekAtFront(B(4), Chunk::PF_ALLOW_INCORRECT)->getBytes() == std::vector({1, 2, 3, 4})); +auto copy = received.dup(); +ASSERT(copy->peekData(Chunk::PF_ALLOW_ALL)->isIncorrect()); +delete copy; +model.probability = 1; +ASSERT(model.computeReceivedData(nullptr, incorrect)->isIncorrect()); +Packet raw("raw", makeShared(std::vector{1, 2, 3, 4, 5, 6, 7, 8, 9})); +model.probability = 0; +auto rawData = model.computeReceivedData(nullptr, raw.peekDataAt(B(4), B(5))); +ASSERT(raw.peekData()->isCorrect()); +ASSERT(rawData->getChunkLength() == B(5) && rawData->isIncorrect()); +model.useBer = true; +for (double ber : {0.0, 1.0}) { + model.ber = ber; + ASSERT(model.computeReceivedData(nullptr, original)->isCorrect() == (ber == 0)); +} +for (double probability : {-1.0, 2.0, NaN}) { + model.useBer = false; + model.probability = probability; + bool refused = false; + try { model.computeReceivedData(nullptr, original); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); +} +model.useBer = true; +model.ber = NaN; +bool refused = false; +try { model.computeReceivedData(nullptr, original); } +catch (const cRuntimeError&) { refused = true; } +ASSERT(refused); +EV << "DATA chunk corruption checks passed\n"; + +%contains: stdout +DATA chunk corruption checks passed From eb7e4157e39459c6ab7261b528b49f2add814304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 3 Nov 2026 16:17:25 +0100 Subject: [PATCH 06/24] ieee80211: add+change: retain partial HT PHY errors in chunks Before this commit, the receiver samples an HT aggregate as one packet. It cannot preserve an incorrect payload chunk beside correct content. Rx can inspect that container as a standalone MAC frame and request an invalid header. After this commit, the receiver validates HT-SIG and PSDU bounds before DATA corruption. The IEEE 802.11 error-model base supplies chunk probabilities through its existing DATA calculation. The common error-model operation samples each chunk once without delimiter or MPDU knowledge. The DATA decision retains the received chunks. WHOLE and result queries reuse them. Failed chunks retain local incorrect flags without a change to the transmitted content. For three typed members with an incorrect middle payload, the received packet retains that error beside correct first and final members. Byte conversion removes those boundaries, so raw input has one DATA outcome. The protocol fixture checks both cases at the production Rx drop. A later CTS must still set its 50-microsecond NAV. On the parent, the required indication is absent and the fixture fails at the drop. Fresh parent execution remains separate from final-HEAD verification. Rx rejects aggregate containers before standalone checks. The indication retains header and content facts; chunk flags retain errors. Aggregate error-rate estimates remain unknown with a present model. Ordinary reception keeps its current path. Member acceptance and implicit Block Ack remain separate work. Protocol verification: tests/protocol/wifi/ampdu-partial-reception.md Protocol comparison: tests/protocol/wifi/ampdu-partial-reception-comparison.md Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: src.ieee80211 | behavior.add+change | test whatsnew migration | ieee80211-ampdu-phy-reception --- .../ieee80211-ht-a-mpdu-phy-reception.rst | 22 + .../ieee80211-ht-sig-representation.rst | 6 - ...e80211-pure-ht-aggregate-probabilities.txt | 2 + .../ieee80211-vht-phy-header-identity.txt | 2 - plan/pending/ieee80211-ampdu-phy-execution.md | 124 +++++ src/inet/linklayer/ieee80211/mac/Rx.cc | 5 +- .../mac/aggregation/MpduDeaggregation.cc | 2 + .../Ieee80211AmpduReceptionDecision.h | 33 ++ .../packetlevel/Ieee80211Receiver.cc | 146 +++++- .../ieee80211/packetlevel/Ieee80211Receiver.h | 4 + .../ieee80211/packetlevel/Ieee80211Tag.msg | 9 + .../errormodel/Ieee80211ErrorModelBase.cc | 12 +- .../errormodel/Ieee80211ErrorModelBase.h | 1 + tests/module/Ieee80211AmpduMacBoundary_1.test | 216 ++++++++ .../module/Ieee80211AmpduPhyReception_1.test | 480 ++++++++++++++++++ .../ampdu-partial-reception-comparison.md | 48 ++ .../protocol/wifi/ampdu-partial-reception.md | 60 +++ .../common/WifiHtAmpduPartialReception.test | 223 ++++++++ 18 files changed, 1382 insertions(+), 13 deletions(-) create mode 100644 doc/src/migration-guide/development/ieee80211-ht-a-mpdu-phy-reception.rst delete mode 100644 doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst create mode 100644 plan/pending/ieee80211-ampdu-phy-execution.md create mode 100644 src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h create mode 100644 tests/module/Ieee80211AmpduMacBoundary_1.test create mode 100644 tests/module/Ieee80211AmpduPhyReception_1.test create mode 100644 tests/protocol/wifi/ampdu-partial-reception-comparison.md create mode 100644 tests/protocol/wifi/ampdu-partial-reception.md create mode 100644 tests/protocol/wifi/common/WifiHtAmpduPartialReception.test diff --git a/doc/src/migration-guide/development/ieee80211-ht-a-mpdu-phy-reception.rst b/doc/src/migration-guide/development/ieee80211-ht-a-mpdu-phy-reception.rst new file mode 100644 index 00000000000..8d0511dc081 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-ht-a-mpdu-phy-reception.rst @@ -0,0 +1,22 @@ +IEEE 802.11 HT A-MPDU PHY reception +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Error models use the generic ``IErrorModel::computeReceivedData()`` operation for partial DATA reception. The caller supplies bounded chunk content. The model returns content of the same length with incorrect chunks marked. The operation permits an entirely correct result. It does not parse delimiters or identify MPDUs. + +Custom models that derive from ``ErrorModelBase`` inherit this operation. Its default calculation uses DATA BER and rejects an unavailable BER. ``Ieee80211ErrorModelBase`` overrides the chunk probability calculation through its existing DATA success function. NIST and Yans need no separate aggregation interface. Direct implementations of ``IErrorModel`` must implement the new operation. + +The receiver checks PHY acquisition, header validity, and available PSDU length before it requests DATA corruption. Header failure prevents DATA draws. A valid header whose Length exceeds the modeled PSDU causes structural refusal. This check establishes bounds safety, with no claim about physical carrier loss. + +The DATA decision retains the received chunks. WHOLE and result queries reuse those chunks without more random draws. The receiver replaces only PSDU content in its local packet. The transmitted chunks remain unchanged. Region tags retain their positions. + +``Ieee80211AmpduInd`` contains header success, aggregation identity, and content completeness. Error outcomes reside in chunk correctness flags. The indication contains no parallel range table. The explicit packet bit-error flag denotes failed prerequisites or missing content. ``Packet::hasBitError()`` also reports incorrect chunks, so a partial DATA error makes that query true. + +For example, a three-member aggregate contains an incorrect payload chunk in its middle MPDU. Extraction preserves that chunk, so the middle MPDU reports incorrect content. The other MPDUs can remain correct. The default ``Rx`` still rejects all aggregate containers before standalone header or FCS checks. Member acceptance, upper delivery, and implicit Block Ack remain separate work. + +Chunk structure controls error granularity. Typed input can preserve separate delimiters, headers, payloads, and trailers. ``sendRawBytes`` converts the transmission to one byte chunk. Its DATA view therefore receives one outcome. The receiver does not reconstruct hidden member boundaries for the error model. + +HT-SIG uses all 48 bits for Length, Aggregation, coding fields, and CRC. The receiver validates the received CRC before DATA corruption. Ordinary reception retains its existing whole-packet path. + +With a present model, aggregate ``ErrorRateInd`` fields contain ``NaN`` for unknown probabilities. An absent model preserves the common metadata path and performs no error draws. BerTable retains its current HT configuration limit. + +The tested profile covers HT20 mixed format, either guard interval, and scalar or dimensional radios. Each chunk uses an independent draw for a probability strictly between zero and one. Padding chunks also receive outcomes. The model does not represent correlated FEC errors. It establishes no new error-accuracy or goodput claim. diff --git a/doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst b/doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst deleted file mode 100644 index 6d6ee98edf5..00000000000 --- a/doc/src/migration-guide/development/ieee80211-ht-sig-representation.rst +++ /dev/null @@ -1,6 +0,0 @@ -IEEE 802.11 HT-SIG representation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The modeled HT-SIG header preserves all 48 bits through byte conversion. ``lengthField`` supplies HT Length. ``aggregation`` distinguishes an A-MPDU from a standalone MPDU, including singleton aggregates. The radio derives mode fields from the selected mode and calculates the CRC after field assignment. Decode preserves the received CRC. HEADER validation rejects invalid CRC and unsupported fields before delimiter or MPDU outcomes. - -Separate-part reception retains its current abort; whole-signal reception supplies a failed container. DATA separately refuses modeled content shorter than a valid HT Length. This refusal retains the accepted header property but supplies no range outcomes. It represents malformed model input, not physical carrier loss. Decapsulation never uses an invalid header or insufficient content for length-based trailer removal. Ordinary valid HT duration inputs remain unchanged. diff --git a/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt b/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt index d5165e80e54..c43a26eda6c 100644 --- a/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt +++ b/doc/whatsnew/incompatible/ieee80211-pure-ht-aggregate-probabilities.txt @@ -1,3 +1,5 @@ PHY DATA chunk errors PHY error models now support unconditional DATA corruption through `IErrorModel::computeReceivedData()`. The operation returns existing chunk content with local error flags. Direct interface implementations must supply this operation. The common base uses DATA BER. A model can override the probability calculation through its existing PHY functions. + +HT aggregate reception caches the returned chunks. It preserves partial errors without an A-MPDU error-model role or a separate range table. Raw byte input has one DATA outcome because byte conversion removes the original chunk boundaries. Rx still rejects aggregate containers before standalone FCS or MAC header checks. MAC member acceptance remains separate work. diff --git a/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt b/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt index 2e86bcdc5bb..df1a3b02fce 100644 --- a/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt +++ b/doc/whatsnew/incompatible/ieee80211-vht-phy-header-identity.txt @@ -1,5 +1,3 @@ IEEE 802.11 HT-SIG bytes and aggregate boundaries HT transmissions now preserve the complete HT-SIG header through byte conversion. Receivers check its CRC, supported fields, and length before length-based trailer removal. Invalid headers mark packets as erroneous. Delimiter inspection supplies bounded MPDU ranges, including singleton aggregates. Ordinary valid HT duration inputs remain unchanged. VHT modes now create their own header type. - -Delimiter recovery now scans across typed MPDU headers. An invalid delimiter no longer causes a chunk conversion error at those headers. Bounded recovery retains later complete MPDU ranges. diff --git a/plan/pending/ieee80211-ampdu-phy-execution.md b/plan/pending/ieee80211-ampdu-phy-execution.md new file mode 100644 index 00000000000..5233c8d88fe --- /dev/null +++ b/plan/pending/ieee80211-ampdu-phy-execution.md @@ -0,0 +1,124 @@ +# HT A-MPDU recovery: implementation and evidence + +## Current scope + +The deaggregation prerequisite supplies one bounded chunk traversal and its extraction callback. It stops at invalid delimiters. The dependent recovery PR adds delimiter recovery, HT-SIG support, and partial PHY reception. Both use `master` at `f5612791b773bc83f654dbfdefa26e243f756c86` as the upstream base. + +The traversal pops chunks from a bounded Packet view. It passes each delimiter candidate and complete MPDU directly to callbacks. Recovery advances four octets after an invalid delimiter. The PHY error model samples chunk error flags without delimiter or MPDU knowledge. The medium retains those outcomes for repeated queries. The deaggregator copies region tags from absolute source positions into each output packet. + +For an overlong middle delimiter, the prerequisite returns only the first complete member. Recovery also returns the final complete member. Rx still rejects aggregate containers before standalone checks. MAC member acceptance, upper delivery, and implicit Block Ack remain outside these PRs. + +## Verification status on 2026-11-03 + +The maintainer explicitly deferred builds and tests. No build, simulation, unit test, module test, or protocol test ran for this revision. Source inspection and Git comparisons check the intended assignments. They supply no runtime evidence. The revised traversal, new deaggregation boundary, and recovery parent remain unverified. + +The original branch remains available as `backup/pr1363-before-split-20261103` at `a0f9a3519578a8e3465a4b2e4292170704b41f57`. The protocol comparison documents retain their original source revisions. Their observations do not establish a pass for these revised commits. + +## Required follow-up + +1. Build fresh debug and release libraries for both PR heads. +2. Run the focused traversal unit fixture and deaggregation protocol fixture at both behavior boundaries. +3. Run the existing HT-SIG, probability, PHY reception, MAC boundary, and ordinary compatibility fixtures for recovery. +4. Repeat the parent comparisons where the source or parent changes. +5. Run the required project gates before the PRs become ready for review. + +The prerequisite parent can fail malformed extraction with an out-of-bounds error. The recovery parent instead stops before the final member. The same recovery assertions must detect that missing member. New results must distinguish those failure mechanisms. + +## Historical evidence for the original implementation + +The following record applies only to the original implementation and its recorded source revisions. It predates direct chunk callbacks and the PR split. + +# HT A-MPDU PHY implementation and verification + +## Scope + +The branch supplies partial HT A-MPDU reception through PHY stages P0 through P4. Its base is `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. Master contains the explicit immediate Compressed Block Ack implementation from PR #1345. The topic contains ten commits above that base. + +The supported aggregate profile is HT mixed format at 20 MHz with NIST or Yans, with long GI or short GI. The model uses independent chunk error flags at the configured scalar SNIR reduction. It does not model correlated FEC errors or interference changes within one member. Padding chunks also receive outcomes. + +For example, an aggregate contains three MPDUs, and the middle MPDU fails. The receiver preserves the aggregate bytes and each range outcome. Repeated queries reuse those outcomes without extra random draws. Rx rejects the aggregate before standalone FCS or MAC header checks. MAC member acceptance, upper delivery, and implicit Block Ack remain pending. + +## Owners and failure behavior + +The radio constructs HT-SIG fields and CRC. The serializer preserves all 48 bits, including the received CRC. The shared validator rejects invalid CRC and unsupported fields before range calculations. A valid header whose length exceeds the available PSDU fails DATA bounds checks. + +The error model receives bounded DATA content through `IErrorModel::computeReceivedData()`. The common base samples existing chunks once. NIST and Yans reuse their PHY DATA calculations for each chunk bit length. No aggregation role or MAC capability validation applies. An absent model retains common PHY checks without error draws. + +The medium cache owns PREAMBLE, HEADER, DATA, and WHOLE decisions. DATA owns the received chunks and their error flags. WHOLE summarizes DATA. The result packet preserves chunk flags. The indication retains header and content facts. With a present model, aggregate PER, BER, and SER contain `NaN` because this profile supplies no aggregate probability estimate. + +Separate-part reception preserves PREAMBLE or HEADER aborts. Whole-signal reception can return a failed container. Rx rejects containers before standalone checks and excludes them from accepted predecessor history. The radio retains metadata for Radiotap. Ordinary MPDUs retain their metadata path. + +## Verification contract + +The current task requires a protocol regression on each applicable commit and its parent. Each comparison uses identical fixture bytes and fresh debug libraries. A parent must fail during execution. The behavior commit must pass every selected run. Generation, compilation, or link errors do not establish the required parent failure. Stored expectations remain unchanged. + +The earlier verification campaign ran only at its final commit. That execution limit does not apply to the current per-commit comparisons. + +All recorded executions below predate the chunk corruption change. They are historical evidence. The [current execution record](../../ai-logs/executions/2026-11-04_pr1363-chunk-errors.md) owns fresh results. + +## Commit review and protocol coverage + +Four commits change observable protocol behavior. Their fixtures follow the production header factory, deaggregator, transmit serializer, and receive path. The helper commits introduce no production consumer. Their direct calculation tests remain appropriate. Documentation and test-only commits change no protocol behavior, so they need no artificial parent failure. + +| Commit purpose | Protocol test decision | Evidence or reason | +| --- | --- | --- | +| Define the PHY stages. | No protocol regression applies. | The commit adds plans without runtime changes. | +| Create VHT headers for VHT signal modes. | Retain the direct transmission fixture. | [Header comparison](../../tests/protocol/wifi/vht-header-identity-comparison.md) checks the VHT class on both revisions. | +| Inspect bounded MPDU aggregate ranges. | Retain the direct unit fixture. | No production caller uses the new helper in this commit. | +| Recover bounded MPDUs during deaggregation. | Retain the extraction fixture. | [Deaggregation comparison](../../tests/protocol/wifi/mpdu-deaggregation-comparison.md) checks valid input and recovery after an overlong delimiter. | +| Preserve HT-SIG fields through byte conversion. | Make the wire fixture compile on the parent. | [Wire comparison](../../tests/protocol/wifi/ampdu-ht-sig-wire-comparison.md) checks independent bytes through production transmission. | +| Sample DATA errors in existing chunks. | Retain `DataChunkCorruption_1.test`. | The common operation permits correct content and duplicates only failed chunks. | +| Retain partial HT A-MPDU PHY outcomes. | Make the fixture compile on the parent. Extend loss coverage. | [Reception comparison](../../tests/protocol/wifi/ampdu-partial-reception-comparison.md) checks each MPDU position, delimiter loss, and a subsequent CTS. | +| Verify seeded NIST and Yans aggregate reception. | Retain the module campaign. | The commit changes tests without new protocol behavior. | +| Verify ordinary PHY compatibility. | Retain the module controls. | These controls require unchanged behavior on both revisions. | +| Record PHY completion. | Record the results and limits. | The commit changes documents without runtime changes. | + +Each behavior commit includes its fixture, guide, and comparison record. The four comparisons use the original production revisions before these fixture amendments. Structural checks must establish identical production trees after the amendments. The source review found no additional protocol behavior without an applicable fixture. + +The earlier direct scope covers HT-SIG bytes, bounded ranges, NIST/Yans probabilities, cached samples, ordinary metadata, Rx rejection, Radiotap, and local interframe observations. The current comparison scope covers the four protocol fixtures. Explicit file selectors define this scope. Automatic dependency selection remains unverified because this checkout has no dependency store. + +## Observed results + +Verification date: 2026-10-29. Eight debug build invocations prepare seven production revisions for the parent comparisons. Every build and fixture compilation passes. Each parent fixture fails during execution at its intended assertion or production error. Each behavior commit passes its complete selected fixture. Commands, input hashes, logs, and native results reside in `/tmp/ampdu-protocol-parent-20261029/`. + +| Protocol fixture | Parent result | Behavior commit result | +| --- | --- | --- | +| `WifiVhtHeaderIdentity.test` | The VHT class assertion fails. One later run is skipped. | Both runs pass. | +| `WifiMpduDeaggregation.test` | Two complete-input controls pass. Malformed extraction errors. One later run is skipped. | All four runs pass. | +| `WifiHtAmpduWire.test` | Serialization writes zero bits for the 48-bit header. One later run is skipped. | Both runs pass. | +| `WifiHtAmpduPartialReception.test` | The copied indication is absent at the Rx drop. The 31 later runs are skipped. | All 32 runs pass. | + +The four behavior fixtures complete 40 successful runs. They declare no expected failures. Cmdenv stops each parent execution after its first error. Each comparison record identifies the executed parent case and the skipped cases. Those skipped cases supply no parent evidence. + +Earlier debug and release builds passed on the original topic. Retained results cover nine unchanged unit fixtures and fifteen unchanged module fixtures. The module results include 468 aggregate runs, 576 ordinary controls, and sixteen MAC boundary runs. The earlier protocol campaign also covered eight fixtures, with eight partial-reception runs before this extension. These are retained results, not fresh unit, module, or release execution for this task. + +Retained fingerprint controls use run 0 with `tplx`, `~tNl`, and `~tND`. `MacQos` and `MsduAggregation` use ten-second limits. `Aggregation`, `NoAggregation`, and `VoicePriorityAggregation` use one-second limits. The source and fingerprint store remain byte-identical. These matches supply regression evidence, not proof of protocol correctness. + +The production `src` tree remains `8e187a213a126c244a3f49d40047758d7aa3e493`. Each amended commit must retain its original production tree, shared test library, descriptor, unit tests, module tests, and fingerprint files. The comparison records identify the original source revisions. `commit-map.json` records their amended owners. + +The VHT and deaggregation fixture bytes remain unchanged. The wire fixture replaces unavailable getters with descriptors. The reception fixture adds descriptors and four loss patterns. + +The earlier mechanical series and source-seal checks passed. Earlier global architecture, naming, and interface checks reported existing findings without additions against pinned master. Those global results remain retained evidence. The current final procedure repeats the series and source-seal checks. It also checks the preserved local edit and the final protocol selection. Local link validation passes all 87 file links in the selected guides, comparison records, and PHY plan documents. + +The installed runner rejects the coverage-report setting in `inet.opp`. Temporary compatible descriptors omit that setting and retain all selected test settings. The checked-in descriptor remains unchanged. The isolated checkout reuses the active build output. Symbolic output links permit that reuse across filesystems. This setup changes no compiler flag or runtime assertion. + +The final procedure refreshes changed source dependencies before the selected debug build. This step prevents stale objects after reuse of the output by the parent comparisons. The final command selects all four fixtures. Its native result file records the candidate's final-head outcome. The procedure accepts the candidate checkout as its working directory. + +```sh +export MAKEFLAGS='--jobserver-style=pipe LN=ln\ -sf' +make MODE=debug -j8 + +record=/tmp/ampdu-protocol-parent-20261029 +common=(--load "$record/inet-final.opp" -p inet -m debug --no-build --no-concurrent) +opp_run_protocol_tests "${common[@]}" -w '^tests/protocol/wifi$' \ + -f 'Wifi(VhtHeaderIdentity|MpduDeaggregation|HtAmpduWire|HtAmpduPartialReception)\.test$' \ + --result-file "$record/final-protocol.json" +``` + +The result files retain each observed and expected verdict. The artifact directory contains `compare.py`, `comparison.json`, per-revision evidence, `fold.py`, and `verify-final.sh`. The final procedure retains the exact commands, environment, and output paths. Per-revision simulation output remains compressed beside its native result. Retained earlier records reside in `/tmp/ampdu-process-correction-20261029/` and `/tmp/ampdu-master-20261029/`. + +## Limits + +The parent comparisons establish fixture-level regressions. They do not establish parent outcomes for cases skipped after the first runtime error. The behavior commits execute every selected case. Ordinary controls remain retained evidence without a fresh master comparison. This task runs no release behavioral tests, feature-isolation builds, C++ lint, or full inherited-source campaign. + +The new partial-reception cases use scalar radios and HCF. Existing module results retain the separate dimensional-radio and DCF coverage. The fixtures make no new error-accuracy or goodput claim. MAC member acceptance and implicit Block Ack remain outside the implemented scope. diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index 364e7e3568f..23f4993f8d4 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -121,7 +121,10 @@ bool Rx::lowerFrameReceived(Packet *packet) Enter_Method("lowerFrameReceived(\"%s\")", packet->getName()); take(packet); - bool isFrameOk = isFcsOk(packet); + // Aggregate containers need member acceptance before standalone FCS or header checks. + // Reject them until MAC aggregate reception exists; chunk flags retain PHY errors. + auto aggregate = packet->findTag(); + bool isFrameOk = !(aggregate && aggregate->getAggregation()) && isFcsOk(packet); attachInterframeResult(packet, isFrameOk); if (isFrameOk) { EV_INFO << "Received frame from PHY: " << packet << endl; diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc index 720cba6b5fe..ca3a0cf6f77 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc @@ -63,6 +63,8 @@ std::vector *MpduDeaggregation::deaggregateFrame(Packet *aggregatedFra { EV_DEBUG << "Deaggregating A-MPDU " << *aggregatedFrame << " into multiple packets.\n"; std::vector *frames = new std::vector(); + // Extraction alone grants no MAC acceptance, including after delimiter recovery. + // The caller must check delimiter validity, PHY outcomes, member FCS, and MAC conditions. cStringTokenizer tokenizer(aggregatedFrame->getName(), "+"); visitSubframes(aggregatedFrame, B(0), B(aggregatedFrame->getDataLength()), [](const auto&, B, bool) {}, diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h new file mode 100644 index 00000000000..834b94a9fbf --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_IEEE80211AMPDURECEPTIONDECISION_H +#define __INET_IEEE80211AMPDURECEPTIONDECISION_H + +#include "inet/physicallayer/wireless/common/radio/packetlevel/ReceptionDecision.h" +#include "inet/common/packet/chunk/Chunk.h" + +namespace inet { +namespace physicallayer { + +class INET_API Ieee80211AmpduReceptionDecision : public ReceptionDecision +{ + protected: + const bool headerSuccessful; + const bool contentComplete; + const Ptr data; + + public: + Ieee80211AmpduReceptionDecision(const IReception *reception, IRadioSignal::SignalPart part, + bool possible, bool attempted, bool successful, bool headerSuccessful, bool contentComplete, + const Ptr& data) : + ReceptionDecision(reception, part, possible, attempted, successful), + headerSuccessful(headerSuccessful), contentComplete(contentComplete), data(data) {} + bool isHeaderSuccessful() const { return headerSuccessful; } + bool isContentComplete() const { return contentComplete; } + const Ptr& getData() const { return data; } +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 4c9d2ecd6d1..955f5bcacdb 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -10,11 +10,28 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/common/packet/chunk/SequenceChunk.h" namespace inet { namespace physicallayer { +namespace { + +Ptr peekHtHeader(const ITransmission *transmission) +{ + return dynamicPtrCast(Ieee80211Radio::peekIeee80211PhyHeaderAtFront( + transmission->getPacket(), b(-1), Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | + Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED)); +} + +} // namespace + Define_Module(Ieee80211Receiver); Ieee80211Receiver::~Ieee80211Receiver() @@ -27,7 +44,7 @@ void Ieee80211Receiver::initialize(int stage) FlatReceiverBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { const char *opMode = par("opMode"); - setModeSet(*opMode ? Ieee80211ModeSet::getModeSet(opMode) : nullptr); + modeSet = *opMode ? Ieee80211ModeSet::getModeSet(opMode) : nullptr; const char *bandName = par("bandName"); setBand(*bandName != '\0' ? Ieee80211CompliantBands::getBand(bandName) : nullptr); int channelNumber = par("channelNumber"); @@ -62,8 +79,32 @@ bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const IReceptionResult *Ieee80211Receiver::computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const { auto transmission = check_and_cast(reception->getTransmission()); - auto receptionResult = FlatReceiverBase::computeReceptionResult(listening, reception, interference, snir, decisions); + auto header = peekHtHeader(transmission); + bool aggregate = header != nullptr && header->getAggregation(); + bool validHeader = header == nullptr || isIeee80211HtSignalValid(*header, transmission->getMode()); + auto receptionResult = aggregate || !validHeader ? + NarrowbandReceiverBase::computeReceptionResult(listening, reception, interference, snir, decisions) : + FlatReceiverBase::computeReceptionResult(listening, reception, interference, snir, decisions); auto packet = const_cast(receptionResult->getPacket()); + if (aggregate || !validHeader) { + auto indication = packet->addTag(); + if (aggregate) { + auto medium = reception->getReceiverRadio()->getMedium(); + auto decision = check_and_cast(medium->getReceptionDecision( + reception->getReceiverRadio(), listening, transmission, IRadioSignal::SIGNAL_PART_DATA)); + indication->setHeaderSuccessful(decision->isHeaderSuccessful()); + indication->setAggregation(decision->isHeaderSuccessful()); + indication->setContentComplete(decision->isContentComplete()); + if (!decision->isHeaderSuccessful() || !decision->isContentComplete() || decision->getData() == nullptr) + packet->setBitError(true); + } + if (errorModel != nullptr) { + auto rates = packet->addTagIfAbsent(); + rates->setPacketErrorRate(NaN); + rates->setBitErrorRate(NaN); + rates->setSymbolErrorRate(NaN); + } + } packet->addTagIfAbsent()->setMode(transmission->getMode()); packet->addTagIfAbsent()->setChannel(transmission->getChannel()); return receptionResult; @@ -74,6 +115,107 @@ void Ieee80211Receiver::setModeSet(const Ieee80211ModeSet *modeSet) this->modeSet = modeSet; } +Packet *Ieee80211Receiver::computeReceivedPacket(const ISnir *snir, bool successful) const +{ + auto transmission = check_and_cast(snir->getReception()->getTransmission()); + auto header = peekHtHeader(transmission); + if (header != nullptr && header->getAggregation()) { + auto radio = snir->getReception()->getReceiverRadio(); + auto medium = radio->getMedium(); + auto decision = check_and_cast(medium->getReceptionDecision( + radio, medium->getListening(radio, transmission), transmission, IRadioSignal::SIGNAL_PART_DATA)); + auto received = ReceiverBase::computeReceivedPacket(snir, + decision->isHeaderSuccessful() && decision->isContentComplete() && decision->getData() != nullptr); + if (decision->getData() != nullptr) { + b offset = received->getFrontOffset() + header->getChunkLength(); + b end = offset + b(header->getLengthField()); + std::deque> chunks{received->peekAt(b(0), offset), decision->getData()}; + if (end < received->getTotalLength()) + chunks.push_back(received->peekAt(end, received->getTotalLength() - end)); + auto content = makeShared(); + // Automatic insertion can merge byte chunks and erase their distinct error flags. + content->setChunks(chunks); + content->copyTags(*received->peekAll(Chunk::PF_ALLOW_ALL)); + content->markImmutable(); + received->replaceAll(content, Chunk::PF_ALLOW_ALL); + } + return received; + } + if (header != nullptr && !isIeee80211HtSignalValid(*header, transmission->getMode())) + return ReceiverBase::computeReceivedPacket(snir, false); + return FlatReceiverBase::computeReceivedPacket(snir, successful); +} + +bool Ieee80211Receiver::computeIsReceptionSuccessful(const IListening *listening, const IReception *reception, + IRadioSignal::SignalPart part, const IInterference *interference, const ISnir *snir) const +{ + auto transmission = check_and_cast(reception->getTransmission()); + auto header = peekHtHeader(transmission); + if (header != nullptr && header->getAggregation()) + return reception->getReceiverRadio()->getMedium()->getReceptionDecision( + reception->getReceiverRadio(), listening, transmission, part)->isReceptionSuccessful(); + if (header != nullptr && part != IRadioSignal::SIGNAL_PART_PREAMBLE) { + if (!isIeee80211HtSignalValid(*header, transmission->getMode())) + return false; + if ((part == IRadioSignal::SIGNAL_PART_DATA || part == IRadioSignal::SIGNAL_PART_WHOLE) && + header->getChunkLength() + header->getLengthField() > transmission->getPacket()->getDataLength()) + return false; + } + return FlatReceiverBase::computeIsReceptionSuccessful(listening, reception, part, interference, snir); +} + +const IReceptionDecision *Ieee80211Receiver::computeReceptionDecision(const IListening *listening, + const IReception *reception, IRadioSignal::SignalPart part, const IInterference *interference, const ISnir *snir) const +{ + auto transmission = check_and_cast(reception->getTransmission()); + auto packet = transmission->getPacket(); + auto header = peekHtHeader(transmission); + if (header == nullptr || !header->getAggregation()) + return FlatReceiverBase::computeReceptionDecision(listening, reception, part, interference, snir); + auto radio = reception->getReceiverRadio(); + auto medium = radio->getMedium(); + bool possible = computeIsReceptionPossible(listening, reception, part); + bool attempted = possible && computeIsReceptionAttempted(listening, reception, part, interference); + bool successful = attempted; + bool headerSuccessful = false; + bool contentComplete = false; + Ptr data; + auto prerequisite = [&](IRadioSignal::SignalPart previous) { + return medium->getReceptionDecision(radio, listening, transmission, previous)->isReceptionSuccessful(); + }; + if (part == IRadioSignal::SIGNAL_PART_PREAMBLE) + successful = successful && FlatReceiverBase::computeIsReceptionSuccessful(listening, reception, part, interference, snir); + else if (part == IRadioSignal::SIGNAL_PART_HEADER) { + successful = successful && prerequisite(IRadioSignal::SIGNAL_PART_PREAMBLE) && + isIeee80211HtSignalValid(*header, transmission->getMode()) && + FlatReceiverBase::computeIsReceptionSuccessful(listening, reception, part, interference, snir); + headerSuccessful = successful; + } + else if (part == IRadioSignal::SIGNAL_PART_DATA) { + headerSuccessful = prerequisite(IRadioSignal::SIGNAL_PART_HEADER); + contentComplete = headerSuccessful && header->getChunkLength() + header->getLengthField() <= packet->getDataLength(); + successful = successful && contentComplete && SnirReceiverBase::computeIsReceptionSuccessful(listening, reception, part, interference, snir); + if (successful) { + data = packet->peekDataAt(b(header->getChunkLength()), b(header->getLengthField()), Chunk::PF_ALLOW_ALL); + if (errorModel != nullptr) + data = errorModel->computeReceivedData(snir, data); + successful = data->isCorrect(); + } + } + else if (part == IRadioSignal::SIGNAL_PART_WHOLE) { + auto decision = check_and_cast(medium->getReceptionDecision( + radio, listening, transmission, IRadioSignal::SIGNAL_PART_DATA)); + successful = successful && decision->isReceptionSuccessful(); + headerSuccessful = decision->isHeaderSuccessful(); + contentComplete = decision->isContentComplete(); + data = decision->getData(); + } + else + throw cRuntimeError("Invalid A-MPDU signal part"); + return new Ieee80211AmpduReceptionDecision(reception, part, possible, attempted, successful, + headerSuccessful, contentComplete, data); +} + void Ieee80211Receiver::setBand(const IIeee80211Band *band) { if (this->band != band) { diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index 450e8b36c45..7a313a92b77 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -30,6 +30,10 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase virtual bool computeIsReceptionPossible(const IListening *listening, const ITransmission *transmission) const override; virtual bool computeIsReceptionPossible(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part) const override; + virtual Packet *computeReceivedPacket(const ISnir *snir, bool isReceptionSuccessful) const override; + virtual bool computeIsReceptionSuccessful(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part, const IInterference *interference, const ISnir *snir) const override; + virtual const IReceptionDecision *computeReceptionDecision(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part, const IInterference *interference, const ISnir *snir) const override; + virtual const IReceptionResult *computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const override; public: diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg index 5d98b79321d..b31ac89ed62 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg @@ -6,11 +6,20 @@ import inet.common.INETDefs; import inet.common.TagBase; +import inet.common.Units; import inet.physicallayer.wireless.ieee80211.mode.IIeee80211Mode; import inet.physicallayer.wireless.ieee80211.mode.Ieee80211Channel; namespace inet::physicallayer; +// PHY facts only. MAC delimiter and FCS checks still control acceptance. +class Ieee80211AmpduInd extends TagBase +{ + bool headerSuccessful = false; + bool aggregation = false; + bool contentComplete = false; +} + // // This is an abstract base class that should not be directly added as a tag. // diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc index 48783a2badd..d084ce05397 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc @@ -18,17 +18,25 @@ Ieee80211ErrorModelBase::Ieee80211ErrorModelBase() { } +double Ieee80211ErrorModelBase::computeChunkSuccessRate(const ISnir *snir, b bitLength) const +{ + auto transmission = check_and_cast(snir->getReception()->getTransmission()); + return getDataSuccessRate(transmission->getMode(), bitLength.get(), getScalarSnir(snir)); +} + double Ieee80211ErrorModelBase::computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const { Enter_Method("computePacketErrorRate"); + if (part == IRadioSignal::SIGNAL_PART_PREAMBLE) + return 0; auto transmission = check_and_cast(snir->getReception()->getTransmission()); auto mode = transmission->getMode(); auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); auto headerLength = mode->getHeaderMode()->getLength(); auto dataLength = b(mode->getDataMode()->getCompleteLength(B(phyHeader->getLengthField()))); // TODO check header length and data length for OFDM (signal) field - double headerSuccessRate = getHeaderSuccessRate(mode, headerLength.get(), getScalarSnir(snir)); - double dataSuccessRate = getDataSuccessRate(mode, dataLength.get(), getScalarSnir(snir)); + double headerSuccessRate = part == IRadioSignal::SIGNAL_PART_DATA ? 1 : getHeaderSuccessRate(mode, headerLength.get(), getScalarSnir(snir)); + double dataSuccessRate = part == IRadioSignal::SIGNAL_PART_HEADER ? 1 : getDataSuccessRate(mode, dataLength.get(), getScalarSnir(snir)); switch (part) { case IRadioSignal::SIGNAL_PART_WHOLE: return 1.0 - headerSuccessRate * dataSuccessRate; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h index 9b0d634d4c1..418268fb885 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h @@ -26,6 +26,7 @@ class INET_API Ieee80211ErrorModelBase : public ErrorModelBase protected: virtual double getHeaderSuccessRate(const IIeee80211Mode *mode, unsigned int bitLength, double snir) const = 0; virtual double getDataSuccessRate(const IIeee80211Mode *mode, unsigned int bitLength, double snir) const = 0; + virtual double computeChunkSuccessRate(const ISnir *snir, b bitLength) const override; virtual Packet *computeCorruptedPacket(const Packet *packet, double ber) const override; diff --git a/tests/module/Ieee80211AmpduMacBoundary_1.test b/tests/module/Ieee80211AmpduMacBoundary_1.test new file mode 100644 index 00000000000..4028ae722be --- /dev/null +++ b/tests/module/Ieee80211AmpduMacBoundary_1.test @@ -0,0 +1,216 @@ +%description: +The production radio sends aggregate containers to the production MAC and Rx. +Rx rejects successful, partially failed, and entirely failed aggregates before standalone FCS or header checks. +The drop signal retains incorrect chunks and the clear container bit-error flag. +A standalone CTS still sets NAV through the normal acceptance path. +Scalar and dimensional radios cover whole-signal and separate-part reception with DCF and HCF. +Malformed delimiters before typed MPDUs permit bounded recovery without a radio exception. +Typed chunks retain partial errors. Byte input retains errors over its entire DATA region. + +%extraargs: -r 0..15 + +%file: AmpduMacBoundary.cc +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class BoundaryErrorModel : public Ieee80211NistErrorModel +{ + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeChunkSuccessRate(const ISnir *snir, b length) const override { + auto packet = snir->getReception()->getTransmission()->getPacket(); + bool failed = packet->isName("failed") || (packet->isName("partial") && (length == B(318) || length == B(101) - DATAFRAME_HEADER_MINLENGTH)); + return length != B(4) && length != B(3) && failed ? 0 : 1; + } +}; +Define_Module(BoundaryErrorModel); + +class BoundaryDriver : public cSimpleModule, public cListener +{ + int sent = 0; + int dropped = 0; + int navSet = 0; + protected: + void initialize() override { + auto rx = getModuleByPath("^.receiver.wlan[0].mac.rx"); + rx->subscribe(packetDroppedSignal, this); + rx->subscribe(Rx::navChangedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + ASSERT(signal == packetDroppedSignal); + auto packet = check_and_cast(object); + ASSERT(dropped < 4); + ASSERT(packet->isName(dropped == 0 ? "good" : dropped == 1 ? "partial" : dropped == 2 ? "failed" : "malformed")); + ASSERT(!packet->cPacket::hasBitError()); + ASSERT(packet->hasBitError() == (dropped == 1 || dropped == 2)); + auto indication = packet->getTag(); + ASSERT(indication->getAggregation() && indication->getHeaderSuccessful() && indication->getContentComplete()); + bool raw = getModuleByPath("^.sender.radio")->par("sendRawBytes"); + const int offsets[]{4, 108, 216}; + const int lengths[]{100, 101, 102}; + for (int i = 0; !raw && i < 3; i++) { + bool failed = dropped == 2 || (dropped == 1 && i == 1); + auto mpdu = packet->peekDataAt(B(offsets[i]), B(lengths[i]), Chunk::PF_ALLOW_ALL); + ASSERT(mpdu->isCorrect() == !failed); + } + if (dropped == 3) { + ASSERT(packet->peekDataAt(B(0), B(4), Chunk::PF_ALLOW_ALL)->isCorrect()); + ASSERT(packet->peekDataAt(B(0), B(4))->getLength() == 2000); + } + dropped++; + } + void receiveSignal(cComponent *, simsignal_t signal, const SimTime& value, cObject *) override { + ASSERT(signal == Rx::navChangedSignal); + if (value > SIMTIME_ZERO) { + ASSERT(sent == 5 && dropped == 4); + ASSERT(value == SimTime(50, SIMTIME_US)); + navSet++; + } + } + void handleMessage(cMessage *message) override { + delete message; + auto packet = new Packet(sent == 0 ? "good" : sent == 1 ? "partial" : sent == 2 ? "failed" : sent == 3 ? "malformed" : "ordinary"); + if (sent < 4) { + for (int length : {100, 101, 102}) { + auto delimiter = makeShared(); + delimiter->setLength(sent == 3 && length == 100 ? 2000 : length); + packet->insertAtBack(delimiter); + auto member = makeShared(); + member->setSequenceNumber(SequenceNumberCyclic(0)); + packet->insertAtBack(member); + packet->insertAtBack(makeShared(std::vector(length - member->getChunkLength().get(), 0))); + if (length != 102 && length % 4) + packet->insertAtBack(makeShared(B(4 - length % 4))); + } + } + else { + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:99")); + header->setDurationField(SimTime(50, SIMTIME_US)); + packet->insertAtBack(header); + auto bytes = packet->peekAllAsBytes()->getBytes(); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_COMPUTED); + trailer->setFcs(ethernetFcs(bytes.data(), bytes.size())); + packet->insertAtBack(trailer); + } + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const IIeee80211Mode *selected = nullptr; + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20) && !mode->isHtShortGuardInterval()) + selected = mode; + } + ASSERT(selected); + packet->addTag()->setMode(selected); + sent++; + send(packet, "out"); + if (sent < 5) + scheduleAt(simTime() + SimTime(1, SIMTIME_MS), new cMessage("send")); + } + using cListener::finish; + void finish() override { + ASSERT(sent == 5 && dropped == 4 && navSet == 1); + EV << "MAC aggregate boundary checks passed\n"; + } +}; +Define_Module(BoundaryDriver); + +%file: test.ned +import inet.mobility.static.StationaryMobility; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; + +module BoundaryErrorModel extends Ieee80211NistErrorModel +{ + parameters: + @class(::BoundaryErrorModel); +} +simple BoundaryDriver +{ + parameters: + @class(::BoundaryDriver); + gates: + output out; +} +module BoundarySender +{ + parameters: + @networkNode; + gates: + input in; + input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: Ieee80211Radio; + connections allowunconnected: + in --> radio.upperLayerIn; + radioIn --> radio.radioIn; +} +network Test +{ + submodules: + radioMedium: Ieee80211RadioMedium; + driver: BoundaryDriver; + sender: BoundarySender; + receiver: AdhocHost; + connections: + driver.out --> sender.in; +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 7ms +seed-set = 0 +cmdenv-express-mode = false +**.vector-recording = false +*.receiver.numWlanInterfaces = 1 +*.receiver.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.receiver.wlan[0].mac.qosStation = ${qos=false,true} +*.receiver.wlan[0].radio.receiver.errorModel.typename = "BoundaryErrorModel" +*.receiver.wlan[0].radio.separateReceptionParts = ${parts=false,true} +*.sender.radio.separateReceptionParts = ${parts} +*.sender.radio.opMode = "n(mixed-2.4Ghz)" +*.sender.radio.initialRadioMode = "transmitter" +*.sender.radio.antenna.mobilityModule = "^.^.mobility" +*.sender.radio.sendRawBytes = ${raw=false,true} +*.radioMedium.signalAnalogRepresentation = ${analog="scalar","dimensional"} +**.radio.signalAnalogRepresentation = ${analog} +**.radio.receiver.bandwidth = 20MHz +*.radioMedium.backgroundNoise.power = -110dBm +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +MAC aggregate boundary checks passed + +%contains: stdout +Run statistics: total 16, successful 16 diff --git a/tests/module/Ieee80211AmpduPhyReception_1.test b/tests/module/Ieee80211AmpduPhyReception_1.test new file mode 100644 index 00000000000..dd968143999 --- /dev/null +++ b/tests/module/Ieee80211AmpduPhyReception_1.test @@ -0,0 +1,480 @@ +%description: +Real HT20 radios preserve incorrect DATA chunks after partial loss. +Scalar and dimensional controls use whole-signal and separate-part reception with seed 0. +Repeated decision and result queries must reuse probabilities and draws. +Invalid headers and insufficient content must prevent member outcomes through distinct decisions. +Production MAC initialization accepts generic PHY error models without an aggregation role. +Ordinary NIST, Yans, absent-model, and non-HT controls retain their defined behavior and metadata. + +%extraargs: -r 0..123 + +%file: AmpduPhyReception.cc +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/signal/WirelessSignal.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +using namespace inet; +using namespace inet::physicallayer; + +static std::vector chunkOutcomes(const Ptr& data) +{ + Packet packet("outcomes", data); + std::vector outcomes; + b offset(0); + while (auto chunk = packet.peekAt(offset, b(-1), Chunk::PF_ALLOW_ALL | Chunk::PF_ALLOW_NULLPTR)) { + outcomes.push_back(chunk->isCorrect()); + offset += chunk->getChunkLength(); + } + return outcomes; +} + +class AmpduDeterministicModel : public Ieee80211NistErrorModel +{ + public: + mutable int chunkCalls = 0; + mutable int headerCalls = 0; + mutable int preambleCalls = 0; + mutable int wholeCalls = 0; + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart part) const override { + int scenario = par("scenario"); + if (part == IRadioSignal::SIGNAL_PART_PREAMBLE) { preambleCalls++; return scenario == 11 ? 1 : 0; } + if (part == IRadioSignal::SIGNAL_PART_HEADER) { headerCalls++; return scenario == 10 ? 1 : 0; } + wholeCalls++; + return 0; + } + double computeChunkSuccessRate(const ISnir *, b length) const override { + int index = chunkCalls++; + int scenario = par("scenario"); + if (scenario == 16) return 0.5; + if ((scenario == 4 && length == B(100)) || (scenario == 5 && length == B(101)) || + (scenario == 6 && length == B(102)) || (scenario == 7 && index == 0) || + (scenario == 8 && index == 2) || (scenario == 9 && length > B(4))) + return 0; + return 1; + } +}; +Define_Module(AmpduDeterministicModel); + +class AmpduLimitedModel : public Ieee80211NistErrorModel +{ + public: + mutable int probabilityCalls = 0; + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override { + probabilityCalls++; + return Ieee80211NistErrorModel::computePacketErrorRate(snir, part); + } + double computeChunkSuccessRate(const ISnir *snir, b length) const override { + probabilityCalls++; + return Ieee80211NistErrorModel::computeChunkSuccessRate(snir, length); + } +}; +Define_Module(AmpduLimitedModel); + +class GenericBerModel : public ErrorModelBase +{ + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeBitErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeSymbolErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } +}; +Define_Module(GenericBerModel); + +class AmpduProbeRadio : public Ieee80211Radio +{ + protected: + void encapsulate(Packet *packet) const override { + Ieee80211Radio::encapsulate(packet); + int scenario = par("scenario"); + if (scenario == 12 || scenario == 13 || scenario == 14 || scenario == 17 || scenario == 18 || scenario == 19) { + auto header = packet->removeAtFront(); + if (scenario == 12 || scenario == 17) + header->setCrc(header->getCrc() ^ 1); + else if (scenario == 13 || scenario == 18) { + header->setFecCoding(true); + header->setCrc(computeIeee80211HtSignalCrc(*header)); + } + else { + header->setLengthField(B(600)); + header->setCrc(computeIeee80211HtSignalCrc(*header)); + } + packet->insertAtFront(header); + } + } + void continueReception(cMessage *timer) override { + Ieee80211Radio::continueReception(timer); + auto model = dynamic_cast(getSubmodule("receiver")->getSubmodule("errorModel")); + if (model != nullptr && timer->getKind() == IRadioSignal::SIGNAL_PART_HEADER) { + ASSERT(model->headerCalls == 0); + ASSERT(model->chunkCalls == 0); + } + } + void endReception(cMessage *timer) override { + auto signal = check_and_cast(timer->getControlInfo()); + if (getParentModule()->getIndex() == 1 && par("scenario").intValue() >= 3 && + par("scenario").intValue() != 21 && par("scenario").intValue() != 10 && par("scenario").intValue() != 11 && + par("scenario").intValue() != 30 && + !(par("separateReceptionParts").boolValue() && + (par("scenario").intValue() == 12 || par("scenario").intValue() == 13 || par("scenario").intValue() == 17 || par("scenario").intValue() == 18))) { + auto transmission = signal->getTransmission(); + auto listening = signal->getListening(); + auto whole = medium->getReceptionDecision(this, listening, transmission, IRadioSignal::SIGNAL_PART_WHOLE); + auto model = dynamic_cast(getSubmodule("receiver")->getSubmodule("errorModel")); + int calls = model ? model->chunkCalls : 0; + auto draws = getSubmodule("receiver")->getRNG(0)->getNumbersDrawn(); + auto result = medium->getReceptionResult(this, listening, transmission); + for (int repeat = 0; repeat < 3; repeat++) { + for (auto part : {IRadioSignal::SIGNAL_PART_PREAMBLE, IRadioSignal::SIGNAL_PART_HEADER, + IRadioSignal::SIGNAL_PART_DATA, IRadioSignal::SIGNAL_PART_WHOLE}) { + auto decision = medium->getReceptionDecision(this, listening, transmission, part); + ASSERT(medium->isReceptionSuccessful(this, transmission, part) == decision->isReceptionSuccessful()); + } + ASSERT(medium->getReceptionDecision(this, listening, transmission, IRadioSignal::SIGNAL_PART_WHOLE) == whole); + ASSERT(medium->getReceptionResult(this, listening, transmission) == result); + } + ASSERT(draws == getSubmodule("receiver")->getRNG(0)->getNumbersDrawn()); + ASSERT(model == nullptr || calls == model->chunkCalls); + } + Ieee80211Radio::endReception(timer); + } +}; +Define_Module(AmpduProbeRadio); + +class AmpduPhyDriver : public cSimpleModule, public cListener +{ + protected: + int received = 0; + simtime_t duration; + Ptr retained; + std::vector retainedOutcomes; + int scenario() const { return par("scenario"); } + bool ordinary() const { return scenario() < 3 || scenario() == 21 || scenario() == 29; } + void checkCapabilities() { + auto type = cModuleType::get("inet.node.inet.WirelessHost"); + auto host = type->create("probe", getParentModule()); + bool failed = false; + try { + host->finalizeParameters(); + host->buildInside(); + host->scheduleStart(simTime()); + host->callInitialize(); + } + catch (const cRuntimeError& error) { + failed = true; + std::string message(error.what()); + EV << "Capability initialization error: " << message << "\n"; + if (scenario() == 28) + ASSERT(message.find("Value \"n(mixed-2.4Ghz)\" is not allowed") != std::string::npos); + else + throw; + auto mib = check_and_cast(host->getSubmodule("wlan", 0)->getSubmodule("mib")); + ASSERT(!mib->isHtOperationSupported()); + } + ASSERT(failed == (scenario() == 28)); + if (!failed) { + auto radio = check_and_cast(host->getSubmodule("wlan", 0)->getSubmodule("radio")); + auto model = dynamic_cast(radio->getSubmodule("receiver")->getSubmodule("errorModel")); + if (model) ASSERT(model->probabilityCalls == 0); + if (scenario() == 26) { + auto receiver = check_and_cast(radio->getSubmodule("receiver")); + receiver->setModeSet(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); + } + } + host->deleteModule(); + if (scenario() == 28) { + auto model = cModuleType::get("inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211BerTableErrorModel")->create("berProbe", getParentModule()); + model->par("opMode").setStringValue("n"); + model->par("berTableFile").setStringValue("berTable.dat"); + bool refused = false; + try { + model->finalizeParameters(); + model->buildInside(); + model->callInitialize(); + } + catch (const cRuntimeError& error) { refused = std::string(error.what()).find("Unknown opMode") != std::string::npos; } + ASSERT(refused); + model->deleteModule(); + } + EV << "PHY capability checks passed\n"; + } + void initialize() override { + getModuleByPath("^.node[0].radio")->subscribe(transmissionStartedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); + } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *) override { + auto transmission = check_and_cast(object); + if (scenario() != 29) + ASSERT(transmission->getPacket()->peekAtFront()->getChunkLength() == b(48)); + duration = transmission->getEndTime() - transmission->getStartTime(); + if (scenario() < 3 || scenario() == 21) + ASSERT(duration == SimTime(170, SIMTIME_US)); + } + void handleMessage(cMessage *message) override { + if (message->isSelfMessage()) { + delete message; + if (scenario() >= 22 && scenario() <= 28) { + checkCapabilities(); + endSimulation(); + return; + } + auto packet = new Packet("input"); + if (ordinary()) + packet->insertAtBack(makeShared(B(100))); + else { + for (int length : {100, 0, 101, 102}) { + auto delimiter = makeShared(); + delimiter->setLength(length); + packet->insertAtBack(delimiter); + if (length) + packet->insertAtBack(makeShared(std::vector(length, 0))); + if (length == 101) + packet->insertAtBack(makeShared(B(3))); + } + } + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + if (scenario() == 29) + packet->addTag()->setMode(Ieee80211ModeSet::getModeSet("g(mixed)")->getMode(Mbps(6))); + for (int i = 0; scenario() != 29 && i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20) && mode->isHtShortGuardInterval() == (scenario() == 15)) { + packet->addTag()->setMode(mode); + break; + } + } + send(packet, "out", 0); + } + else { + auto packet = check_and_cast(message); + bool refused = scenario() == 10 || scenario() == 11 || scenario() == 12 || scenario() == 13 || + scenario() == 14 || scenario() == 17 || scenario() == 18 || scenario() == 19 || scenario() == 30; + ASSERT(packet->cPacket::hasBitError() == refused); + if (ordinary()) + ASSERT(packet->getDataLength() == B(100)); + else { + auto indication = packet->getTag(); + ASSERT(packet->getDataLength() == B(322)); + retained = makeShared(*indication); + retainedOutcomes = chunkOutcomes(packet->peekData(Chunk::PF_ALLOW_ALL)); + bool badHeader = refused && scenario() != 14 && scenario() != 19; + ASSERT(indication->getHeaderSuccessful() == !badHeader); + ASSERT(indication->getAggregation() == !badHeader); + ASSERT(indication->getContentComplete() == !refused); + ASSERT(refused || retainedOutcomes.size() == 8); + if (!refused) { + int failed = 0; + for (bool successful : retainedOutcomes) + failed += !successful; + if (scenario() >= 4 && scenario() <= 8) ASSERT(failed == 1); + else if (scenario() == 9) ASSERT(failed == 3); + else if (scenario() != 16) ASSERT(failed == 0); + } + } + ASSERT(packet->getTag()->getMode()->getHtMcsIndex() == (scenario() == 29 ? -1 : 0)); + ASSERT(packet->getTag()->getChannel() != nullptr); + ASSERT(packet->getTag()->getPower() > W(0)); + ASSERT(packet->getTag()->getMinimumSnir() > 0); + ASSERT(packet->getTag()->getStartTime() >= SIMTIME_ZERO); + ASSERT(packet->getTag()->getEndTime() - packet->getTag()->getStartTime() == duration); + auto error = packet->findTag(); + if (scenario() >= 3 && scenario() <= 16) { + ASSERT(error != nullptr); + ASSERT(std::isnan(error->getPacketErrorRate())); + ASSERT(std::isnan(error->getBitErrorRate())); + ASSERT(std::isnan(error->getSymbolErrorRate())); + } + if ((scenario() >= 17 && scenario() <= 20) || scenario() == 30) + ASSERT(error == nullptr); + if (error) + EV << "ordinary PER=" << error->getPacketErrorRate() << " BER=" << error->getBitErrorRate() << " SER=" << error->getSymbolErrorRate() << "\n"; + received++; + delete packet; + } + } + using cListener::finish; + void finish() override { + if (scenario() >= 22 && scenario() <= 28) { + EV << "PHY reception checks passed\n"; + return; + } + bool headerFailure = scenario() == 10 || scenario() == 11 || scenario() == 12 || scenario() == 13 || scenario() == 17 || scenario() == 18 || scenario() == 30; + bool separate = getModuleByPath("^.node[1].radio")->par("separateReceptionParts"); + ASSERT(received == (headerFailure && separate ? 0 : 1)); + auto rx = getModuleByPath("^.node[1].radio.receiver"); + if (scenario() == 30) ASSERT(rx->getRNG(0)->getNumbersDrawn() == 0); + auto model = dynamic_cast(rx->getSubmodule("errorModel")); + if (model) { + ASSERT(model->wholeCalls == 0); + ASSERT(model->chunkCalls == (scenario() >= 10 && scenario() <= 14 ? 0 : 8)); + if (scenario() == 11) ASSERT(model->headerCalls == 0); + ASSERT(rx->getRNG(0)->getNumbersDrawn() == (scenario() == 16 ? 8 : 0)); + } + auto medium = check_and_cast(getModuleByPath("^.radioMedium")); + ASSERT(medium->getCommunicationCache()->getNumTransmissions() == 0); + if (retained && !headerFailure && scenario() != 14 && scenario() != 19) + ASSERT(retainedOutcomes.size() == 8); + auto radio = check_and_cast(getModuleByPath("^.node[1].radio")); + EV << "ordinary next RNG=" << radio->getSubmodule("receiver")->getRNG(0)->intRand() << "\n"; + EV << "PHY reception checks passed\n"; + } +}; +Define_Module(AmpduPhyDriver); + +%file: test.ned +import inet.mobility.static.StationaryMobility; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.common.medium.RadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.physicallayer.wireless.common.base.packetlevel.ErrorModelBase; + +module AmpduLimitedModel extends Ieee80211NistErrorModel +{ + parameters: + bool longOnly; + @class(::AmpduLimitedModel); +} +module GenericBerModel extends ErrorModelBase +{ + parameters: + @class(::GenericBerModel); +} + +module AmpduDeterministicModel extends Ieee80211NistErrorModel +{ + parameters: + int scenario; + @class(::AmpduDeterministicModel); +} +module AmpduProbeRadio extends Ieee80211Radio +{ + parameters: + int scenario; + receiver.errorModel.typename = default(scenario == 29 ? "GenericBerModel" : (scenario == 0 ? "Ieee80211NistErrorModel" : (scenario == 1 || scenario == 21 ? "Ieee80211YansErrorModel" : (scenario == 2 || scenario >= 17 ? "" : "AmpduDeterministicModel")))); + @class(::AmpduProbeRadio); +} +module AmpduCapabilityRadio extends Ieee80211Radio +{ + parameters: + int scenario; + signalAnalogRepresentation = default(scenario == 25 ? "unitDisk" : "scalar"); + radioMediumModule = default(scenario == 25 ? "unitDiskMedium" : "radioMedium"); + receiver.errorModel.typename = default(scenario == 22 || scenario == 26 ? "GenericBerModel" : (scenario == 23 || scenario == 24 ? "AmpduLimitedModel" : (scenario == 27 || scenario == 28 ? "Ieee80211BerTableErrorModel" : ""))); +} + +simple AmpduPhyDriver +{ + parameters: + @class(::AmpduPhyDriver); + int scenario; + gates: + input in[2]; + output out[2]; +} +module AmpduPhyNode +{ + parameters: + @networkNode; + gates: + input in; + output out; + input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: AmpduProbeRadio; + connections: + in --> radio.upperLayerIn; + radio.upperLayerOut --> out; + radioIn --> radio.radioIn; +} +network Test +{ + submodules: + radioMedium: Ieee80211RadioMedium; + unitDiskMedium: RadioMedium { + signalAnalogRepresentation = "unitDisk"; + } + driver: AmpduPhyDriver; + node[2]: AmpduPhyNode; + connections: + for i=0..1 { + driver.out[i] --> node[i].in; + node[i].out --> driver.in[i]; + } +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = false +**.scalar-recording = true +**.vector-recording = false +*.node[*].radio.opMode = ${scenario} == 29 ? "g(mixed)" : "n(mixed-2.4Ghz)" +*.node[*].radio.initialRadioMode = "transceiver" +*.driver.scenario = ${scenario=0..30} +*.node[*].radio.scenario = ${scenario} +*.node[*].radio.receiver.errorModel.scenario = ${scenario} +*.node[*].radio.separateReceptionParts = ${parts=false,true} +*.node[*].radio.signalAnalogRepresentation = ${analog="scalar","dimensional"} +*.radioMedium.signalAnalogRepresentation = ${analog} +*.node[*].radio.sendRawBytes = ${scenario} == 21 +*.node[*].radio.receiver.bandwidth = 20MHz +*.node[*].radio.receiver.snirThreshold = ${scenario} == 30 ? 100dB : 4dB +*.node[*].radio.antenna.mobilityModule = "^.^.mobility" +*.node[*].mobility.initFromDisplayString = false +*.node[*].mobility.initialX = 10m + parentIndex() * 1m +*.node[*].mobility.initialY = 10m +*.node[*].mobility.initialZ = 0m +*.node[*].mobility.constraintAreaMinX = 0m +*.node[*].mobility.constraintAreaMinY = 0m +*.node[*].mobility.constraintAreaMinZ = 0m +*.node[*].mobility.constraintAreaMaxX = 100m +*.node[*].mobility.constraintAreaMaxY = 100m +*.node[*].mobility.constraintAreaMaxZ = 0m +*.radioMedium.backgroundNoise.power = -110dBm +*.probe.wlan[0].opMode = ${scenario} == 26 || ${scenario} == 27 ? "g(mixed)" : "n(mixed-2.4Ghz)" +*.probe.wlan[0].radio.typename = "AmpduCapabilityRadio" +*.probe.wlan[0].radio.scenario = ${scenario} +*.probe.wlan[0].radio.receiver.bandwidth = 20MHz +*.probe.wlan[0].radio.transmitter.analogModel.communicationRange = 100m +*.probe.wlan[0].radio.receiver.errorModel.longOnly = ${scenario} == 23 +*.probe.wlan[0].radio.receiver.errorModel.berTableFile = "berTable.dat" +*.probe.wlan[0].mac.qosStation = true +*.probe.wlan[0].mgmt.typename = "Ieee80211MgmtAdhoc" +*.probe.mobility.initFromDisplayString = false +*.probe.mobility.initialX = 20m +*.probe.mobility.initialY = 10m +*.probe.mobility.initialZ = 0m +*.probe.mobility.constraintAreaMinX = 0m +*.probe.mobility.constraintAreaMinY = 0m +*.probe.mobility.constraintAreaMinZ = 0m +*.probe.mobility.constraintAreaMaxX = 100m +*.probe.mobility.constraintAreaMaxY = 100m +*.probe.mobility.constraintAreaMaxZ = 0m + +%file: berTable.dat +$self add Mode6Mb 128 0 0 +$self add Mode9Mb 128 0 0 +$self add Mode12Mb 128 0 0 +$self add Mode18Mb 128 0 0 +$self add Mode24Mb 128 0 0 +$self add Mode36Mb 128 0 0 +$self add Mode48Mb 128 0 0 +$self add Mode54Mb 128 0 0 + +%contains: stdout +PHY reception checks passed diff --git a/tests/protocol/wifi/ampdu-partial-reception-comparison.md b/tests/protocol/wifi/ampdu-partial-reception-comparison.md new file mode 100644 index 00000000000..49318ce70e8 --- /dev/null +++ b/tests/protocol/wifi/ampdu-partial-reception-comparison.md @@ -0,0 +1,48 @@ +# Partial HT reception: parent and commit comparison + +> **Kind:** report · **Status:** snapshot 2026-10-29 · **Seal:** none · **Owns:** — · **Stands on:** [Test guide](ampdu-partial-reception.md) + +This comparison checks chunk error flags at the production Rx drop. The same [fixture](common/WifiHtAmpduPartialReception.test) must fail on the parent and pass after chunk error flags reach Rx. The [guide](ampdu-partial-reception.md) explains the production path and its current MAC rejection boundary. + +## Inputs and expected difference + +| Input | Revision | +| --- | --- | +| Parent source | `bad67fef81824773e9eca138b755a01e63d7442c` | +| Behavior source | `bda41c85ad64ad35064d4771917f1f641ba5117a` | +| Behavior commit subject | `ieee80211: add+change: retain partial HT A-MPDU PHY outcomes` | + +Each aggregate contains MPDUs of 100, 101, and 102 octets. Four loss patterns fail one MPDU position or all four-octet delimiters. The parent samples the aggregate as one packet and supplies no copied range indication. After the correction, Rx must receive intact bytes and six exact outcomes. Rx must reject the container before standalone frame checks. A subsequent valid CTS must set NAV for 50 microseconds. + +The fixture reads the new indication through descriptors. It therefore compiles on the parent without that class. The assertions require the indication and every field. Both revisions receive identical fixture bytes. Their fixture SHA-256 is `270f1f4be6907fed36c995719d5b3248b21cc0f6202d4b88d18c71ff635b011c`. + +The four loss patterns combine with both GIs, both byte representations, and both reception-part modes. These combinations produce 32 runs with seed zero. Each probability equals zero or one, so this claim requires no seed campaign. These source revisions precede the fixture amendments. The amendments must preserve each production tree and the shared test library. + +## Observed results + +Both debug builds and both fixture compilations pass. The parent reaches the production Rx drop but supplies no copied indication. The observer fails its `indication != nullptr` assertion. After the correction, all 32 cases retain the required outcomes and pass the subsequent CTS control. The missing indication therefore causes a runtime regression failure without a dependency on its new C++ class. + +| Revision | Build exit | Runner exit | Observed simulation runs | +| --- | ---: | ---: | --- | +| Parent | 0 | 1 | One runtime error. The 31 later runs are skipped. | +| Behavior commit | 0 | 0 | All 32 cases pass. No case is skipped. | + +Cmdenv stops after the first error. The executed parent case selects first-MPDU loss, long GI, typed bytes, and whole-signal reception. This comparison supplies no executed parent result for the other 31 combinations. The corrected commit executes every combination. + +The execution below predates the chunk corruption change. It supplies historical evidence only. Fresh parent comparisons for the current fixture remain unverified. + +## Commands and evidence + +The comparison directory is `/tmp/ampdu-protocol-parent-20261029/checkout`. Each execution installs the same fixture at its original path after source checkout. A fresh debug build precedes each filtered invocation. The temporary descriptor preserves the checked-in protocol settings and points to this directory. It omits an unsupported coverage-report setting. + +```sh +record=/tmp/ampdu-protocol-parent-20261029 +export MAKEFLAGS='--jobserver-style=pipe LN=ln\ -sf' +make MODE=debug -j8 +opp_run_protocol_tests --load "$record/inet-compatible.opp" -p inet -m debug --no-build --no-concurrent \ + -w '^tests/protocol/wifi$' -f 'WifiHtAmpduPartialReception\.test$' +``` + +The `partial-parent/` and `partial-current/` artifact directories contain build logs, runner logs, native results, compressed simulation output, and input hashes. Each `evidence.json` records the exact revision, production tree, command, and exit status. The fixture expects PASS on both revisions. Its parent failure must remain visible without an expected-failure declaration. + +This comparison proves copied PHY facts and current MAC rejection. It does not prove member delivery, implicit Block Ack, or correlated FEC behavior. The guide supplies the sequence diagram; this record adds no separate interaction. diff --git a/tests/protocol/wifi/ampdu-partial-reception.md b/tests/protocol/wifi/ampdu-partial-reception.md new file mode 100644 index 00000000000..ec040a6e584 --- /dev/null +++ b/tests/protocol/wifi/ampdu-partial-reception.md @@ -0,0 +1,60 @@ +# PHY P2: Preserve partial outcomes at the MAC boundary + +This document belongs to PHY P2, **ieee80211: add+change: retain partial HT A-MPDU PHY outcomes**. Its protocol test is [WifiHtAmpduPartialReception.test](common/WifiHtAmpduPartialReception.test). The [PHY plan](../../../plan/pending/ieee80211-ampdu-phy-reception.md) defines the receiver contract and later MAC work. + +Before this commit, the receiver samples the aggregate as one packet. It cannot retain a failed member beside successful members. Rx can also inspect an aggregate container as a standalone MAC frame. After this commit, cached PHY decisions retain chunks with error flags. The result packet preserves those flags. Rx rejects the container before standalone FCS or MAC header checks. + +## Requirement and model boundary + +IEEE Std 802.11-2024 Clause 9.7.1 and Table 9-659 define A-MPDU subframes and delimiter fields. Their canonical nodes are `ieee80211-2024:clause:9.7.1` and `ieee80211-2024:table:9-659`. Physical PDF pages 1840–1842 cover the relevant layout. Figure 19-6 defines the HT-SIG Aggregation field on physical PDF pages 3432–3434. + +These requirements define the represented aggregate. Independent chunk error probabilities are an explicit INET model abstraction. The standard does not require the fixture's chosen synthetic loss pattern. That pattern makes the chunk error contract deterministic. It supplies no error-accuracy or goodput claim. + +## Setup, stimulus, and assertions + +The fixture uses a production scalar transmitter, medium, receiver, MAC, and Rx. The recipient uses HCF in HT mixed mode at 20 MHz. A test error model inherits NIST support but fixes common-field success. Its chunk probability function returns failure for the configured bit length. Four configurations select lengths of 100, 101, 102, and 4 octets. Typed input retains separate chunk errors at those positions. Raw byte input has one DATA outcome and fails throughout. + +At 1 ms, the driver submits members of 100, 101, and 102 octets. Three alignment octets follow the 101-octet member. The complete PSDU contains 318 octets. The driver selects HT MCS 0 and the run's GI before production radio encapsulation. + +At the production Rx drop signal, the observer checks the intact PSDU length and clear container bit-error flag. The indication must report a successful header and complete content. Chunk flags supply the error outcomes. Their offsets are 0, 4, 104, 108, 212, and 216 octets. For typed input, each selected chunk must be incorrect exactly when its length equals the configured failed length. Raw input marks the entire DATA view incorrect. The test uses a distinct padding chunk to preserve the typed boundaries. + +The observer reads the indication through OMNeT++ descriptors, which expose generated fields by name. The fixture therefore uses the generic corruption API from the preparation commit. It compiles on the receiver commit's parent without the new indication class. A missing indication fails at runtime. The observer reads each field without a change to protocol state. No conditional check accepts missing data. + +Rx must reject that container once through its normal drop path. At 2 ms, the driver submits an independent CTS with computed FCS and a 50-microsecond Duration field. The observer requires one 50-microsecond NAV update after the aggregate drop. This control proves that the ordinary acceptance path remains usable after the rejection. + +The protocol tester requires `testHtAmpduPartial` followed by `testHtAmpduControl`. These events occur only after their assertions pass. A missing aggregate, missing drop, wrong outcome, or missing control result cannot pass through an empty observation. + +The four loss patterns combine with both GIs, typed and raw-byte transmission, and whole-signal and separate-part reception. These combinations produce 32 runs. Every run uses seed zero and ends at 4 ms. The deterministic probabilities require no extra seeds. The existing module fixture separately retains dimensional-radio, DCF, malformed-input, and cached-query controls. + +```mermaid +sequenceDiagram + participant Driver + participant Tx as Production transmitter + participant Receiver as Production receiver + participant Rx + participant Tester as Protocol tester + Driver->>Tx: Three-member HT aggregate + Tx->>Receiver: One PPDU through the medium + Receiver->>Receiver: Retain every delimiter and MPDU outcome + Receiver->>Rx: Intact container and copied ranges + Rx->>Tester: Drop with verified partial outcomes + Driver->>Tx: Independent CTS with computed FCS + Tx->>Receiver: Ordinary PPDU + Receiver->>Rx: Decoded CTS + Rx->>Tester: Verified NAV update +``` + +The parent lacks the copied range indication and the aggregate-safe Rx boundary. Its whole-packet result cannot satisfy these assertions. This commit must retain the six outcomes and complete the later CTS control for every loss pattern. No expected-failure marker suppresses either requirement. + +## Verification and limits + +Use the current debug library before the selected protocol run: + +```sh +make MODE=debug -j8 +opp_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiHtAmpduPartialReception\.test$' --no-concurrent +``` + +The installed runner can require the temporary compatible descriptor described in the [execution record](../../../plan/pending/ieee80211-ampdu-phy-execution.md). The expected result is 32 successful runs and `PROTOCOLTEST wifi_ht_ampdu_partial_reception: PASS`. The [comparison record](ampdu-partial-reception-comparison.md) reports the parent and commit executions with identical fixture bytes. + +This fixture verifies PHY outcomes at the current MAC boundary. It does not verify member acceptance, upper delivery, implicit Block Ack, or correlated FEC errors. The diagram follows the actual send, receive, drop, and NAV sequence. Diagram rendering remains unverified when no Mermaid renderer is available. diff --git a/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test b/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test new file mode 100644 index 00000000000..fe32973a0b6 --- /dev/null +++ b/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test @@ -0,0 +1,223 @@ +%description: +Check copied partial HT A-MPDU outcomes through physical reception and production Rx rejection. +Typed input retains partial chunk errors. Byte input retains one incorrect DATA region. The aggregate bytes remain available at the drop. +A later valid CTS must set NAV. The protocol tester requires both observations in order. +Four loss patterns, both byte representations, both GIs, and both reception-part modes form 32 runs. + +%extraargs: -r 0..31 + +%file: HtAmpduPartialReception.cc +#include "ProtocolTest.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; +using namespace inet::protocoltest; + +class HtAmpduPartialErrorModel : public Ieee80211NistErrorModel +{ + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeChunkSuccessRate(const ISnir *, b length) const override { + return length == B(318) || length == B(par("failedLength").intValue()) ? 0 : 1; + } +}; +Define_Module(HtAmpduPartialErrorModel); + +class HtAmpduPartialDriver : public cSimpleModule, public cListener +{ + int sent = 0; + bool dropped = false; + bool nav = false; + protected: + void initialize() override { + auto rx = getModuleByPath("^.receiver.wlan[0].mac.rx"); + rx->subscribe(packetDroppedSignal, this); + rx->subscribe(Rx::navChangedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + ASSERT(signal == packetDroppedSignal && !dropped); + auto packet = check_and_cast(object); + ASSERT(packet->isName("partial") && !packet->cPacket::hasBitError() && packet->hasBitError()); + ASSERT(packet->getDataLength() == B(318)); + // Read the generated indication without a compile-time dependency on its new class. + cObject *indication = nullptr; + for (int i = 0; i < packet->getNumTags(); i++) { + auto tag = packet->getTag(i); + if (strcmp(tag->getClassName(), "inet::physicallayer::Ieee80211AmpduInd") == 0) + indication = const_cast(tag.get()); + } + ASSERT(indication != nullptr); + auto descriptor = cClassDescriptor::getDescriptorFor(indication); + auto field = [](cClassDescriptor *type, any_ptr objectPointer, const char *name) { + int index = type->findField(name); + ASSERT(index >= 0); + return type->getFieldValue(objectPointer, index, 0); + }; + auto objectPointer = toAnyPtr(indication); + ASSERT(field(descriptor, objectPointer, "aggregation").boolValue()); + ASSERT(field(descriptor, objectPointer, "headerSuccessful").boolValue()); + ASSERT(field(descriptor, objectPointer, "contentComplete").boolValue()); + const int offsets[]{0, 4, 104, 108, 212, 216}; + const int lengths[]{4, 100, 4, 101, 4, 102}; + bool raw = getModuleByPath("^.sender.radio")->par("sendRawBytes"); + ASSERT(packet->peekData(Chunk::PF_ALLOW_ALL)->isIncorrect()); + for (size_t i = 0; !raw && i < 6; i++) { + auto chunk = packet->peekDataAt(B(offsets[i]), B(lengths[i]), Chunk::PF_ALLOW_ALL); + ASSERT(chunk->isCorrect() == (lengths[i] != par("failedLength").intValue())); + } + dropped = true; + emit(registerSignal("testHtAmpduPartial"), 1L); + } + void receiveSignal(cComponent *, simsignal_t signal, const SimTime& value, cObject *) override { + Enter_Method_Silent(); + ASSERT(signal == Rx::navChangedSignal); + if (value > SIMTIME_ZERO) { + ASSERT(sent == 2 && dropped && !nav && value == SimTime(50, SIMTIME_US)); + nav = true; + emit(registerSignal("testHtAmpduControl"), 1L); + } + } + void handleMessage(cMessage *message) override { + delete message; + auto packet = new Packet(sent == 0 ? "partial" : "control"); + if (sent == 0) { + for (int length : {100, 101, 102}) { + auto delimiter = makeShared(); + delimiter->setLength(length); + packet->insertAtBack(delimiter); + packet->insertAtBack(makeShared(std::vector(length, 0))); + if (length == 101) + packet->insertAtBack(makeShared(B(3))); + } + } + else { + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:99")); + header->setDurationField(SimTime(50, SIMTIME_US)); + packet->insertAtBack(header); + auto bytes = packet->peekAllAsBytes()->getBytes(); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_COMPUTED); + trailer->setFcs(ethernetFcs(bytes.data(), bytes.size())); + packet->insertAtBack(trailer); + } + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const IIeee80211Mode *selected = nullptr; + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20) && + mode->isHtShortGuardInterval() == par("shortGi").boolValue()) + selected = mode; + } + ASSERT(selected); + packet->addTag()->setMode(selected); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + sent++; + send(packet, "out"); + if (sent == 1) + scheduleAt(simTime() + SimTime(1, SIMTIME_MS), new cMessage("send")); + } + using cListener::finish; + void finish() override { ASSERT(sent == 2 && dropped && nav); } +}; +Define_Module(HtAmpduPartialDriver); + +Define_ProtocolTest(wifi_ht_ampdu_partial_reception) { + return ProtocolTest("wifi_ht_ampdu_partial_reception") + .once(on("driver").signal("testHtAmpduPartial").assertValue(1).within(0.01)) + .once(on("driver").signal("testHtAmpduControl").assertValue(1).within(0.01)); +} + +%file: test.ned +import inet.protocoltest.ProtocolTester; +import inet.mobility.static.StationaryMobility; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module HtAmpduPartialErrorModel extends Ieee80211NistErrorModel { + parameters: + @class(::HtAmpduPartialErrorModel); + int failedLength; +} +simple HtAmpduPartialDriver { + parameters: + @class(::HtAmpduPartialDriver); + bool shortGi; + int failedLength; + @signal[testHtAmpduPartial](type=long); + @signal[testHtAmpduControl](type=long); + gates: output out; +} +module HtAmpduPartialSender { + parameters: @networkNode; + gates: input in; input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + connections allowunconnected: + in --> radio.upperLayerIn; + radioIn --> radio.radioIn; +} +network Test { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + tester: ProtocolTester; + driver: HtAmpduPartialDriver; + sender: HtAmpduPartialSender; + receiver: AdhocHost; + connections: driver.out --> sender.in; +} + +%inifile: omnetpp.ini +[General] +network = Test +sim-time-limit = 4ms +seed-set = 0 +*.tester.testName = "wifi_ht_ampdu_partial_reception" +*.tester.stateSignals = "testHtAmpduPartial testHtAmpduControl" +*.driver.shortGi = ${shortGi=false,true} +*.driver.failedLength = ${failedLength=100,101,102,4} +*.receiver.numWlanInterfaces = 1 +*.receiver.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.receiver.wlan[0].mac.qosStation = true +*.receiver.wlan[0].radio.receiver.errorModel.typename = "HtAmpduPartialErrorModel" +*.receiver.wlan[0].radio.receiver.errorModel.failedLength = ${failedLength} +*.receiver.wlan[0].radio.separateReceptionParts = ${parts=false,true} +*.sender.radio.separateReceptionParts = ${parts} +*.sender.radio.opMode = "n(mixed-2.4Ghz)" +*.sender.radio.initialRadioMode = "transmitter" +*.sender.radio.antenna.mobilityModule = "^.^.mobility" +*.sender.radio.sendRawBytes = ${raw=false,true} +*.radioMedium.backgroundNoise.power = -110dBm +**.radio.receiver.bandwidth = 20MHz +**.vector-recording = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +PROTOCOLTEST wifi_ht_ampdu_partial_reception: PASS +%contains: stdout +Run statistics: total 32, successful 32 +%not-contains: stdout +PROTOCOLTEST wifi_ht_ampdu_partial_reception: FAIL From 4eeab252e8e95fbb2d4306677d7ed01deae07640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:54:50 +0200 Subject: [PATCH 07/24] tests: add: verify seeded NIST and Yans chunk errors Before this commit, deterministic fixtures fix selected chunk failures but do not compare production model probabilities and random draws. A receiver could repeat DATA samples without those fixtures detecting every difference. After this commit, the receiver fixture compares NIST and Yans chunk probabilities with their existing DATA calculations. Fixed seeds verify the sampled chunk flags, retained packet content, cache reuse, and the next random value. Scalar and dimensional radios cover both guard intervals and both reception-part modes. Each existing chunk receives one outcome, including padding. For an intermediate probability, the reference generator consumes one draw per chunk. Repeated PREAMBLE, HEADER, DATA, WHOLE, and result queries must preserve the recorded flags and draw count. Header failure and insufficient content must prevent DATA draws. A generic BER model needs no aggregation role for initialization. The fixture also checks pure PHY calculations across 64 HT20 modes, six lengths, and seven SNIR values. Production code and stored fingerprint expectations remain unchanged. Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: tests | behavior.add | - | ieee80211-ampdu-phy-reception --- .../module/Ieee80211AmpduPhyReception_1.test | 207 +++++++++++++++++- 1 file changed, 198 insertions(+), 9 deletions(-) diff --git a/tests/module/Ieee80211AmpduPhyReception_1.test b/tests/module/Ieee80211AmpduPhyReception_1.test index dd968143999..32ad3d7340d 100644 --- a/tests/module/Ieee80211AmpduPhyReception_1.test +++ b/tests/module/Ieee80211AmpduPhyReception_1.test @@ -1,12 +1,14 @@ %description: Real HT20 radios preserve incorrect DATA chunks after partial loss. -Scalar and dimensional controls use whole-signal and separate-part reception with seed 0. +Scalar and dimensional controls use whole-signal and separate-part reception with seeds 0, 1, and 2. Repeated decision and result queries must reuse probabilities and draws. Invalid headers and insufficient content must prevent member outcomes through distinct decisions. Production MAC initialization accepts generic PHY error models without an aggregation role. Ordinary NIST, Yans, absent-model, and non-HT controls retain their defined behavior and metadata. -%extraargs: -r 0..123 +%extraargs: -r 0..467 + +%postrun-command: python3 check-models.py %file: AmpduPhyReception.cc #include "inet/common/ProtocolTag_m.h" @@ -23,13 +25,27 @@ Ordinary NIST, Yans, absent-model, and non-HT controls retain their defined beha #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/SnirBase.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" using namespace inet; using namespace inet::physicallayer; +class AmpduReferenceSnir : public SnirBase +{ + double value; + public: + explicit AmpduReferenceSnir(double value) : SnirBase(nullptr, nullptr), value(value) {} + double getMin() const override { return value; } + double getMax() const override { return 2 * value; } + double getMean() const override { return 1.5 * value; } +}; + static std::vector chunkOutcomes(const Ptr& data) { + if (data == nullptr) + return {}; Packet packet("outcomes", data); std::vector outcomes; b offset(0); @@ -40,6 +56,76 @@ static std::vector chunkOutcomes(const Ptr& data) return outcomes; } +class AmpduModelAudit +{ + public: + virtual ~AmpduModelAudit() = default; + mutable int chunkCalls = 0; + mutable int headerCalls = 0; + mutable int preambleCalls = 0; + mutable int wholeCalls = 0; + mutable double headerError = NaN; + mutable std::vector probabilities; + std::vector outcomes; +}; + +template class AmpduSharedModel : public Base, public AmpduModelAudit +{ + protected: + void initialize(int stage) override { + Base::initialize(stage); + if (stage != INITSTAGE_PHYSICAL_LAYER || !this->par("referenceMatrix").boolValue()) + return; + auto draws = this->getRNG(0)->getNumbersDrawn(); + int checked = 0; + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + bool supported = mode->getHtMcsIndex() >= 0 && mode->getHtMcsIndex() <= 31 && mode->getDataMode()->getBandwidth() == MHz(20); + if (!supported) + continue; + for (double value : {0.01, 0.1, 1.0, 3.0, 10.0, 100.0, 1000.0}) { + AmpduReferenceSnir snir(value); + double scalar = (this->par("snirMode").stdstringValue() == "min" ? value : 1.5 * value) * + inet::math::dB2fraction(this->par("snirOffset")); + for (int length : {4, 100, 101, 102, 4095, 65535}) { + double expected = Base::getDataSuccessRate(mode, length * 8, scalar); + double actual = Base::getDataSuccessRate(mode, length * 8, this->getScalarSnir(&snir)); + ASSERT(actual == expected && actual >= 0 && actual <= 1); + checked++; + } + } + } + ASSERT(checked == 64 * 7 * 6); + ASSERT(this->getRNG(0)->getNumbersDrawn() == draws); + ASSERT(chunkCalls == 0 && headerCalls == 0 && preambleCalls == 0 && wholeCalls == 0); + } + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override { + double probability = Base::computePacketErrorRate(snir, part); + if (part == IRadioSignal::SIGNAL_PART_PREAMBLE) preambleCalls++; + else if (part == IRadioSignal::SIGNAL_PART_HEADER) { headerCalls++; headerError = probability; } + else wholeCalls++; + return probability; + } + double computeChunkSuccessRate(const ISnir *snir, b length) const override { + auto mode = check_and_cast(snir->getReception()->getTransmission())->getMode(); + auto draws = this->getRNG(0)->getNumbersDrawn(); + double actual = Base::computeChunkSuccessRate(snir, length); + double expected = Base::getDataSuccessRate(mode, b(length).template get(), this->getScalarSnir(snir)); + ASSERT(actual == expected); + ASSERT(this->getRNG(0)->getNumbersDrawn() == draws); + probabilities.push_back(expected); + chunkCalls++; + return actual; + } +}; + +class AmpduNistProbe : public AmpduSharedModel {}; +class AmpduYansProbe : public AmpduSharedModel {}; +Define_Module(AmpduNistProbe); +Define_Module(AmpduYansProbe); + class AmpduDeterministicModel : public Ieee80211NistErrorModel { public: @@ -119,6 +205,18 @@ class AmpduProbeRadio : public Ieee80211Radio ASSERT(model->headerCalls == 0); ASSERT(model->chunkCalls == 0); } + auto audit = dynamic_cast(getSubmodule("receiver")->getSubmodule("errorModel")); + if (audit && timer->getKind() == IRadioSignal::SIGNAL_PART_HEADER) { + ASSERT(audit->preambleCalls == 1 && audit->headerCalls == 0 && audit->chunkCalls == 0 && audit->wholeCalls == 0); + auto signal = check_and_cast(timer->getControlInfo()); + auto draws = getSubmodule("receiver")->getRNG(0)->getNumbersDrawn(); + for (int repeat = 0; repeat < 3; repeat++) { + ASSERT(medium->getReceptionDecision(this, signal->getListening(), signal->getTransmission(), IRadioSignal::SIGNAL_PART_PREAMBLE)->isReceptionSuccessful()); + ASSERT(medium->isReceptionSuccessful(this, signal->getTransmission(), IRadioSignal::SIGNAL_PART_PREAMBLE)); + } + ASSERT(audit->preambleCalls == 1 && audit->headerCalls == 0 && audit->chunkCalls == 0 && audit->wholeCalls == 0); + ASSERT(draws == getSubmodule("receiver")->getRNG(0)->getNumbersDrawn()); + } } void endReception(cMessage *timer) override { auto signal = check_and_cast(timer->getControlInfo()); @@ -131,7 +229,9 @@ class AmpduProbeRadio : public Ieee80211Radio auto listening = signal->getListening(); auto whole = medium->getReceptionDecision(this, listening, transmission, IRadioSignal::SIGNAL_PART_WHOLE); auto model = dynamic_cast(getSubmodule("receiver")->getSubmodule("errorModel")); + auto audit = dynamic_cast(getSubmodule("receiver")->getSubmodule("errorModel")); int calls = model ? model->chunkCalls : 0; + int auditCalls = audit ? audit->chunkCalls + audit->headerCalls + audit->preambleCalls + audit->wholeCalls : 0; auto draws = getSubmodule("receiver")->getRNG(0)->getNumbersDrawn(); auto result = medium->getReceptionResult(this, listening, transmission); for (int repeat = 0; repeat < 3; repeat++) { @@ -145,6 +245,11 @@ class AmpduProbeRadio : public Ieee80211Radio } ASSERT(draws == getSubmodule("receiver")->getRNG(0)->getNumbersDrawn()); ASSERT(model == nullptr || calls == model->chunkCalls); + if (audit) { + ASSERT(auditCalls == audit->chunkCalls + audit->headerCalls + audit->preambleCalls + audit->wholeCalls); + auto data = check_and_cast(medium->getReceptionDecision(this, listening, transmission, IRadioSignal::SIGNAL_PART_DATA)); + audit->outcomes = chunkOutcomes(data->getData()); + } } Ieee80211Radio::endReception(timer); } @@ -248,7 +353,8 @@ class AmpduPhyDriver : public cSimpleModule, public cListener packet->addTag()->setMode(Ieee80211ModeSet::getModeSet("g(mixed)")->getMode(Mbps(6))); for (int i = 0; scenario() != 29 && i < modes->getNumModes(); i++) { auto mode = modes->getMode(i); - if (mode->getHtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20) && mode->isHtShortGuardInterval() == (scenario() == 15)) { + if (mode->getHtMcsIndex() == (scenario() >= 35 ? 7 : 0) && mode->getDataMode()->getBandwidth() == MHz(20) && + mode->isHtShortGuardInterval() == (scenario() == 15 || (scenario() >= 31 && (scenario() - 31) % 4 >= 2))) { packet->addTag()->setMode(mode); break; } @@ -257,6 +363,19 @@ class AmpduPhyDriver : public cSimpleModule, public cListener } else { auto packet = check_and_cast(message); + if (scenario() >= 31) { + auto indication = packet->getTag(); + retained = makeShared(*indication); + retainedOutcomes = chunkOutcomes(packet->peekData(Chunk::PF_ALLOW_ALL)); + ASSERT(packet->getDataLength() == B(322)); + ASSERT(packet->cPacket::hasBitError() == !indication->getHeaderSuccessful()); + ASSERT(packet->hasBitError() == (!indication->getHeaderSuccessful() || !packet->peekData(Chunk::PF_ALLOW_ALL)->isCorrect())); + auto error = packet->getTag(); + ASSERT(std::isnan(error->getPacketErrorRate()) && std::isnan(error->getBitErrorRate()) && std::isnan(error->getSymbolErrorRate())); + received++; + delete packet; + return; + } bool refused = scenario() == 10 || scenario() == 11 || scenario() == 12 || scenario() == 13 || scenario() == 14 || scenario() == 17 || scenario() == 18 || scenario() == 19 || scenario() == 30; ASSERT(packet->cPacket::hasBitError() == refused); @@ -310,8 +429,36 @@ class AmpduPhyDriver : public cSimpleModule, public cListener } bool headerFailure = scenario() == 10 || scenario() == 11 || scenario() == 12 || scenario() == 13 || scenario() == 17 || scenario() == 18 || scenario() == 30; bool separate = getModuleByPath("^.node[1].radio")->par("separateReceptionParts"); - ASSERT(received == (headerFailure && separate ? 0 : 1)); auto rx = getModuleByPath("^.node[1].radio.receiver"); + if (scenario() >= 31) { + auto audit = check_and_cast(rx->getSubmodule("errorModel")); + auto reference = getRNG(1); + bool header = audit->headerError == 0 || (audit->headerError != 1 && reference->doubleRand() > audit->headerError); + ASSERT(audit->preambleCalls == 1 && audit->headerCalls == 1 && audit->wholeCalls == 0); + ASSERT(audit->chunkCalls == (header ? 8 : 0)); + ASSERT(received == (separate && !header ? 0 : 1)); + std::string bits; + for (size_t i = 0; i < audit->probabilities.size(); i++) { + double probability = audit->probabilities[i]; + bool expected = probability == 1 || (probability != 0 && reference->doubleRand() > 1 - probability); + ASSERT(audit->outcomes.at(i) == expected); + ASSERT(retained && retainedOutcomes.at(i) == expected); + bits += expected ? '1' : '0'; + } + auto draws = rx->getRNG(0)->getNumbersDrawn(); + ASSERT(draws == reference->getNumbersDrawn()); + auto next = rx->getRNG(0)->intRand(); + ASSERT(next == reference->intRand()); + auto cache = check_and_cast(getModuleByPath("^.radioMedium"))->getCommunicationCache(); + ASSERT(cache->getNumTransmissions() == 0); + EV << "MODEL_RESULT scenario=" << scenario() << " seed=" << par("seed").intValue() + << " analog=" << getModuleByPath("^.radioMedium")->par("signalAnalogRepresentation").stdstringValue() + << " parts=" << separate << " header=" << header << " bits=" << (bits.empty() ? "none" : bits) + << " draws=" << draws << " next=" << next << "\n"; + EV << "PHY reception checks passed\n"; + return; + } + ASSERT(received == (headerFailure && separate ? 0 : 1)); if (scenario() == 30) ASSERT(rx->getRNG(0)->getNumbersDrawn() == 0); auto model = dynamic_cast(rx->getSubmodule("errorModel")); if (model) { @@ -337,8 +484,21 @@ import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; import inet.physicallayer.wireless.common.medium.RadioMedium; import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211YansErrorModel; import inet.physicallayer.wireless.common.base.packetlevel.ErrorModelBase; +module AmpduNistProbe extends Ieee80211NistErrorModel +{ + parameters: + bool referenceMatrix = default(false); + @class(::AmpduNistProbe); +} +module AmpduYansProbe extends Ieee80211YansErrorModel +{ + parameters: + bool referenceMatrix = default(false); + @class(::AmpduYansProbe); +} module AmpduLimitedModel extends Ieee80211NistErrorModel { parameters: @@ -361,7 +521,7 @@ module AmpduProbeRadio extends Ieee80211Radio { parameters: int scenario; - receiver.errorModel.typename = default(scenario == 29 ? "GenericBerModel" : (scenario == 0 ? "Ieee80211NistErrorModel" : (scenario == 1 || scenario == 21 ? "Ieee80211YansErrorModel" : (scenario == 2 || scenario >= 17 ? "" : "AmpduDeterministicModel")))); + receiver.errorModel.typename = default(scenario >= 31 ? ((scenario - 31) % 2 == 0 ? "AmpduNistProbe" : "AmpduYansProbe") : (scenario == 29 ? "GenericBerModel" : (scenario == 0 ? "Ieee80211NistErrorModel" : (scenario == 1 || scenario == 21 ? "Ieee80211YansErrorModel" : (scenario == 2 || scenario >= 17 ? "" : "AmpduDeterministicModel"))))); @class(::AmpduProbeRadio); } module AmpduCapabilityRadio extends Ieee80211Radio @@ -378,6 +538,7 @@ simple AmpduPhyDriver parameters: @class(::AmpduPhyDriver); int scenario; + int seed; gates: input in[2]; output out[2]; @@ -419,21 +580,29 @@ network Test network = Test ned-path = .;../../../../src sim-time-limit = 20ms -seed-set = 0 +seed-set = ${seed=0,1,2} +num-rngs = 2 +seed-0-mt = ${seed} +seed-1-mt = ${seed} cmdenv-express-mode = false **.scalar-recording = true **.vector-recording = false *.node[*].radio.opMode = ${scenario} == 29 ? "g(mixed)" : "n(mixed-2.4Ghz)" *.node[*].radio.initialRadioMode = "transceiver" -*.driver.scenario = ${scenario=0..30} +*.driver.scenario = ${scenario=0..38} +*.driver.seed = ${seed} *.node[*].radio.scenario = ${scenario} *.node[*].radio.receiver.errorModel.scenario = ${scenario} +*.node[1].radio.receiver.errorModel.referenceMatrix = (${scenario} == 31 || ${scenario} == 32) && !${parts} && ${seed} == 0 +*.node[*].radio.receiver.errorModel.snirMode = ${analog} == "scalar" ? "min" : "mean" +*.node[*].radio.receiver.errorModel.cmdenv-log-level = off *.node[*].radio.separateReceptionParts = ${parts=false,true} *.node[*].radio.signalAnalogRepresentation = ${analog="scalar","dimensional"} *.radioMedium.signalAnalogRepresentation = ${analog} *.node[*].radio.sendRawBytes = ${scenario} == 21 *.node[*].radio.receiver.bandwidth = 20MHz -*.node[*].radio.receiver.snirThreshold = ${scenario} == 30 ? 100dB : 4dB +*.node[*].radio.receiver.energyDetection = ${scenario} >= 35 ? -49dBm : (${scenario} >= 31 ? -29dBm : -85dBm) +*.node[*].radio.receiver.snirThreshold = ${scenario} == 30 ? 100dB : (${scenario} >= 31 ? -10dB : 4dB) *.node[*].radio.antenna.mobilityModule = "^.^.mobility" *.node[*].mobility.initFromDisplayString = false *.node[*].mobility.initialX = 10m + parentIndex() * 1m @@ -445,7 +614,7 @@ cmdenv-express-mode = false *.node[*].mobility.constraintAreaMaxX = 100m *.node[*].mobility.constraintAreaMaxY = 100m *.node[*].mobility.constraintAreaMaxZ = 0m -*.radioMedium.backgroundNoise.power = -110dBm +*.radioMedium.backgroundNoise.power = ${scenario} >= 35 ? -50dBm : (${scenario} >= 31 ? -30dBm : -110dBm) *.probe.wlan[0].opMode = ${scenario} == 26 || ${scenario} == 27 ? "g(mixed)" : "n(mixed-2.4Ghz)" *.probe.wlan[0].radio.typename = "AmpduCapabilityRadio" *.probe.wlan[0].radio.scenario = ${scenario} @@ -478,3 +647,23 @@ $self add Mode54Mb 128 0 0 %contains: stdout PHY reception checks passed + +%file: check-models.py +from pathlib import Path +import re + +text = Path("test.out").read_text() +assert "Run statistics: total 468, successful 468" in text +rows = [dict(re.findall(r"(\w+)=([^ ]+)", line)) for line in text.splitlines() if line.startswith("MODEL_RESULT ")] +assert len(rows) == 96, len(rows) +groups = {} +for row in rows: + key = tuple(row[name] for name in ("scenario", "seed", "analog")) + outcome = tuple(row[name] for name in ("header", "bits", "draws", "next")) + groups.setdefault(key, []).append(outcome) +assert len(groups) == 48 +assert all(len(values) == 2 and values[0] == values[1] for values in groups.values()) +for model in (0, 1): + selected = [row for row in rows if (int(row["scenario"]) - 31) % 2 == model] + assert any("0" in row["bits"] and "1" in row["bits"] for row in selected), selected +print("NIST/Yans references, seeded outcomes, and signal-part equivalence passed.") From 614f94a64025c982067e099043fc03071cfa98f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 27 Oct 2026 20:34:18 +0100 Subject: [PATCH 08/24] tests: add: verify ordinary PHY compatibility with the parent Before this commit, ordinary reception controls lack a fixture that compiles unchanged on the pinned parent and the feature branch. The earlier receiver fixture cannot provide that independent compatibility comparison. After this commit, one fixture records ordinary reception with identical inputs on both revisions. NIST, Yans, absent-model, and non-HT custom controls cover three noise levels and seeds 0, 1, and 2. The campaign uses scalar and dimensional radios with whole and separate signal parts. Each record includes outcomes, duration, metadata, draw counts, and subsequent random values. For example, a failed ordinary reception must retain the same metadata and next random value as its parent control. The migration guide states the independent-range abstraction and its limits. MAC aggregate receipt and implicit Block Ack remain pending. BerTable retains its documented parent crash, which supplies no successful reception evidence. The additional fixture and records change no production contract or stored expectation. Execution results belong in the PR description and execution record. Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: tests | behavior.add | - | ieee80211-ampdu-phy-reception --- .../Ieee80211AmpduPhyCompatibility_1.test | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tests/module/Ieee80211AmpduPhyCompatibility_1.test diff --git a/tests/module/Ieee80211AmpduPhyCompatibility_1.test b/tests/module/Ieee80211AmpduPhyCompatibility_1.test new file mode 100644 index 00000000000..b3ee5f8c54f --- /dev/null +++ b/tests/module/Ieee80211AmpduPhyCompatibility_1.test @@ -0,0 +1,226 @@ +%description: +Ordinary MPDU controls use the same source on the pinned parent and feature branch. +NIST, Yans, absent-model, and non-HT custom controls cover three noise levels and seeds 0, 1, and 2. +Scalar and dimensional radios cover whole-signal and separate-part reception. +CONTROL records expose outcomes, duration, metadata, draw counts, and the next random value for exact comparison. + +%extraargs: -r 0..575 + +%postrun-command: python3 check-controls.py + +%file: Compatibility.cc +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; + +class CompatibilityNoRole : public ErrorModelBase +{ + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeBitErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeSymbolErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } +}; +Define_Module(CompatibilityNoRole); + +class CompatibilityDriver : public cSimpleModule, public cListener +{ + int received = 0; + bool error = false; + simtime_t duration; + std::string metadata = "absent"; + protected: + void initialize() override { + getModuleByPath("^.node[0].radio")->subscribe(transmissionStartedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); + } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *) override { + auto transmission = check_and_cast(object); + auto header = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + ASSERT(header->getChunkLength() == transmission->getMode()->getHeaderMode()->getLength()); + duration = transmission->getEndTime() - transmission->getStartTime(); + } + void handleMessage(cMessage *message) override { + if (message->isSelfMessage()) { + delete message; + auto packet = new Packet("ordinary", makeShared(B(100))); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + auto modes = Ieee80211ModeSet::getModeSet(par("model").intValue() == 3 ? "g(mixed)" : "n(mixed-2.4Ghz)"); + const IIeee80211Mode *selected = nullptr; + if (par("model").intValue() == 3) + selected = modes->getMode(Mbps(6)); + else { + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == par("mcs").intValue() && mode->getDataMode()->getBandwidth() == MHz(20) && + mode->isHtShortGuardInterval() == par("shortGi").boolValue()) + selected = mode; + } + } + ASSERT(selected); + packet->addTag()->setMode(selected); + send(packet, "out", 0); + return; + } + auto packet = check_and_cast(message); + ASSERT(packet->getDataLength() == B(100)); + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() == (par("model").intValue() == 3 ? -1 : par("mcs").intValue())); + ASSERT(packet->getTag()->getChannel() != nullptr); + auto time = packet->getTag(); + ASSERT(time->getStartTime() >= SIMTIME_ZERO); + ASSERT(time->getEndTime() - time->getStartTime() == duration); + std::ostringstream output; + output << std::setprecision(17); + auto rate = packet->findTag(); + ASSERT((rate != nullptr) == (par("model").intValue() != 2)); + if (rate) + output << "per:" << rate->getPacketErrorRate() << ",ber:" << rate->getBitErrorRate() << ",ser:" << rate->getSymbolErrorRate(); + else + output << "absent"; + output << ",power:" << packet->getTag()->getPower().get() + << ",snir:" << packet->getTag()->getMinimumSnir() + << ",start:" << time->getStartTime().raw() << ",end:" << time->getEndTime().raw(); + metadata = output.str(); + error = packet->hasBitError(); + received++; + delete packet; + } + using cListener::finish; + void finish() override { + ASSERT(received <= 1); + if (par("noise").intValue() == 0 || par("model").intValue() >= 2) + ASSERT(received == 1 && !error); + auto rng = getModuleByPath("^.node[1].radio.receiver")->getRNG(0); + auto draws = rng->getNumbersDrawn(); + if (par("model").intValue() >= 2) ASSERT(draws == 0); + EV << "CONTROL model=" << par("model").intValue() << " mcs=" << par("mcs").intValue() + << " gi=" << par("shortGi").boolValue() << " noise=" << par("noise").intValue() + << " seed=" << par("seed").intValue() << " parts=" << par("parts").boolValue() + << " analog=" << par("analog").stdstringValue() << " received=" << received << " error=" << error + << " duration=" << duration.raw() << " metadata=" << metadata << " draws=" << draws << " next=" << rng->intRand() << "\n"; + } +}; +Define_Module(CompatibilityDriver); + +%file: test.ned +import inet.mobility.static.StationaryMobility; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.common.base.packetlevel.ErrorModelBase; + +module CompatibilityNoRole extends ErrorModelBase { parameters: @class(::CompatibilityNoRole); } +module CompatibilityRadio extends Ieee80211Radio +{ + parameters: + int model; + receiver.errorModel.typename = default(model == 0 ? "Ieee80211NistErrorModel" : (model == 1 ? "Ieee80211YansErrorModel" : (model == 2 ? "" : "CompatibilityNoRole"))); +} +simple CompatibilityDriver +{ + parameters: + @class(::CompatibilityDriver); + int model; + int mcs; + bool shortGi; + int noise; + int seed; + bool parts; + string analog; + gates: + input in[2]; + output out[2]; +} +module CompatibilityNode +{ + parameters: + @networkNode; + gates: + input in; + output out; + input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: CompatibilityRadio; + connections: + in --> radio.upperLayerIn; + radio.upperLayerOut --> out; + radioIn --> radio.radioIn; +} +network Test +{ + submodules: + radioMedium: Ieee80211RadioMedium; + driver: CompatibilityDriver; + node[2]: CompatibilityNode; + connections: + for i=0..1 { + driver.out[i] --> node[i].in; + node[i].out --> driver.in[i]; + } +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = ${seed=0,1,2} +seed-0-mt = ${seed} +cmdenv-express-mode = false +**.vector-recording = false +**.scalar-recording = false +*.driver.model = ${model=0,1,2,3} +*.driver.mcs = ${mcs=0,7} +*.driver.shortGi = ${gi=false,true} +*.driver.noise = ${noise=0,1,2} +*.driver.seed = ${seed} +*.driver.parts = ${parts=false,true} +*.driver.analog = ${analog="scalar","dimensional"} +*.node[*].radio.opMode = ${model} == 3 ? "g(mixed)" : "n(mixed-2.4Ghz)" +*.node[*].radio.initialRadioMode = "transceiver" +*.node[*].radio.separateReceptionParts = ${parts} +*.node[*].radio.signalAnalogRepresentation = ${analog} +*.radioMedium.signalAnalogRepresentation = ${analog} +*.node[*].radio.model = ${model} +*.node[*].radio.receiver.bandwidth = 20MHz +*.node[*].radio.receiver.energyDetection = ${noise} == 0 ? -85dBm : (${noise} == 1 ? (${mcs} == 0 ? -29dBm : -49dBm) : (${mcs} == 0 ? -27.9dBm : -42.9dBm)) +*.node[*].radio.receiver.snirThreshold = -10dB +*.node[*].radio.antenna.mobilityModule = "^.^.mobility" +*.node[*].mobility.initFromDisplayString = false +*.node[*].mobility.initialX = 10m + parentIndex() * 1m +*.node[*].mobility.initialY = 10m +*.node[*].mobility.initialZ = 0m +*.node[*].mobility.constraintAreaMinX = 0m +*.node[*].mobility.constraintAreaMinY = 0m +*.node[*].mobility.constraintAreaMinZ = 0m +*.node[*].mobility.constraintAreaMaxX = 100m +*.node[*].mobility.constraintAreaMaxY = 100m +*.node[*].mobility.constraintAreaMaxZ = 0m +*.radioMedium.backgroundNoise.power = ${noise} == 0 ? -110dBm : (${noise} == 1 ? (${mcs} == 0 ? -30dBm : -50dBm) : (${mcs} == 0 ? -28dBm : -43dBm)) + +%contains: stdout +CONTROL model= + +%file: check-controls.py +from pathlib import Path +import re + +text = Path("test.out").read_text() +assert "Run statistics: total 576, successful 576" in text +rows = [dict(re.findall(r"(\w+)=([^ ]+)", line)) for line in text.splitlines() if line.startswith("CONTROL ")] +keys = [tuple(row[name] for name in ("model", "mcs", "gi", "noise", "seed", "parts", "analog")) for row in rows] +assert len(rows) == len(set(keys)) == 576 +for model in ("0", "1"): + selected = [row for row in rows if row["model"] == model] + assert any(int(row["draws"]) > 0 for row in selected) + assert any(row["error"] == "1" or row["received"] == "0" for row in selected) +print("All 576 ordinary reception controls passed.") From c136f222f30fab53144b438e28ff9bc3b7107c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 3 Nov 2026 16:17:25 +0100 Subject: [PATCH 09/24] plan: change: define chunk-based PHY reception boundaries Before this commit, the plans describe aggregate-specific probability roles and separate range outcomes. They also describe bounded deaggregation as a prerequisite in the same visible PR. These descriptions obscure the responsibilities of the PHY error model and the MAC parser. After this commit, the plans assign chunk corruption to the common PHY interface. The receiver caches the received chunks. Error models use PHY conditions and bit lengths without MAC format knowledge. MAC extraction retains delimiter parsing and member validation. Raw byte input has one DATA outcome because byte conversion removes its original chunk boundaries. The plans also separate the deaggregation prerequisite from recovery and partial PHY reception. Master supplies bounded extraction. The dependent work adds delimiter recovery and cached chunk errors. Member acceptance and implicit Block Ack remain separate future work. Expected results and retained historical evidence remain distinct. Plan: plan/pending/ieee80211-ampdu-phy-reception.md Change: plan | behavior.change | - | ieee80211-ampdu-phy-reception --- ...ee80211-ampdu-implicit-blockack-summary.md | 22 +-- .../ieee80211-ampdu-implicit-blockack.md | 12 +- plan/pending/ieee80211-ampdu-phy-reception.md | 145 +++++++++--------- 3 files changed, 86 insertions(+), 93 deletions(-) diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md index 3253f199433..6c56fc574dd 100644 --- a/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md +++ b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md @@ -10,7 +10,7 @@ Master contains the merged explicit Compressed Block Ack implementation from PR The PHY plan supplies aggregate reception before the MAC feature uses it. Its first gate proves deterministic member outcomes and includes the shared NIST/Yans implementation. It also preserves default HT operation and reception without an error model. Its second gate extends the NIST/Yans and compatibility evidence. The MAC feature can reach deterministic integration between those gates. -The series defines PHY P0 through P4 and gates D1 and D2. The final execution record states verification and its limits. VHT identity, bounded inspection, deaggregation use, and HT-SIG representation have separate commit boundaries. P3 checks NIST/Yans probabilities and seeded outcomes. P4 supplies ordinary controls. MAC aggregate receipt and implicit Block Ack remain pending. +The ten-commit series supplies PHY P0 through P4 and gates D1 and D2. The [final execution record](ieee80211-ampdu-phy-execution.md) states verification and its limits. VHT identity, bounded inspection, deaggregation use, and HT-SIG representation have separate commit boundaries. P3 checks NIST/Yans probabilities and seeded outcomes. P4 supplies ordinary controls. MAC aggregate receipt and implicit Block Ack remain pending. The merge prerequisite is satisfied on the integrated master base. MAC step A0 must compare the contracts and record a feature-off baseline before MAC implementation. @@ -47,7 +47,7 @@ The radio already assigns that 48-bit header length. P0 records its current valu Correct bytes alone do not establish rejection of an invalid received header. P1 also defines one stateless HT-SIG validator. P2 applies it before member outcomes and before radio decapsulation trusts HT Length. Invalid CRC or unsupported fields cause HEADER failure without delimiter or member draws. The current header-failure paths prevent receipt updates, delivery, and responses. [HT-SIG receive validation](ieee80211-ampdu-phy-reception.md#ht-sig-receive-validation). -A separate bounds check refuses modeled PSDU content shorter than a valid header's HT Length. HEADER retains success and the decoded Aggregation property. DATA reports failure without range outcomes or draws, and MAC refuses the content before header or trailer peeks. Decapsulation skips length-based trailer removal. This case proves bounds safety for malformed input. It establishes no physical carrier-loss behavior or acceptance of a received prefix. +A separate bounds check refuses modeled PSDU content shorter than a valid header's HT Length. HEADER retains success and the decoded Aggregation property. DATA reports failure without chunk error flags or draws, and MAC refuses the content before header or trailer peeks. Decapsulation skips length-based trailer removal. This case proves bounds safety for malformed input. It establishes no physical carrier-loss behavior or acceptance of a received prefix. The diagram shows two members. Nonfinal HT subframes need alignment padding of zero to three octets. The final HT subframe has no alignment padding. Extra zero-length delimiters can provide the minimum MPDU start spacing that the receiver requires. @@ -70,7 +70,7 @@ Each complete HT member must fit the 4095-octet delimiter limit. The whole aggre The first transmit profile uses packet-level `Ieee80211Radio`, scalar or dimensional reception, HT mixed format, 20 MHz, and long GI. Aggregate transmission requires fixed DATA rates and no HCF rate-control submodule. Each aggregate serves one recipient and TID under an accepted immediate Compressed agreement. Compressed responses retain the 64-bit bitmap. Multiple peers and agreements can coexist; each retains separate state. [Supported profile](ieee80211-ampdu-implicit-blockack.md#first-supported-profile). -Receive support also covers advertised HT20 short-GI modes at the permitted MCS and NSS values. Before the MIB update, the MAC supplies its exact mode set, effective channel widths, and NSS limit for receiver validation. The check covers advertised modes; unsupported catalog modes outside those limits do not cause rejection. A peer can select an advertised short-GI receive mode even when the local aggregate transmitter uses long GI. The new transmit policy remains limited to long GI. [Required receive coverage](ieee80211-ampdu-phy-reception.md#42-optional-role-and-shared-implementation). +Receive support covers HT20 long GI and short GI within the current radio limits. The new transmit policy remains limited to long GI. [PHY receive contract](ieee80211-ampdu-phy-reception.md#42-generic-data-corruption). HCF coordinates eligibility and the exchange. Management and the MIB supply local and peer capabilities. The proposed HT policy selects members within those capabilities and the agreement's transmit window. The policy cannot grant an agreement or bypass a PHY limit. @@ -89,11 +89,11 @@ flowchart TD The transmit feature requires an explicit policy choice. The default policy continues to select no A-MPDU. A receiver must recognize supported received aggregates even when its own transmitter uses the default policy. Absent peer HT state retains S5's Normal Ack fallback. -The WLAN receiver checks role presence at PHY initialization. The MAC requests exact coverage validation later, before its MIB update at link-layer initialization. Absent roles and incomplete advertised coverage cause failure before management sends capability information. These checks apply even when local transmit aggregation remains disabled. Non-HT custom models retain ordinary MPDU reception without that role. [Early compatibility checks](ieee80211-ampdu-phy-reception.md#44-model-detail-and-diagnostics). +Generic DATA corruption accepts chunk content without MAC format knowledge. The common base uses BER; the WLAN base uses its existing DATA success calculation. An unavailable probability causes a runtime error. No separate MAC capability validation applies. -HT length exponent zero still advertises 8191 octets; it cannot disable aggregate reception. A model that exposes the role but lacks advertised short-GI support therefore fails initialization. A one-antenna configuration must pass when the model supports every advertised mode. The catalog can contain unsupported modes for more spatial streams. The validation adds no second capability store or independent filter. +Generic DATA corruption accepts chunk content without MAC format knowledge. The common base uses BER; the WLAN base uses its existing DATA success calculation. An unavailable probability causes a runtime error. No separate MAC capability validation applies. -BerTable remains outside the HT aggregate profile. Its current initializer rejects HT operation strings before the new role check. This delivery adds no aggregate role to BerTable. The migration guide directs supported HT configurations to NIST or Yans. The PHY fixture compares a non-HT parent control and verifies the current HT refusal separately from the custom-model role diagnostic. [Compatibility decision and checks](ieee80211-ampdu-phy-reception.md#5-affected-components-and-compatibility). +BerTable retains its current HT configuration limit. The generic DATA operation adds no aggregation role. NIST and Yans calculate chunk probabilities through their existing PHY calculations. An absent error model retains the current error-free abstraction after PHY acquisition, possibility, attempt, and SNIR checks. Successful common checks permit physical success for bounded delimiter and member ranges without new error draws. Delimiter, FCS, address, and agreement checks still control MAC acceptance. This case adds no replacement module or interface requirement. @@ -196,7 +196,7 @@ flowchart TD END -->|No| SILENT["Send no Block Ack"] ``` -The cache keeps separate entries for each receiver, transmission, and signal part. PREAMBLE preserves the current per-part checks without early HEADER or DATA calculation. HEADER reuses PREAMBLE before its own header calculation. DATA owns bounded range outcomes or structural refusal after successful header reception. A WHOLE query derives its summary from those entries without another error draw. Aggregate success queries must use those same entries. +The cache keeps separate entries for each receiver, transmission, and signal part. PREAMBLE preserves the current per-part checks without early HEADER or DATA calculation. HEADER reuses PREAMBLE before its own header calculation. DATA owns bounded chunk error flags or structural refusal after successful header reception. A WHOLE query derives its summary from those entries without another error draw. Aggregate success queries must use those same entries. In separate-part mode, PREAMBLE or HEADER failure aborts the attempted reception before a container reaches MAC. Existing radio state notifications let HCF invalidate its physical history. Whole-signal mode can supply a failure container that MAC rejects before header or trailer parsing. Both paths prevent acceptance, responses, and range draws. The design preserves their existing event boundaries. @@ -208,9 +208,9 @@ With a present error model, aggregate results retain `ErrorRateInd` with all thr The tag contract permits `NaN` for unknown probabilities. P0 must audit readers, filters, and recorders that can receive aggregate metadata before P2 implements this contract. A reachable incompatibility blocks that implementation until the PHY plan records its resolution. Tag omission requires evidence that all affected consumers accept absence. The PHY fixture must verify the identified consumer paths before D1 closes. -The [PHY plan](ieee80211-ampdu-phy-reception.md#42-optional-role-and-shared-implementation) defines an optional WLAN role for per-range probabilities and a pure mode-support query. A shared WLAN base implementation reuses the current length-based calculations for NIST, Yans, and compatible custom subclasses. P2 supplies this implementation and the MAC validation call before it enables the HT compatibility checks. The receiver checks the exact MAC capability inputs without a probability call or random draw. It calls the probability operations only for aggregates. An absent model follows the explicit error-free case. +The [PHY plan](ieee80211-ampdu-phy-reception.md#42-generic-data-corruption) defines generic DATA corruption through the existing error-model interface. Error models sample chunk flags from PHY conditions and bit lengths. The receiver caches the returned chunks. MAC code interprets delimiters and member correctness. Raw byte input has one DATA outcome because byte conversion removes the original boundaries. -Deterministic tests supply probabilities of zero or one through the same role. The real receiver still produces and caches the outcomes. Those tests establish receiver integration and selective recovery. They do not establish the full NIST/Yans profile. +Generic DATA corruption accepts chunk content without MAC format knowledge. The common base uses BER; the WLAN base uses its existing DATA success calculation. An unavailable probability causes a runtime error. No separate MAC capability validation applies. The shared NIST/Yans implementation uses independent delimiter and MPDU outcomes under the current scalar SNIR reduction. This is a packet-level abstraction. It does not model correlated FEC errors or reconstruct interference within each MPDU. D2 extends the evidence for that implementation before completion of the documented profile. @@ -351,7 +351,7 @@ PHY checks also prove HT-SIG golden bytes and independent decode, PREAMBLE query The proposed MAC checks reject aggregate transmit opt-in with AARF. Proposed response checks separate observable mismatches and retired callbacks from indistinguishable valid frames inside an active interval. MAC FCS acceptance remains pending. -Completed PHY checks reject absent aggregate roles and incomplete advertised receive-mode coverage before the MIB update. The PHY controls preserve ordinary HT reception with NIST, Yans, and an absent error model. Aggregate checks without an error model retain common PHY checks without new error draws. The PHY execution record states the direct evidence and its limits. +Generic DATA corruption accepts chunk content without MAC format knowledge. The common base uses BER; the WLAN base uses its existing DATA success calculation. An unavailable probability causes a runtime error. No separate MAC capability validation applies. A fixture peer supplies a valid HT20 short-GI aggregate through the real radio. D1 and A2 verify copied PHY indications without MAC receipt or response requirements. After A4 supplies member validation, A5 verifies MAC receipt and one Compressed response after SIFS. A6 verifies production capability negotiation with the local aggregate transmit policy at long GI. This receive case does not establish short-GI selection from production transmit queues. [Receive acceptance cases](ieee80211-ampdu-implicit-blockack.md#proposed-direct-checks). @@ -367,7 +367,7 @@ The first version excludes these larger features: - Reverse direction, mixed control/data aggregates, adaptive aggregate transmission, new rate-control algorithms, and general MAC stop/start recovery. - Bit-level FEC behavior, correlated error accuracy, errors confined to alignment padding, and a guaranteed performance gain. -Custom data services, MPDU policies, Tx/Rx modules, response policies, and response-rate selectors can need interface updates. The DATA rate-control API remains unchanged; aggregate transmit opt-in requires its HCF submodule to be absent. Present custom HT models need the optional PHY role with coverage of advertised receive modes. This includes configurations that previously used only ordinary MPDUs. Non-HT custom reception and operation without an error model remain available without that role. The implementation must document those obligations. +Generic DATA corruption accepts chunk content without MAC format knowledge. The common base uses BER; the WLAN base uses its existing DATA success calculation. An unavailable probability causes a runtime error. No separate MAC capability validation applies. The design reuses the public Packet API and proposes no edit to its sealed implementation. Related feature branches remain separate integration work. [Compatibility and scope](ieee80211-ampdu-implicit-blockack.md#5-affected-components-and-compatibility). diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack.md b/plan/pending/ieee80211-ampdu-implicit-blockack.md index 242d8c3ace1..d6856937def 100644 --- a/plan/pending/ieee80211-ampdu-implicit-blockack.md +++ b/plan/pending/ieee80211-ampdu-implicit-blockack.md @@ -77,7 +77,7 @@ Prior local edits affect `AGENTS.md`, the replacement plan, its summary, the agr The first aggregate transmit profile targets packet-level `Ieee80211Radio` with scalar or dimensional reception, HT mixed format, 20 MHz, and long GI. Aggregate transmission requires a fixed DATA rate and no HCF rate-control submodule. The transmitter must select an actual HT mode. An `ac` interface can qualify only when its actual transmission uses this HT profile and valid peer HT state. This does not add VHT PPDU support. -Reception also covers HT20 short GI when local HT capability advertises that receive mode. The PHY contract requires coverage of the advertised MCS and NSS values within the effective radio limits. Interface presence alone cannot establish that coverage. The MAC supplies its exact capability inputs to the receiver for validation before the MIB update. This receive requirement does not extend the new aggregate transmit policy to short GI. [PHY receive contract](ieee80211-ampdu-phy-reception.md#42-optional-role-and-shared-implementation). +Reception also covers HT20 short GI within the current radio limits. Error models use generic DATA chunk corruption. This receive requirement does not extend the new aggregate transmit policy to short GI. [PHY receive contract](ieee80211-ampdu-phy-reception.md#42-generic-data-corruption). Each aggregate contains unfragmented, individually addressed QoS Data MPDUs for one RA, TA, and TID. An accepted immediate Compressed agreement must exist. Multiple peers and agreements can coexist; each aggregate remains within one agreement. The first transmit policy forms ordinary MSDU members. New selection of nested A-MSDUs remains separate work; current individual A-MSDU operation remains available. @@ -87,7 +87,7 @@ A-MPDU transmission uses a new `HtMpduAggregationPolicy` through the current opt When no aggregate qualifies, use the current individual-frame path where its conditions permit transmission. Absent peer HT state retains S5's Normal Ack fallback. An enabled policy does not grant an agreement or override a PHY limit. -The PHY plan rejects a present HT model without the aggregate role or required receive-mode coverage before management advertises capabilities. This check applies independently of local transmit aggregation. An absent error model retains the current error-free abstraction after the required PHY checks. The PHY plan's [compatibility section](ieee80211-ampdu-phy-reception.md#5-affected-components-and-compatibility) defines custom-model migration and the supported absent-model case. +The PHY plan uses generic DATA corruption without an aggregation role or a separate capability check. An absent error model retains the current error-free abstraction after the required PHY checks. The [compatibility section](ieee80211-ampdu-phy-reception.md#5-affected-components-and-compatibility) defines custom-model migration. This work remains outside this plan: @@ -119,7 +119,7 @@ Current explicit Compressed and Basic exchanges remain supported. A valid explic 15. A correctly received aggregate member prevents EIFS, an access delay after specified reception errors. This also applies to valid overheard members. 16. Agreement removal during SIFS cancels an unsent aggregate before queued-source commitment, new sequence assignment, or DATA attempt accounting. Existing canonical members retain their assigned numbers unless current disposal rules remove them. 17. Aggregate transmit opt-in with a rate-control submodule fails initialization, even when a fixed-rate override would bypass its rate selection. -18. Present HT models without the aggregate role or required receive-mode coverage fail before capability advertisement, also with local transmit aggregation disabled. +18. PHY error models calculate chunk outcomes without an aggregation interface or MAC delimiter knowledge. 19. Response acceptance uses the active receive interval and observable frame fields. Retired local callbacks cannot complete another wait. 20. An absent error model preserves current PHY checks and adds no error draws. PHY D1 preserves default NIST and configured Yans operation. 21. The modeled HT-SIG header preserves HT Length and Aggregation through serialization and independent decode. @@ -223,7 +223,7 @@ The radio already declares a 48-bit HT header. PHY P0 records this length and th PHY P1 defines one stateless HT-SIG validator; P2 applies it at the receiver's HEADER boundary. Invalid CRC or unsupported fields prevent delimiter and member draws. MAC receives no accepted Aggregation property from that header. Current header-failure paths prevent receipt updates, delivery, and responses. Encoded-header rejection needs separate evidence from probabilistic header loss. [HT-SIG receive validation](ieee80211-ampdu-phy-reception.md#ht-sig-receive-validation). -The PHY separately bounds valid HT Length against the available modeled PSDU before range calculation or length-based decapsulation. Insufficient content denotes malformed model input. HEADER retains success and its decoded Aggregation property; DATA refuses the content without range outcomes or draws. The radio skips length-based trailer removal and supplies a failure container. MAC refuses that content before header or trailer peeks. This case establishes bounds safety, with no claim about physical carrier loss or acceptance of a received prefix. +The PHY separately bounds valid HT Length against the available modeled PSDU before range calculation or length-based decapsulation. Insufficient content denotes malformed model input. HEADER retains success and its decoded Aggregation property; DATA refuses the content without chunk error flags or draws. The radio skips length-based trailer removal and supplies a failure container. MAC refuses that content before header or trailer peeks. This case establishes bounds safety, with no claim about physical carrier loss or acceptance of a received prefix. PHY P2 defines PREAMBLE, HEADER, DATA, and WHOLE decisions at their actual radio boundaries. PREAMBLE must not construct HEADER or DATA. A2 accepts the mode-specific failed-header paths and applicable receive metadata. The PHY plan owns the aggregate probability fields; MAC must not present an aggregate estimate as a member estimate. [Decision and metadata contracts](ieee80211-ampdu-phy-reception.md#43-decision-ownership-and-random-draws). @@ -235,7 +235,7 @@ BerTable remains outside this HT aggregate profile and receives no aggregate rol An absent error model follows the current error-free abstraction after PHY acquisition, possibility, attempt, and SNIR checks. The receiver records successful physical ranges without error-model calls or random draws only after those checks succeed. MAC delimiter, FCS, address, and agreement checks still control acceptance. An absent model needs no substitute role or module. -D1 permits MAC integration and deterministic exchange checks. It includes the shared NIST/Yans implementation before the early HT check becomes active. D2 completes the broader NIST/Yans and compatibility evidence. A7 requires D2 before the feature claims the full NIST/Yans profile. The abstraction uses common PHY acquisition and header success with independent range outcomes under scalar SNIR reduction. It makes no claim about correlated FEC errors or interference within each member. +D1 permits MAC integration and deterministic exchange checks. It includes the shared NIST/Yans implementation before the early HT check becomes active. D2 completes the broader NIST/Yans and compatibility evidence. A7 requires D2 before the feature claims the full NIST/Yans profile. The abstraction uses common PHY acquisition and header success with independent chunk error flags under scalar SNIR reduction. It makes no claim about correlated FEC errors or interference within each member. ### 4.5 Receive validation and one response per PPDU @@ -415,7 +415,7 @@ The PHY plan defines two gates. D1 proves cached aggregate reception with determ | A1: Preserve capabilities and construct legal aggregates. | Extend MIB/management spacing state. Consume PHY P1 representation, HT-SIG serialization and validation, and bounded parsing. Implement aggregate construction calculations for spacing, serialization, and region-tag rules. | Capability and representation checks prove directional limits, valid aggregate bytes, and singleton identity after byte conversion. The PHY plan owns the shared wire fixture and HT-SIG round trips. | A0; PHY P1. | | A2: Accept deterministic PHY reception. | MAC/Rx consumers adopt the PHY D1 indication and aggregation property. Accept the four per-part decisions, header rejection, structural refusal, actual failure paths, and receive metadata contract. | Valid long-GI and short-GI aggregates supply copied PHY outcomes without a second error draw. Failure containers stop before MAC header or trailer peeks. Incomplete advertised receive-mode support fails before the MIB update. An absent model follows its supported error-free case. MAC receipt and response acceptance belong to A4 and A5. | A1; PHY D1. | | A3: Prepare and retain independent members. | Add the HT policy, dynamic member selection, and batch preparation. Require fixed DATA rates and reject aggregate transmit opt-in with a rate-control submodule. Select one mode before spacing and batch reduction. Apply A1's construction calculations at that mode. Include numbered, unsent candidates through current acknowledgment state. Preserve single-frame internal collision recovery and current queue/lifetime handoff. | Unsupported rate-control combinations fail before DATA attempts. Refusal preserves source ownership and sequence state. Numbered, unsent members retain their assigned numbers and first-DATA Retry value. Commitment retains independent deadlines. | A1. | -| A4: Receive aggregates and apply mandatory release. | Project range outcomes onto separate member Packet flags before Rx validation. Integrate one contention outcome, receipt updates, reorder context, and history. | Physical reception proves FCS rejection, correct EIFS behavior, delimiter continuity, mandatory release, and exactly-once delivery. Silent discard creates no receive state. | A1, A2. | +| A4: Receive aggregates and apply mandatory release. | Preserve chunk error flags in each extracted member before Rx validation. Integrate one contention outcome, receipt updates, reorder context, and history. | Physical reception proves FCS rejection, correct EIFS behavior, delimiter continuity, mandatory release, and exactly-once delivery. Silent discard creates no receive state. | A1, A2. | | A5: Complete the implicit exchange and probe recovery. | Add aggregate steps, Tx start/cancellation, member finalization, Block Ack wait, response calculation, admission, and transmitted-member records. Select RTS from the complete aggregate preview after each reduction. Apply one contention outcome after aggregate RTS failure without member retry accounting. Derive the probe EDCAF from TID. Apply the observable response acceptance rules. | Exchanges prove computed FCS, SIFS cancellation, selective retry, response validation, probe priority, unchanged deadlines, and both HT duration conditions. Threshold cases prove PSDU-based protection. Mixed-batch RTS failure preserves member state at the contention retry boundary. A short-GI aggregate updates MAC receipt and receives one Compressed response after SIFS. At most one probe occurs between DATA attempts. | A2, A3, A4. | | A6: Verify negotiation, recovery, and compatibility. | Add infrastructure and compatibility coverage. Verify absent-agreement recovery for finite batches and continuous arrivals with zero and nonzero inactivity values. Verify DATA progress for a healthy agreement under controlled response loss. | Real management establishes limits. Tests prove transfer completion, probe-driven retirement without queue drain, later setup, continued DATA transmission, and historical compatibility. | A5. | | A7: Document and verify the supported profile. | Add the example, user and migration text, release note, and execution record. Reconcile relevant expected-failure declarations under separate authorization. | A6 and PHY D2 evidence support the documented profile. Required publication checks pass before any separately authorized publication. | A6; PHY D2. | diff --git a/plan/pending/ieee80211-ampdu-phy-reception.md b/plan/pending/ieee80211-ampdu-phy-reception.md index aff74f51461..70300709ef5 100644 --- a/plan/pending/ieee80211-ampdu-phy-reception.md +++ b/plan/pending/ieee80211-ampdu-phy-reception.md @@ -10,7 +10,16 @@ The feature plan defines PSDU-based RTS selection, recovery before DATA submissi The project plan procedure remains a draft. -Later commits supply P0 through P4 for the declared PHY profile. The final record states verification and its limits. Only the final commit receives build and test execution. Earlier commits receive structural checks only. MAC acceptance and implicit Block Ack remain pending. +The ten-commit series supplies P0 through P4 for the declared PHY profile. The [final record](ieee80211-ampdu-phy-execution.md) states verification and its limits. Only the final commit receives build and test execution. Earlier commits receive structural checks only. MAC acceptance and implicit Block Ack remain pending. + + +## PR boundary and current evidence + +The [deaggregation prerequisite](ieee80211-mpdu-deaggregation.md) supplies bounded chunk traversal and ordinary member extraction. It stops at an invalid delimiter. The recovery PR adds a search for later delimiters, HT-SIG support, and partial PHY reception. The PHY receiver records outcomes as traversal supplies each delimiter and MPDU chunk. No `MpduRange` or `MpduLayout` production type remains. + +For example, a malformed middle delimiter declares 2000 octets where only 124 remain. The prerequisite returns only the first MPDU. Recovery examines later delimiter candidates at four-octet intervals and also returns the final complete MPDU. Extraction grants no MAC acceptance. + +The maintainer deferred all builds and tests for this revision. Historical results below apply only to their recorded source revisions. They do not verify the shared traversal or the new commit boundaries. Both PRs require new build and test evidence. ## 1. Problem and intended behavior @@ -24,13 +33,13 @@ The PHY reports reception facts. The MAC separately checks delimiters, FCS value flowchart TD RX["One received HT PPDU"] --> H{"Do PHY acquisition and
header reception succeed?"} H -->|No| FAIL["Report no valid aggregate content"] - H -->|Yes| R["Obtain bounded delimiter
and MPDU ranges"] + H -->|Yes| R["Obtain bounded DATA chunks"] R --> MODEL{"Is an error model present?"} - MODEL -->|Yes| E["Obtain each range probability
from the selected error model"] + MODEL -->|Yes| E["Sample chunk error flags
from PHY conditions"] MODEL -->|No| GOOD["Record physical success
Retain delimiter and FCS checks"] E --> C["Sample when necessary
Retain immutable cached outcomes"] GOOD --> C - C --> I["Radio supplies a typed indication
with copied range outcomes"] + C --> I["Radio supplies a typed indication
with copied chunk error flags"] I --> MAC["MAC applies delimiter, FCS,
address, and agreement checks"] ``` @@ -43,23 +52,23 @@ The source assessment established the facts below at the pinned revision. The co | Source and current behavior | Consequence | | --- | --- | | The PER operation in [IErrorModel](../../src/inet/physicallayer/wireless/common/contract/packetlevel/IErrorModel.h) accepts an SNIR object and signal part. It has no arbitrary range-length argument. | An aggregate calculation needs an explicit callable range boundary. A generic whole-DATA call cannot score each member. | -| [Ieee80211ErrorModelBase](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h) declares protected header and DATA success calculations with a bit length. | The shared WLAN implementation can reuse those calculations for range probabilities. | +| [Ieee80211ErrorModelBase](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h) declares protected header and DATA success calculations with a bit length. | The shared WLAN implementation can reuse those calculations for chunk probabilities. | | [NIST](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h) and [Yans](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.h) derive from that WLAN base. | One base implementation can support both models and compatible custom subclasses. | -| [BerTable](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211BerTableErrorModel.h) derives directly from the generic base. Its [initializer](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211BerTableErrorModel.cc) rejects HT operation strings at the local stage. | BerTable remains outside the HT aggregate profile. Its current HT refusal must remain distinct from the new role check. | +| [BerTable](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211BerTableErrorModel.h) derives directly from the generic base. Its [initializer](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211BerTableErrorModel.cc) rejects HT operation strings at the local stage. | BerTable remains outside the HT aggregate profile. Its current HT refusal remains unchanged. | | [Ieee80211Receiver](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc) delegates reception results to the common receiver path. | Aggregate reception needs its own outcomes before whole-DATA failure can discard valid members. | | [Ieee80211PhyHeader.msg](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader.msg) has no modeled HT aggregation field. | A PHY property must distinguish an A-MPDU from a standalone MPDU. This also applies to a singleton aggregate. | | The HT methods in [Ieee80211PhyHeaderSerializer](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeaderSerializer.cc) write no fields and reconstruct an empty header. | P1 must extend the typed HT-SIG header and serializer together. A new MSG field alone cannot survive conversion to bytes. | -| [Ieee80211Radio](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc) validates some PHY headers but performs no HT CRC check. | Correct serialization alone cannot establish rejection of an invalid received HT-SIG. P2 needs receive validation before range outcomes. | +| [Ieee80211Radio](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc) validates some PHY headers but performs no HT CRC check. | Correct serialization alone cannot establish rejection of an invalid received HT-SIG. P2 needs receive validation before chunk error flags. | | [MpduDeaggregation](../../src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc) creates separate member Packets, but assumes valid lengths and uses the aggregate name. | Shared bounded parsing must replace unchecked range discovery. Feature A4 projects the PHY outcome onto each member Packet. | | [CommunicationCacheBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/CommunicationCacheBase.cc) stores decision pointers per receiver, transmission, and signal part. It deletes replaced decisions and retained decisions at entry destruction. | The cache can own a derived WLAN decision. Separate signal-part slots need explicit sample ownership. | | [RadioMedium](../../src/inet/physicallayer/wireless/common/medium/RadioMedium.cc) caches decision lookups, but directly computes `isReceptionSuccessful()`. Its result path requests a WHOLE decision. | Aggregate queries must share cached samples across both paths and signal parts. Generic cache presence alone does not prevent repeated draws. | | Separate-part reception in [Radio](../../src/inet/physicallayer/wireless/common/radio/packetlevel/Radio.cc) queries PREAMBLE before HEADER. A failed PREAMBLE or HEADER removes the attempted reception. | Preserve the per-part checks without early HEADER or DATA calculation. That aborted attempt supplies no result to MAC. | | [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) attaches error-rate metadata through the old whole-PSDU calculation. [ReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc) supplies power, SNIR, and reception times. | Aggregate result construction needs an explicit probability contract. It must preserve the other applicable receive metadata. | | [Rx](../../src/inet/linklayer/ieee80211/mac/Rx.cc) rejects a Packet bit-error flag before its FCS-mode branches. | A failed member can use its own Packet flag without a false checksum or shared chunk mutation. | -| [Ieee80211Receiver](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc) sets its mode set at local initialization. [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) resolves its error model at that stage. | The receiver can check role presence at the physical-layer stage. That stage does not supply the exact limits for capability advertisement. | -| [Ieee80211Mac](../../src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc) calculates effective channel widths and NSS at link-layer initialization, before its MIB update. | Exact coverage validation belongs at that handoff, with the same mode set and effective limits. | -| [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) permits reception without an error model after its SNIR check. The [VHT/HT fixture](../../tests/module/Ieee80211VhtHtSupport_1.test) uses this path with transmit aggregation disabled. | An absent error model is a supported case. The new interface check must preserve it. | -| [Ieee80211Radio](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned) selects NIST by default for scalar and dimensional reception. | D1 needs the shared implementation before it enables the HT interface check. Otherwise, ordinary default HT configurations fail initialization. | +| [Ieee80211Receiver](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc) sets its mode set at local initialization. [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) resolves its error model at that stage. | The receiver supplies bounded DATA content after header success. No aggregation role check applies. | +| [Ieee80211Mac](../../src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc) calculates effective channel widths and NSS at link-layer initialization, before its MIB update. | The model no longer needs an aggregation role or a separate capability handoff. | +| [FlatReceiverBase](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) permits reception without an error model after its SNIR check. The [VHT/HT fixture](../../tests/module/Ieee80211VhtHtSupport_1.test) uses this path with transmit aggregation disabled. | An absent error model is a supported case. The DATA corruption path must preserve it. | +| [Ieee80211Radio](../../src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned) selects NIST by default for scalar and dimensional reception. | D1 supplies the shared DATA probability calculation without a separate HT interface check. | | [Ieee80211Mib](../../src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc) derives local HT capability from the mode set and effective radio limits. This includes short GI. | Receive support must cover those advertised modes before capability publication. HT length exponent zero still advertises 8191 octets. | The feature plan and its summary describe the proposed MAC implementation. The execution record supplies results for the completed PHY series. @@ -68,7 +77,7 @@ The feature plan and its summary describe the proposed MAC implementation. The e The scope is packet-level `Ieee80211Radio` with scalar or dimensional reception and HT mixed format at 20 MHz. Reception covers long GI and short GI at the advertised MCS and NSS values. The MAC feature's aggregate transmit policy remains limited to long GI. D1 supplies receiver integration, the shared NIST/Yans implementation, and basic compatibility checks. Deterministic probabilities establish its partial-reception evidence. D2 completes the NIST/Yans and compatibility evidence under the documented scalar SNIR abstraction. -The plan owns PHY representation, shared bounded delimiter parsing, the optional aggregate error-model role, cached decisions, and receive indications. The MAC feature plan owns member selection, negotiated spacing, canonical MPDU ownership, release rules, responses, and recovery. +The plan owns PHY representation, cached DATA chunks, generic error-model integration, and header indications. Bounded delimiter parsing belongs to MAC extraction. The MAC feature plan owns member selection, spacing, ownership, release rules, responses, and recovery. The required results are: @@ -80,7 +89,7 @@ The required results are: 6. Repeated decision and result access causes no additional random draw. 7. After successful header reception, whole-signal and separate-part reception produce consistent aggregate outcomes. 8. Compatible ordinary MPDU reception preserves its calculations and random draw sequence. -9. A present HT model without the aggregate role or required receive-mode coverage fails before capability advertisement, independently of local transmit aggregation. +9. Error models calculate DATA chunk probabilities without an aggregation role or MAC delimiter knowledge. 10. NIST and Yans share the WLAN base implementation without duplicate aggregate algorithms. 11. Spacing delimiters receive outcomes, but alignment padding consumes no separate random draw. 12. The indication permits a failed member Packet to retain its error without an incorrect whole envelope. @@ -93,7 +102,7 @@ The required results are: 19. Aggregate error-rate metadata follows the explicit probability contract in Section 4.3. 20. Advertised HT20 short-GI reception has aggregate support even when the local transmit policy uses long GI or selects no aggregate. 21. Invalid HT-SIG CRC or unsupported fields cause HEADER failure before delimiter or member draws. -22. A valid header with insufficient modeled PSDU content causes bounded DATA refusal without range draws. Its HEADER result remains valid. +22. A valid header with insufficient modeled PSDU content causes bounded DATA refusal without DATA draws. Its HEADER result remains valid. This plan excludes correlated FEC errors, bit-level decoding, interference reconstruction within each member, and statistical accuracy or goodput claims. It cannot represent errors confined to alignment padding. It adds no MAC recovery timer, agreement store, or sealed Packet API change. @@ -121,53 +130,37 @@ At that boundary, the receiver checks CRC, supported field combinations, and agr An invalid header supplies no accepted Aggregation property. Separate-part reception uses the current abort; whole-signal reception uses the current failure-container path. MAC rejects that container before any MAC header or trailer peek. These header checks apply Clause 19.3.21 within the packet-level model without a new radio event or PHY primitive. -A separate bounds check compares a valid header's HT Length with the available modeled PSDU before range calculation or length-based decapsulation. Insufficient content denotes malformed model input. The HEADER decision retains success and its decoded Aggregation property. The DATA decision reports failure with no range outcomes or range draws. The radio uses the current failure-container path without length-based trailer removal. MAC rejects that refused content before header or trailer peeks. +A separate bounds check compares a valid header's HT Length with the available modeled PSDU before chunk corruption or length-based decapsulation. Insufficient content denotes malformed model input. The HEADER decision retains success and its decoded Aggregation property. The DATA decision reports failure with no chunk error flags or DATA draws. The radio uses the current failure-container path without length-based trailer removal. MAC rejects that refused content before header or trailer peeks. This structural refusal does not represent physical signal loss. Clause 19.3.21 distinguishes invalid HT-SIG CRC from carrier loss after a valid header. The short-content fixture proves bounds safety only. It establishes no carrier-loss behavior or acceptance of a physically received prefix. Physical carrier-loss behavior remains outside this plan. Bad encoded bytes and probabilistic header loss remain separate test causes. In a hypothetical case, the error model reports header success, but the transmitted HT-SIG has an invalid CRC. The receiver rejects that header before any member draw or Block Ack response. Repeat the case without an error model. A successful probability result cannot override invalid encoded content. -Extend the current aggregate decoder with bounded delimiter inspection that the WLAN receiver and MAC can reuse. Check every length, alignment interval, zero-length delimiter, CRC, and signature against the available PSDU. Each scan must advance. A malformed length cannot produce an accepted range or an out-of-bounds read. +Extend the current aggregate decoder with bounded delimiter traversal that the WLAN receiver and MAC can reuse. Check every length, alignment interval, zero-length delimiter, CRC, and signature against the available PSDU. Each scan must advance. A malformed length cannot produce an accepted range or an out-of-bounds read. The shared parser supplies range facts, not delivery permission. The MAC must apply the received delimiter outcome before it trusts a boundary. A later recovered delimiter does not erase an earlier continuity break. The parser must not use names or a transmitter-private membership list to bypass corruption. -The indication identifies each scored range by kind, byte offset, byte length, and outcome. It also retains common header success. A spacing delimiter has a four-octet delimiter range and no MPDU range. These values define the member-error projection before P2 starts. - -#### Scored ranges +The received packet retains chunk correctness flags. The indication retains header success, aggregation identity, and content completeness. It contains no separate outcome table. -These choices define the packet-level error abstraction. They do not claim that the standard specifies independent error draws. +#### Chunk errors -| PSDU content | Probability and outcome rule | -| --- | --- | -| Each four-octet delimiter. | Score 32 bits once. This includes every zero-length spacing delimiter. Its failure breaks the applicable delimiter-continuity chain. | -| Each nonzero MPDU range. | Score its complete header, payload, and FCS once. Its outcome belongs to that member only. | -| Alignment padding. | Do not score it. It contributes airtime but has no error outcome or separate error draw. | +The error model receives a bounded DATA chunk view. It samples each chunk once from PHY conditions and bit length. This includes padding chunks. Zero and unit probabilities consume no random number. An intermediate probability consumes one draw. Header failure or insufficient content prevents DATA draws. -Evaluate common acquisition and header success first. Header failure prevents all delimiter and member draws. After header success, process the bounded transmitted ranges in wire order: delimiter, member if present, padding, and the next delimiter. Probabilities zero and one give fixed outcomes without an RNG draw. An intermediate probability consumes one draw. +For example, an incorrect payload chunk remains inside its MPDU after extraction. That MPDU reports incorrect content. Other MPDUs can remain correct. MAC delimiter and FCS checks still control acceptance. -A failed delimiter does not stop the physical calculation for known later ranges. MAC delimiter validation and bounded resynchronization still control which members it can accept. The range indication cannot authorize a jump through a failed delimiter. +Typed content can preserve delimiter and member structure. Byte conversion merges the transmission into one byte chunk. Its DATA view receives one outcome. The error model does not reconstruct MAC boundaries. Independent chunk errors do not model correlated FEC failures. The wire fixture covers HT-SIG serialization, validation, delimiters, member limits, alignment, and bounded recovery. Feature A1 owns capability propagation and aggregate construction calculations for spacing, serialization, and member layout. Feature A3 owns dynamic member selection, spacing at the retained DATA mode, batch reduction, and canonical member ownership. -### 4.2 Optional role and shared implementation - -Add a paired pure C++/NED WLAN contract for aggregate error probabilities. Its caller supplies the selected mode, modeled SNIR, and bounded delimiter or member ranges. The role distinguishes supported probabilities, invalid input, and unsupported aggregate reception. A pure mode-support query lets the receiver check required coverage before capability publication. That query performs no probability calculation or random draw. - -An implementation must support every advertised HT20 receive mode, whether it uses long GI or short GI. The MAC supplies its mode set, effective channel widths, and NSS limit to the receiver's coverage operation before the MIB update. These are the same values that the MAC supplies to `updateLocalHtCapabilities()`. Validation follows that operation's current MCS and GI rules. It adds no second capability store or independent filter. Unsupported input denotes modes or reception detail outside this contract. - -Table 9-224 defines short-GI support for reception of HT PPDUs. It supplies no separate short-GI permission for standalone MPDUs and aggregates. A model cannot advertise short-GI reception and refuse an otherwise valid short-GI aggregate. HT40 and VHT aggregate reception remain outside this plan. A later extension of advertised receive modes requires receiver support for those modes before advertisement. - -Keep the generic `IErrorModel` API free of WLAN dependencies. The receiver calls aggregate probability operations only for aggregate reception. When an error model is present, HT operation requires the role because advertised HT capability includes aggregate reception. Non-HT custom models retain their ordinary MPDU path without the new role. Custom HT models need the role even when local transmit aggregation remains disabled. - -This plan adds no aggregate role to `Ieee80211BerTableErrorModel`. Its current HT refusal precedes the new role check. The plan claims no loss of supported HT reception through BerTable. P0 records that boundary and a non-HT parent control. P2 compares the control and verifies the expected HT refusal. Tests for the new role diagnostic use the custom model, whose local initialization permits HT operation. +### 4.2 Generic DATA corruption -Place the reusable probability calculation in `Ieee80211ErrorModelBase`. That implementation calls the current protected `getDataSuccessRate()` for each range length at the selected scalar SNIR. The pure contract contains no default method body. NIST, Yans, and compatible custom subclasses inherit the shared implementation. P2 includes this implementation before it enables the HT compatibility check. P3 extends its evidence rather than its interface availability. +Add `IErrorModel::computeReceivedData(snir, data)`. The input and output are shared chunk content. The operation preserves length and bytes. It marks failed chunks in the result without a change to the transmitted chunks. It permits an entirely correct result and performs no retry. -An absent error model retains the current error-free abstraction after PHY acquisition, possibility, attempt, and SNIR checks. After those checks succeed, the receiver records successful physical outcomes for bounded delimiter and member ranges without error-model calls or random draws. Delimiter CRC, bounds, and member FCS checks still control MAC acceptance. A failed common PHY check still prevents valid aggregate content. This case adds no substitute error-model module or support flag. +`ErrorModelBase` supplies the common traversal and sampling. Its protected `computeChunkSuccessRate(snir, bitLength)` uses DATA BER. An invalid or unavailable BER causes a runtime error. Direct interface implementations must supply the new operation. -The deterministic test model implements the same role with probabilities of zero or one. Thus, tests control error conditions while the production receiver still creates the reception outcomes. Models outside the WLAN base can supply their own implementation. +`Ieee80211ErrorModelBase` overrides the probability calculation through `getDataSuccessRate()`. It obtains mode and SNIR from the PHY reception. NIST and Yans inherit this calculation. The models receive no delimiter, MPDU, or aggregation information. No aggregation role or MAC capability check applies. -The new role has concrete substitutes: the deterministic model and the shared WLAN implementation for NIST/Yans. A new callable boundary is necessary because the public generic API cannot accept an arbitrary member length. No synthetic reception object or temporary PHY length change supplies that absent argument. +An absent error model preserves the original DATA chunks after the common PHY checks. Ordinary reception keeps its existing whole-packet path. BerTable retains its current HT configuration limit. ### 4.3 Decision ownership and random draws @@ -177,13 +170,13 @@ P0 must confirm these paths against the merged implementation base. Record cache For aggregates, retain a PREAMBLE decision in its current cache slot. Its calculation preserves current per-part acquisition, possibility, attempt, and SNIR checks. It does not request HEADER or DATA. Shared WLAN NIST/Yans calculations add no PREAMBLE error draw. Direct PREAMBLE queries reuse that decision. -HEADER first checks the cached PREAMBLE result before its own calculation. It retains common header success once the header decision boundary permits that calculation. An immutable WLAN decision in DATA retains the bounded delimiter and member outcomes, or structural refusal without ranges. A DATA lookup first obtains the cached HEADER result. A failed prerequisite or structural refusal prevents range draws. A WHOLE lookup derives its summary from these entries at the valid result boundary. +HEADER first checks cached PREAMBLE success. DATA obtains cached HEADER success before its own calculation. The immutable DATA decision retains received chunks or structural refusal. WHOLE and result queries reuse the same chunks without additional samples. | Query | Required calculation and retained result | | --- | --- | | PREAMBLE | Apply current per-part checks. Retain its decision without HEADER or DATA calculation. | | HEADER | Reuse PREAMBLE. Retain one header outcome at the header boundary. | -| DATA | Reuse HEADER. Refuse insufficient modeled content before range draws. Otherwise, retain each bounded delimiter and member outcome once after successful header reception. | +| DATA | Reuse HEADER. Refuse insufficient content before DATA draws. Otherwise, sample the bounded chunk content once and retain its error flags. | | WHOLE | Reuse prerequisite decisions. Derive the summary without another error draw. | The WHOLE slot owns a separate summary decision. It does not own the DATA pointer. Aggregate result construction reads the DATA outcomes and copies them into the WLAN indication. This design uses the current cache and adds no second outcome store. @@ -196,11 +189,11 @@ The WHOLE summary does not decide whether the MAC invokes EIFS. Feature A4 appli The radio converts retained facts into a typed WLAN indication with copied values. It retains no pointer to a reception or transmission whose lifetime can end first. Feature A4 maps a failed complete member to that member's `Packet::setBitError(true)` before the normal Rx FCS predicate. -In separate-part reception, a failed PREAMBLE or HEADER makes `Radio::continueReception()` abort the attempted reception. That attempt supplies no result or failure container to MAC. Existing radio state notifications let HCF invalidate its physical history. If whole-signal reception supplies a container after header failure, MAC rejects it before format-specific header or trailer peeks. Its indication reports failure without a decoded aggregation property. Both paths prevent aggregate acceptance, responses, range draws, and stale history without a new notification event. +In separate-part reception, a failed PREAMBLE or HEADER makes `Radio::continueReception()` abort the attempted reception. That attempt supplies no result or failure container to MAC. Existing radio state notifications let HCF invalidate its physical history. If whole-signal reception supplies a container after header failure, MAC rejects it before format-specific header or trailer peeks. Its indication reports failure without a decoded aggregation property. Both paths prevent aggregate acceptance, responses, DATA draws, and stale history without a new notification event. #### Aggregate error-rate metadata -The current [result path](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) obtains `ErrorRateInd` through a whole-PSDU probability calculation. That estimate includes content outside the new scored ranges. It does not describe the selected range model. With a present error model, the aggregate result retains `ErrorRateInd` with `packetErrorRate`, `bitErrorRate`, and `symbolErrorRate` set to `NaN`. The typed WLAN indication separately supplies the retained outcomes. This first profile exposes no aggregate probability estimate. +The ordinary [result path](../../src/inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.cc) calculates whole-PSDU error rates. The aggregate path retains `ErrorRateInd` with PER, BER, and SER set to `NaN`. Chunk flags retain sampled outcomes separately from those unknown probability estimates. The [tag contract](../../src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg) permits `NaN` for unknown probabilities. P0 must trace aggregate results through direct readers, [result filters](../../src/inet/common/ResultFilters.cc), and the [configured radio recorders](../../src/inet/physicallayer/wireless/common/radio/packetlevel/Radio.ned). The [network configurator](../../src/inet/networklayer/configurator/base/L3NetworkConfiguratorBase.cc) requires the tag for an ordinary probe; this read alone establishes no aggregate path. P0 must verify behavior with unknown values and absent tags for reachable consumers. A reachable incompatibility blocks P2's metadata implementation until the plan records its resolution. Tag omission requires evidence that all affected consumers accept absence. @@ -210,34 +203,34 @@ In a hypothetical partial reception with a present model, MPDU 101 fails beside ### 4.4 Model detail and diagnostics -The first abstraction has common PHY acquisition and header success, followed by independent delimiter and MPDU outcomes. Each range uses the current scalar SNIR reduction. The calculation adds no separate PHY header to every member. +The abstraction uses common PHY acquisition and header success, followed by independent chunk outcomes under scalar SNIR. It adds no separate PHY header per member. Chunk flags describe physical errors without a MAC acceptance decision. This abstraction does not model correlated FEC errors or variation of interference within a member. It cannot represent errors confined to alignment padding. Padding still contributes to PSDU length and PPDU airtime. More detailed error models remain separate work. NIST/Yans integration reuses current calculations; it does not establish new PHY accuracy. -The WLAN receiver owns the HT compatibility operations. At `INITSTAGE_PHYSICAL_LAYER`, it rejects a present HT model if the aggregate role is absent. This stage follows local error-model initialization. Exact coverage validation occurs later, before the MAC updates local HT capability at `INITSTAGE_LINK_LAYER`. Both checks precede management capability advertisements and apply independently of the local transmit policy. Exponent zero cannot disable advertised A-MPDU reception because it represents 8191 octets under Table 9-225. +The WLAN receiver checks HT-SIG and available PSDU length before DATA corruption. It does not require an aggregation-specific error-model role. Models use their existing PHY mode calculations. Exponent zero still represents 8191 octets under Table 9-225. The MAC calculates effective channel widths from transmitter and receiver support. It calculates the NSS limit from antenna count and its mode set. Before `updateLocalHtCapabilities()`, the MAC calls the receiver's pure coverage operation with those exact values and the same mode set. The receiver checks the HT20 MCS and GI combinations that those inputs advertise. Failure prevents the MIB update and capability publication. No probability call or random draw occurs. In a hypothetical configuration, one antenna limits advertisement to one spatial stream. The mode catalog also contains modes for more spatial streams. A model with complete support for the advertised modes must pass validation. Unsupported modes outside those effective limits must not cause rejection. This case prevents an incorrect coverage requirement for the full catalog. -After initialization, `setModeSet()` checks role presence before it accepts an HT set. That setter alone cannot establish exact advertised-mode coverage. The current capability-update caller exists only in MAC initialization. Any later capability-update path must validate its exact inputs before the MIB changes local HT capability. This plan adds no runtime capability-update mechanism or second capability store. +`setModeSet()` retains its existing PHY mode assignment. It does not require an aggregation role or a second receive-capability store. The check must not clear short-GI capability to obtain model compatibility. A present model must not substitute uniform success or one whole-PSDU result for the required aggregate outcomes. An absent model follows the explicit error-free case in Section 4.2. No new capability bit or mutable support flag represents this local compatibility condition. -In a hypothetical configuration, a custom model supports ordinary HT MPDUs but lacks the aggregate role. The default transmit policy still selects no A-MPDU. Initialization must fail before any capability advertisement or association, rather than at the first received aggregate. The custom model needs an aggregate implementation before this HT configuration can run. The same model can retain its current non-HT MPDU operation without that role. +In a hypothetical configuration, a custom model supplies DATA BER through the common base. HT initialization succeeds without an aggregation role. The first valid DATA request samples its chunk structure through BER. An unavailable BER causes an explicit runtime error. -In another hypothetical case, a custom model supplies the role but supports only long GI. The effective limits permit advertised HT20 short-GI reception. The mode-support query must cause initialization failure before the MIB update or advertisement. With complete support, a peer's short-GI aggregate must produce a valid PHY indication. Feature A5 separately verifies MAC receipt and the response. The local transmitter can retain long GI throughout this case. +A custom model can supply DATA BER through the common base without any aggregation interface. A model with unavailable BER must override the chunk probability calculation or the received-DATA operation. An invalid probability causes a runtime error before a draw. ### 4.5 Reuse and justified additions | Addition | Responsible component and consumer | Reuse decision | | --- | --- | --- | | HT-SIG fields, serializer, validator, and typed indication. | WLAN radio and receiver validate header fields before MAC/Rx receives aggregate content. | Current PHY state cannot distinguish singleton aggregates. Its empty HT serializer cannot preserve the new property through bytes. Current radio validation omits HT CRC. | -| Optional aggregate error-model role. | WLAN receiver checks receive-mode support before it calls deterministic or shared WLAN probability implementations. | Generic `IErrorModel` has no range-length argument. Interface presence alone cannot establish coverage of advertised receive modes. | +| Generic DATA corruption. | The common PHY interface accepts chunk content. The WLAN base supplies its existing DATA probability calculation. | The operation needs no delimiter, MPDU, or aggregation type. | | Shared probability implementation. | `Ieee80211ErrorModelBase` serves NIST, Yans, and compatible custom subclasses. | Current protected per-length calculations already supply the required arithmetic. | | Immutable aggregate decision. | Current medium cache owns outcomes for receiver and radio consumers. | Recomputed random decisions would change repeated observations. No second cache is necessary. | | Aggregate error-rate metadata. | WLAN receiver preserves applicable receive tags and reports unspecified probability fields through `NaN`. | The old whole-PSDU estimate describes a different calculation. This policy needs no new probability store. | -| Bounded delimiter inspection. | Current decoder supplies common range rules to PHY and MAC. | Unchecked lengths cannot establish valid ranges or delimiter continuity. | +| Bounded delimiter traversal. | MAC extraction uses the existing traversal. PHY error models use only chunk structure and bit lengths. | | Early HT compatibility checks. | WLAN receiver checks role presence. MAC supplies exact advertised-mode inputs for receiver coverage validation before the MIB update. | PHY initialization lacks the effective advertisement limits. A runtime aggregate diagnostic occurs after capability advertisement. Exponent zero cannot represent absent aggregate reception. | | Explicit case without an error model. | WLAN receiver preserves current PHY checks and supplies bounded physical outcomes. | The current error-free abstraction needs no new role, module, or random draw. | @@ -249,15 +242,15 @@ Changes affect MSG inputs for the HT PHY header and WLAN receive tag. They also HT-SIG serialization completes byte conversion for ordinary HT MPDUs as well as aggregates within the current 48-bit header length. P0 records that declared length and the current mode/PSDU duration path. P1 verifies 48-bit serialization and decode; ordinary HT controls verify unchanged airtime under identical mode and PSDU inputs. Record byte changes separately from reception calculations, frame sequences, and random draws. Any fingerprint change needs its own causal explanation. Any affected recorded expectation follows the current baseline rules and authorization scope. -D1 changes no default transmit aggregation policy. It includes the shared NIST/Yans implementation before the early HT check becomes active. D1 must retain default HT initialization and compatible ordinary MPDU reception. Its deterministic test model establishes partial-reception integration. Those deterministic results alone do not establish the full NIST/Yans profile; D2 completes that evidence. +D1 changes no default transmit aggregation policy. It supplies NIST/Yans chunk probabilities through their shared PHY base. Ordinary HT initialization and reception remain valid. Deterministic fixtures establish chunk propagation. D2 adds seeded production-model evidence. The early diagnostic changes compatibility for present custom HT models without the role or required receive-mode coverage. This includes configurations that previously used only ordinary MPDUs. Those configurations require model migration. Compatible ordinary MPDU calculations retain their current behavior and draws. Non-HT custom configurations need no aggregate implementation. Invalid encoded HT-SIG content now causes reception failure; valid ordinary HT content retains its current calculations. -BerTable receives no HT support in this delivery. Its current initializer rejects `opMode="n"` and the HT operation strings that the radio supplies before the new role check. The migration guide lists this current limit and directs supported HT configurations to NIST or Yans. It must distinguish the current initializer error from the new diagnostic for an otherwise valid custom HT model. Non-HT BerTable controls compare initialization and reception with the pinned parent; they add no aggregate-role requirement. A failure in the parent remains a prior limitation, with no inferred compatibility pass. +BerTable retains its current HT operation-mode refusal. The fixture uses a valid table to separate that refusal from a file error. Its non-HT control remains a compatibility check. No aggregation role or separate capability check applies. An absent error model remains valid for ordinary HT operation, including current `Ieee80211UnitDiskRadio` configurations. The receiver preserves their current PHY checks without error draws. The aggregate profile remains scalar or dimensional reception under the declared HT conditions. This compatibility requirement adds no aggregate support claim for unit-disk reception. -The migration guide must identify the optional role, mode-support query, shared base, typed indication, range semantics, and early HT diagnostics. It must explain the MAC-to-receiver coverage handoff before the MIB update. It must distinguish absent, compatible, and incompatible models, such as a present role with incomplete receive-mode coverage. It must separate HT20 short-GI reception from the long-GI transmit policy. The current [seal registry](../../doc/project/audit/seal-list.md) protects `src/inet/common/packet/`. This plan uses its public API. +The migration guide must explain generic DATA corruption, chunk error flags, custom-model requirements, and cache ownership. It must state the loss of separate error regions after byte conversion. It must distinguish an absent model from an unavailable BER. The [seal registry](../../doc/project/audit/seal-list.md) protects `src/inet/common/packet/`. This plan uses its public API. The guide must explain HT-SIG byte conversion, header validation, per-part failure delivery, and the aggregate probability fields. It must distinguish structural PSDU refusal from header failure and physical carrier loss. @@ -271,8 +264,8 @@ Each step forms a buildable commit or short series with its required consumers a | --- | --- | --- | --- | | P0: Establish the merged base and cache contract. | Pin the integrated master base. Trace all signal parts, direct success queries, result construction, failure delivery, and cache lifetime. Audit header length, the duration path, BerTable controls, and aggregate metadata consumers. | Record keys, insertion, replacement, destruction, result consumption, and shared-sample paths. Record the current 48-bit header length, duration inputs, and BerTable parent controls. Close the audit of aggregate metadata consumers before P2 implements its contract. Confirm error-model APIs and the feature-off baseline. | The merge prerequisite is satisfied on master. | | P1: Define HT-SIG representation. | Extend typed HT-SIG state and its serializer together. Define the shared stateless validator. Derive mode fields and aggregate identity during radio encapsulation. | Golden bytes and independent decode preserve HT Length and Aggregation within the current 48-bit header length. Ordinary HT controls preserve airtime for identical mode and PSDU inputs. Wire checks distinguish invalid headers from insufficient modeled PSDU content. | P0 and VHT-PRE. Independent RANGE and RANGE-USE commits supply bounded parsing. | -| P2: Deliver receiver integration and basic compatibility. | Add the optional role, mode-support query, and shared WLAN implementation for NIST/Yans. Preserve reception without an error model. Include the role check and MAC coverage handoff with their required implementations. Apply HT-SIG validation at HEADER and structural content bounds at DATA. Add the deterministic model, per-part decisions, WHOLE summary, cache-backed queries, copied indication, and defined metadata. | Actual reception proves partial loss, delimiter failure, per-part failure delivery, query consistency, cache lifetime, and metadata without duplicate draws or probability calls. Invalid headers and structural refusal prevent range draws through distinct decisions. Advertised HT20 short-GI aggregates produce valid PHY indications. Default NIST, configured Yans, absent-model operation, and non-HT BerTable controls match their documented parent behavior. Incompatible present models fail before advertisement; BerTable's current HT refusal remains distinct. This closes D1 without MAC receipt or response acceptance. | P1. Close the metadata audit before its dependent implementation. Cache reuse, scored ranges, and member-error projection must be defined first. | -| P3: Verify NIST/Yans aggregate outcomes. | Exercise the shared implementation from P2 through the real receiver. Compare range probabilities with the current per-length calculations. | Deterministic reference checks and fixed-seed runs prove cache use, repeatability, and supported model selection. No additional interface implementation is necessary to restore default HT initialization. | D1. Feature A3–A6 need not wait. | +| P2: `ieee80211: add+change: retain partial HT PHY errors in chunks` | The receiver caches received DATA chunks. Rx rejects aggregate containers before standalone checks. The parent lacks partial chunk propagation and the header indication. | Include `WifiHtAmpduPartialReception.test` and its guide and comparison record. Thirty-two seed-zero runs cover typed partial errors, raw byte errors, both GIs, and both reception-part modes. An accepted CTS must still set NAV after the drop. | +| P3: Verify NIST/Yans aggregate outcomes. | Exercise the shared implementation from P2 through the real receiver. Compare chunk probabilities with the current per-length calculations. | Deterministic reference checks and fixed-seed runs prove cache use, repeatability, and supported model selection. No additional interface implementation is necessary to restore default HT initialization. | D1. Feature A3–A6 need not wait. | | P4: Close PHY compatibility evidence. | Complete migration text, affected receiver controls, and the execution record. Compare feature-off calculations and draws with the pinned parent. | NIST/Yans and unsupported-model cases match the documented boundary. Scoped checks close D2. Feature A7 requires D2. | P3. | ### Commit boundaries @@ -283,24 +276,24 @@ The table assigns each commit its own purpose and verification. Each entry assum | --- | --- | --- | | PLANS: `plan: add: define HT A-MPDU reception stages` | The three plan documents define PHY and MAC owners, limits, and artifact assignments. | This is the first commit. It changes no runtime behavior, so no protocol fixture or reviewer document applies. Check links and commit assignments. | | VHT-PRE: `ieee80211: fix: create VHT headers for VHT signal modes` | The VHT mode factory must create the existing VHT header rather than an HT header. Change `Ieee80211VhtMode.h` and its release note. | After PLANS, two production-radio runs must observe a VHT header for long and short GI. Include `WifiVhtHeaderIdentity.test` and `vht-header-identity.md` from the protocol table below. | -| RANGE: `ieee80211: add: inspect bounded MPDU aggregate ranges` | `MpduAggregationUtils` supplies a pure bounded layout query. Add the helper and `tests/unit/Ieee80211MpduLayout_1.test`. | After PLANS, direct cases verify bounds, alignment, truncation, and typed/raw recovery. Existing callers remain unchanged. This pure query adds no protocol transition, so unit checks apply without a protocol reviewer document. | -| RANGE-USE: `ieee80211: fix: recover bounded MPDUs during deaggregation` | `MpduDeaggregation` replaces unchecked member extraction with RANGE results. Add the direct protocol fixture and reviewer document. | After RANGE, complete and malformed aggregates must return only complete bounded members in typed and byte representations. Include `WifiMpduDeaggregation.test` and `mpdu-deaggregation.md`. Extraction grants no MAC acceptance. | +| RANGE: `ieee80211: add: visit bounded A-MPDU subframes` | `MpduDeaggregation::visitSubframes` supplies bounded chunk traversal in master through the deaggregation prerequisite PR. The helper passes chunks directly to callbacks. It stores no intermediate layout. Retain `tests/unit/Ieee80211MpduDeaggregation_1.test`. | The prerequisite checks bounds, alignment, and truncation. Recovery adds typed/raw recovery cases. Existing callers remain unchanged. This pure query adds no protocol transition, so unit checks apply without a protocol reviewer document. | +| RANGE-USE: `ieee80211: fix: recover bounded MPDUs during deaggregation` | The shared traversal adds recovery after invalid delimiters. `MpduDeaggregation` receives recovered MPDU chunks through its existing callback. Add the direct protocol fixture and reviewer document. | After RANGE, complete and malformed aggregates must return only complete bounded members in typed and byte representations. Include `WifiMpduDeaggregation.test` and `mpdu-deaggregation.md`. Extraction grants no MAC acceptance. | | P1: `ieee80211: add: preserve HT-SIG fields through byte conversion` | The HT MSG header, field helpers, serializer, and radio preserve HT-SIG. Retain the wire unit fixture and production wire protocol fixture. | After VHT-PRE, independent octets verify field positions, CRC, and singleton identity. RANGE is not a prerequisite for serialization. Include `WifiHtAmpduWire.test` and `ampdu-ht-sig-wire.md`. | -| PROB: `ieee80211: add: expose pure HT A-MPDU range probabilities` | The optional role and WLAN error-model base supply pure support queries and probabilities. NIST/Yans NED types declare the role. | After PLANS, the direct probability matrix checks the declared range contract without random draws. No receiver uses the role yet. Pure calculations add no protocol transition, so direct unit checks apply without a protocol reviewer document. | -| P2: `ieee80211: add+change: retain partial HT A-MPDU PHY outcomes` | The receiver, decision values, indication, MAC validation, and Rx integrate cached partial reception. | After RANGE, RANGE-USE, P1, and PROB, production reception must retain a failed middle member beside successful neighbors. Include the module fixtures and `WifiHtAmpduPartialReception.test` with `ampdu-partial-reception.md`. | +| PROB: `physicallayer: add: sample DATA errors in existing chunks` | The common error-model interface and base supply unconditional DATA corruption. The direct fixture checks local flags, unchanged bytes, invalid probabilities, and an all-correct result. | After PLANS, `DataChunkCorruption_1.test` checks the common operation without a MAC format. The receiver does not use the operation yet. | +| P2: `ieee80211: add+change: retain partial HT PHY errors in chunks` | The receiver caches received DATA chunks. Rx rejects aggregate containers before standalone checks. The parent lacks partial chunk propagation and the header indication. | Include `WifiHtAmpduPartialReception.test` and its guide and comparison record. Thirty-two seed-zero runs cover typed partial errors, raw byte errors, both GIs, and both reception-part modes. An accepted CTS must still set NAV after the drop. | | P3: `tests: add: verify seeded NIST and Yans aggregate reception` | The receiver fixture consolidates direct probability checks and adds seeded real-model observations. | After P2, independent generators verify probabilities, samples, draw counts, and cache reuse. This test-only change adds no protocol behavior, so no separate protocol reviewer document applies. | | P4: `tests: add: verify ordinary PHY compatibility with the parent` | The ordinary compatibility fixture records unchanged reception properties. Migration text states the model abstraction. | After P3, run the 576 ordinary controls. A fresh parent comparison requires separate execution. This test-only change adds no protocol behavior, so no separate protocol reviewer document applies. | | RECORD: `plan: change: record HT A-MPDU PHY completion on master` | The final plan record states scope, exact verification, and limits. | After P4, verify the final source tree and run the explicit final selectors. Documentation changes add no protocol behavior, so no separate protocol fixture or reviewer document applies. | -VHT-PRE, RANGE, and PROB can stand as separate prerequisites. RANGE-USE introduces the inspector's first production caller. P1 preserves wire fields independently of delimiter inspection. P2 combines their completed contracts before it supplies copied outcomes to Rx. +VHT-PRE, RANGE, and PROB can stand as separate prerequisites. The deaggregation prerequisite introduces the first production caller. RANGE-USE adds delimiter recovery to the same loop. P1 preserves wire fields independently of delimiter traversal. P2 combines their completed contracts before it supplies chunk error flags to Rx. ```mermaid flowchart TD M["Integrated master
P0: Pin the base"] --> V["VHT-PRE: VHT header identity"] V --> P1["P1: HT-SIG representation"] - M --> R["RANGE: Bounded inspector"] + M --> R["RANGE: Bounded chunk traversal"] R --> U["RANGE-USE: Deaggregation"] - M --> B["PROB: Pure range probabilities"] + M --> B["PROB: Pure chunk probabilities"] P1 --> P2 U --> P2 B --> P2 @@ -323,22 +316,22 @@ VHT-PRE, RANGE-USE, P1, and P2 each include their protocol fixture and reviewer | Commit ID and proposed subject | Purpose, owner, and parent comparison | Exact protocol artifacts and verification | | --- | --- | --- | | VHT-PRE: `ieee80211: fix: create VHT headers for VHT signal modes` | The VHT factory creates an HT object on the parent. Later HT validation would then receive a VHT mode without an HT MCS. Existing module controls check integrated VHT delivery but do not directly assert the header class. | Add `tests/protocol/wifi/common/WifiVhtHeaderIdentity.test` and `tests/protocol/wifi/vht-header-identity.md` in VHT-PRE. At 1 ms, the driver sends an ordinary PSDU through a production scalar radio. Two seed-zero runs select VHT20 MCS 0. Long GI uses one stream; short GI uses two streams from the existing catalog. Observe `transmissionStarted`. Require VHT class, exclude HT class, and compare the selected header length before the tester event. The parent fails the class check. Select `WifiVhtHeaderIdentity\.test$`. Typed transmission only; no serializer or delivery claim applies. | -| RANGE-USE: `ieee80211: fix: recover bounded MPDUs during deaggregation` | The parent has RANGE but retains unchecked extraction. An overlong delimiter can cause an out-of-bounds extraction instead of recovery. No existing protocol fixture directly tests this production extraction boundary. | Add `tests/protocol/wifi/common/WifiMpduDeaggregation.test` and `tests/protocol/wifi/mpdu-deaggregation.md`. Four seed-zero runs cover complete and overlong layouts as typed and byte representations. The driver calls production deaggregation, verifies exact returned members, and deletes all outputs. A valid first member precedes a malformed 128-octet segment. A later 20-octet member begins at offset 212. The parent cannot complete the required recovery. Select `WifiMpduDeaggregation\.test$`. This tests extraction, not radio reception or MAC acceptance. | +| RANGE-USE: `ieee80211: fix: recover bounded MPDUs during deaggregation` | The prerequisite parent stops at an invalid delimiter. It returns earlier members but loses later complete members. The prerequisite fixture checks extraction but expects a stop at an invalid delimiter. | Extend `tests/protocol/wifi/common/WifiMpduDeaggregation.test` and `tests/protocol/wifi/mpdu-deaggregation.md` for recovery. Four seed-zero runs cover complete and overlong layouts as typed and byte representations. The driver calls production deaggregation, verifies exact returned members, and deletes all outputs. A valid first member precedes a malformed 128-octet segment. A later 20-octet member begins at offset 212. The parent stops before the final complete member and cannot complete the required recovery. Select `WifiMpduDeaggregation\.test$`. This tests extraction, not radio reception or MAC acceptance. | | P1: `ieee80211: add: preserve HT-SIG fields through byte conversion` | The radio and serializer must preserve singleton aggregation through byte conversion. The parent declares a 48-bit HT header but supplies no HT-SIG octets. After P1, a transmitted singleton must match the independent six-octet vector. This entry depends on P0 and VHT-PRE. | Add `tests/protocol/wifi/common/WifiHtAmpduWire.test` and `tests/protocol/wifi/ampdu-ht-sig-wire.md` in P1. Two seed-zero runs submit a 104-octet singleton PSDU through the production radio. Typed and raw bytes must preserve MCS 0, 20 MHz, long GI, Aggregation, Length, and CRC. Select `WifiHtAmpduWire\.test$` with the debug protocol runner. A missing transmission or different byte vector fails. No existing protocol case establishes this path. | -| P2: `ieee80211: add+change: retain partial HT A-MPDU PHY outcomes` | The receiver must retain separate outcomes, and Rx must reject containers before standalone checks. This entry assumes P1 wire representation and the preceding probability-role commit. The parent has no copied per-range indication. After P2, the middle failed MPDU must retain correct neighbors at the Rx drop. | Add `tests/protocol/wifi/common/WifiHtAmpduPartialReception.test` and `tests/protocol/wifi/ampdu-partial-reception.md` in P2. Eight seed-zero runs cover both GIs, typed and raw bytes, and whole and separate reception parts. Production radios receive three members. Only the 101-octet middle member fails. The protocol tester requires the verified drop followed by an accepted CTS NAV update. Select `WifiHtAmpduPartialReception\.test$` with the debug protocol runner. No existing protocol case establishes these copied outcomes. | +| P2: `ieee80211: add+change: retain partial HT PHY errors in chunks` | The receiver caches received DATA chunks. Rx rejects aggregate containers before standalone checks. The parent lacks partial chunk propagation and the header indication. | Include `WifiHtAmpduPartialReception.test` and its guide and comparison record. Thirty-two seed-zero runs cover typed partial errors, raw byte errors, both GIs, and both reception-part modes. An accepted CTS must still set NAV after the drop. | These fixtures add protocol observations to the unit and module coverage. VHT-PRE checks transmitted header identity. RANGE-USE checks production extraction. P1 checks HT-SIG bytes. P2 checks physical reception and the current MAC rejection boundary. None establishes member delivery or implicit Block Ack. Build the current debug library before the explicit protocol selectors. The execution record states observed results separately from these expected results. -The PHY plan owns its unit, module, and protocol fixtures. RANGE supplies `Ieee80211MpduLayout_1.test`. P1 supplies wire checks. P2 supplies receiver and MAC rejection checks. P4 supplies ordinary controls whose source can compile on the parent without new PHY interfaces. The feature plan owns seven proposed fixtures; its MAC features remain separate. +The PHY plan owns its unit, module, and protocol fixtures. RANGE supplies `Ieee80211MpduDeaggregation_1.test`. P1 supplies wire checks. P2 supplies receiver and MAC rejection checks. P4 supplies ordinary controls whose source can compile on the parent without new PHY interfaces. The feature plan owns seven proposed fixtures; its MAC features remain separate. | Proposed fixture | Boundary and required observations | | --- | --- | -| `Ieee80211MpduLayout_1.test`, unit. | Direct layout checks cover delimiter lengths, alignment, truncation, final padding, typed error flags, and bounded typed/byte recovery. This fixture calls the pure inspector before production deaggregation uses it. | +| `Ieee80211MpduDeaggregation_1.test`, unit. | Direct callback checks cover delimiter lengths, alignment, truncation, final padding, typed error flags, and bounded typed/byte recovery. This fixture checks shared chunk traversal without a production caller. | | `Ieee80211AmpduMacBoundary_1.test`, module. | Production radio reception reaches default Rx rejection before standalone FCS and MAC header checks. Sixteen cases cover DCF/HCF, typed/raw bytes, scalar/dimensional radios, and both reception-part modes. | | `Ieee80211AmpduWire_1.test`, unit/serializer. | HT-SIG golden bytes and independent decode preserve length, Aggregation, CRC, and the current 48-bit header length. Cover standalone, singleton, and multiple-member cases. Validator cases reject invalid CRC and unsupported field combinations. The field-limit case refuses an HT Length above 65535 octets. The receiver fixture separately checks insufficient PSDU content. | -| `Ieee80211AmpduPhyReception_1.test`, module. | Actual reception proves all-good, first/middle/last member loss, regular/spacing delimiter failure, all-member failure, per-part header failure, and query consistency. It distinguishes HT-SIG rejection from structural DATA refusal before range draws. It also verifies copied indication lifetime, metadata with reachable consumers, and HT20 short-GI indications without MAC response acceptance. D1 checks NIST/Yans initialization, ordinary reception, absent-model operation, BerTable controls, and exact coverage at the MAC handoff. Absent roles and incomplete advertised coverage fail; unsupported unadvertised modes do not cause failure. D2 extends NIST/Yans aggregate evidence. | +| `Ieee80211AmpduPhyReception_1.test`, module. | Actual reception proves all-good, first/middle/last member loss, regular/spacing delimiter failure, all-member failure, per-part header failure, and query consistency. It distinguishes HT-SIG rejection from structural DATA refusal before DATA draws. It also verifies copied indication lifetime, metadata with reachable consumers, and HT20 short-GI indications without MAC response acceptance. D1 checks NIST/Yans initialization, ordinary reception, absent-model operation, BerTable controls, and exact coverage at the MAC handoff. Absent roles and incomplete advertised coverage fail; unsupported unadvertised modes do not cause failure. D2 extends NIST/Yans aggregate evidence. | | `Ieee80211AmpduPhyCompatibility_1.test`, module. | The same source runs on the pinned parent and feature branch. NIST, Yans, absent-model, and non-HT custom controls expose exact outcomes, duration, metadata, draw counts, and subsequent random values. Three noise levels and seeds 0, 1, and 2 cover scalar and dimensional reception with whole and separate signal parts. | Use deterministic probabilities and seed 0 for the causal cases. Cover whole-signal and separate-part reception with scalar and dimensional models. Verify indication values after the producer's source objects leave their normal lifetime. @@ -347,7 +340,7 @@ Add a fixed probability between zero and one for cache checks. Record random dra Exercise PREAMBLE, HEADER, DATA, and WHOLE lookups, direct success queries, result creation, and repeated access in each supported reception configuration. At the production PREAMBLE boundary, verify no HEADER or DATA calculation or draw. Repeat that query without a new draw. Query WHOLE before DATA in a separate case at the valid result boundary. Both later lookup orders must use the same retained outcomes and add no duplicate draws. Verify result consumption and cache cleanup against P0's recorded ownership. -Force PREAMBLE and HEADER failure separately through the production radio in separate-part mode. Verify that the failed attempt supplies no container to MAC. Verify no later range draws, response, accepted member, or retained physical history. In whole-signal mode, verify rejection of any delivered failure container before aggregate or ordinary MAC header parsing. These checks preserve the existing event boundaries and require no synthetic failure delivery. +Force PREAMBLE and HEADER failure separately through the production radio in separate-part mode. Verify that the failed attempt supplies no container to MAC. Verify no later DATA draws, response, accepted member, or retained physical history. In whole-signal mode, verify rejection of any delivered failure container before aggregate or ordinary MAC header parsing. These checks preserve the existing event boundaries and require no synthetic failure delivery. With a present model, check all three `ErrorRateInd` fields for `NaN` after aggregate result construction and repeated reads. Count the old WHOLE probability calls; aggregate metadata must add none. Check power, SNIR, reception times, transmission identity, mode, and channel at their existing boundaries. Without a model, compare tag presence and exact values with the current common-base behavior. Ordinary MPDU controls must retain their existing metadata. Member results must never present an aggregate probability as a member estimate. @@ -359,23 +352,23 @@ The wire fixture must decode fresh HT-SIG bytes without access to the original t Corrupt an encoded HT-SIG bit without repair of its CRC. Separately encode an unsupported field combination with a valid CRC. Run both cases through the production receiver with successful header probabilities and without an error model. Verify HEADER failure, no accepted Aggregation property, and no delimiter or member draws. Preserve the current separate-part abort and whole-signal failure-container paths. -Separately supply a valid HT-SIG whose HT Length exceeds the available modeled PSDU. Verify HEADER success and preservation of the decoded Aggregation property. Verify DATA refusal with no range outcomes or draws. Verify bounded decapsulation and MAC refusal before header or trailer peeks in both reception modes. Repeat this case without an error model. This malformed-input check establishes no physical carrier-loss result. +Separately supply a valid HT-SIG whose HT Length exceeds the available modeled PSDU. Verify HEADER success and preservation of the decoded Aggregation property. Verify DATA refusal with no chunk error flags or draws. Verify bounded decapsulation and MAC refusal before header or trailer peeks in both reception modes. Repeat this case without an error model. This malformed-input check establishes no physical carrier-loss result. -Vary alignment padding without a separate outcome or draw. Add zero-length spacing delimiters and verify their extra scored ranges. A failed spacing delimiter must preserve the continuity break after later recovery. The feature's A4 fixture separately proves rejection under declared-correct and computed FCS modes. +Use distinct padding chunks and verify their separate outcomes. Include zero-length spacing delimiters. Each existing chunk receives one outcome. The feature A4 fixture separately proves delimiter continuity and FCS rejection. Feature A4 also verifies EIFS suppression after partial reception and one corruption notification after total failure. Feature A5 owns production member finalization, computed-FCS retry checks, and aggregate cancellation during SIFS. The PHY wire fixture does not establish those MAC transmission results. [Feature acceptance fixtures](ieee80211-ampdu-implicit-blockack.md#proposed-direct-checks). -P3 also compares range probabilities against each model's current per-length calculation. NIST/Yans runs use seeds 0, 1, and 2 to check invariants and reproducibility. These runs establish no statistical goodput or error-accuracy claim. Repeated cache access must leave later random draws unchanged. +P3 also compares chunk probabilities against each model's current per-length calculation. NIST/Yans runs use seeds 0, 1, and 2 to check invariants and reproducibility. These runs establish no statistical goodput or error-accuracy claim. Repeated cache access must leave later random draws unchanged. Feature-off controls compare compatible ordinary MPDU results and subsequent draws with the pinned parent under identical inputs. A non-HT custom model without the role must retain ordinary MPDU reception. Its HT configuration must fail initialization before any capability advertisement, also with the default transmit policy. After initialization, a change to an HT mode set without the required role must fail before receiver acceptance. The feature capability fixture separately proves the absence of management advertisements and association after the initialization failure. -Use a custom model that exposes the role but supports only long GI. Use exact MAC inputs that permit short-GI advertisement. Verify initialization failure before the MIB changes local HT capability. The mode-support query must add no probability call or random draw. Also verify success with one antenna and unsupported modes outside the effective NSS limit. These checks must use the production MAC handoff and its exact mode set, channel widths, and NSS limit. +Use a generic BER model without an aggregation role. Verify successful production MAC initialization. Verify that the common DATA operation samples chunks through BER. Test an unavailable BER and an invalid probability as explicit runtime failures. With complete coverage, receive a valid HT20 short-GI aggregate through the production radio. D1 and feature A2 verify the copied PHY indication and member outcomes. Feature A5 separately verifies MAC receipt and one Compressed response after SIFS. The local aggregate transmit policy remains at long GI. Any later capability-update caller must use the same validation boundary; this plan adds no such runtime caller. D1 also checks ordinary HT reception with an absent error model and with each shared NIST/Yans implementation. Compare results and subsequent draws with the pinned parent. Retain the current VHT/HT fixture's absent-model configuration. Within the aggregate profile, an absent model produces successful physical ranges only after all common PHY checks succeed. Failed acquisition or SNIR checks still prevent valid aggregate content. Delimiter and FCS rejection remain active without a model, and no error draw occurs. -The PHY module fixture includes a non-HT BerTable control and an expected HT refusal case. Supply a valid table file so that a file error cannot mask the operation-mode refusal. Compare the non-HT control with P0's parent evidence. Record any parent failure separately from feature regressions. Verify that BerTable's current error at local initialization precedes the new role check. Keep the custom HT model case as the direct proof of that new check. +The PHY module fixture retains a non-HT BerTable control and its expected HT configuration refusal. A valid table file separates the operation-mode refusal from a file error. No additional role check applies. The planned commands use the current runners. They cannot establish a result before the fixtures exist: From 35e43f487bd8fef05d7a7143351ef8df0be3b24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 18 Oct 2026 02:29:59 +0200 Subject: [PATCH 10/24] ieee80211: fix: permit cyclic sequence arithmetic across zero Before this commit, valid sequence arithmetic can produce an intermediate value of minus one. The shared helper rejects that value, so 0 - 1 fails instead of returning sequence 4095. After this commit, the helper reduces every valid intermediate modulo 4096. Constructors and arithmetic operators still reject an unspecified source. The unit fixture checks every sequence value with positive and negative offsets. Aggregate reorder advancement needs this correction at sequence wrap. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.change.fix | test | ieee80211-implicit-blockack --- .../ieee80211/mac/common/Ieee80211Defs.h | 2 +- .../unit/Ieee80211SequenceNumberCyclic_1.test | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/unit/Ieee80211SequenceNumberCyclic_1.test diff --git a/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h b/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h index a2ebabd6644..f9927401f68 100644 --- a/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h +++ b/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h @@ -27,7 +27,7 @@ struct SequenceNumberCyclic SequenceNumber value; private: - SequenceNumber modulo4096(SequenceNumber value) const { ASSERT(value != -1); return (value % 4096 + 4096) % 4096; } // always returns positive result + SequenceNumber modulo4096(SequenceNumber value) const { return (value % 4096 + 4096) % 4096; } // always returns positive result SequenceNumber distance4096(SequenceNumber other) const { ASSERT(0 <= other && other < 4096); return (value - other + 4096) % 4096; } public: diff --git a/tests/unit/Ieee80211SequenceNumberCyclic_1.test b/tests/unit/Ieee80211SequenceNumberCyclic_1.test new file mode 100644 index 00000000000..b0fb1057e9e --- /dev/null +++ b/tests/unit/Ieee80211SequenceNumberCyclic_1.test @@ -0,0 +1,36 @@ +%description: +All specified sequence numbers support cyclic addition and subtraction across zero. +An intermediate value of minus one maps to 4095. It does not represent an unspecified source object. +The operators retain refusal of an unspecified source object. + +%includes: +#include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" +using namespace inet::ieee80211; + +%activity: +int checked = 0; +for (int value = 0; value < 4096; value++) { + SequenceNumberCyclic sequence(value); + for (int offset : {-4096, -4095, -2048, -64, -2, -1, 0, 1, 2, 63, 64, 2047, 2048, 4095, 4096}) { + auto normalize = [](int number) { return (number % 4096 + 4096) % 4096; }; + ASSERT((sequence + offset).get() == normalize(value + offset)); + ASSERT((sequence - offset).get() == normalize(value - offset)); + checked += 2; + } +} +SequenceNumberCyclic unspecified; +ASSERT(!unspecified.isSpecified()); +for (bool subtract : {false, true}) { + bool refused = false; + try { + auto result = subtract ? unspecified - 1 : unspecified + 1; + (void)result; + } + catch (const omnetpp::cRuntimeError&) { refused = true; } + ASSERT(refused); +} +ASSERT(checked == 4096 * 15 * 2); +EV << "Cyclic sequence arithmetic and unspecified-source guards passed.\n"; + +%contains: stdout +Cyclic sequence arithmetic and unspecified-source guards passed. From d8c991494286bb4462e015de5a55ea66a869983e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 18 Oct 2026 02:29:59 +0200 Subject: [PATCH 11/24] ieee80211: add: retain directional HT MPDU spacing limits Before this commit, negotiated HT state lacks the receiver's minimum MPDU start spacing. An aggregate builder therefore cannot apply that receiver limit. After this commit, full and simplified management preserve the encoded spacing in each negotiated direction. The MIB exposes the local receiver setting. The transmitter can obtain the peer receiver's limit before aggregate construction. Capability fixtures check asymmetric limits, encoding, initialization, and reset. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-implicit-blockack --- .../ieee80211/mgmt/Ieee80211HtMgmtElements.h | 4 + .../ieee80211/mib/Ieee80211HtCapabilities.h | 7 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 3 + .../linklayer/ieee80211/mib/Ieee80211Mib.ned | 1 + .../module/Ieee80211AmpduCapabilities_1.test | 102 ++++++++++++++++++ tests/module/Ieee80211HtAssociation_1.test | 14 +++ ...0211MgmtStaSimplifiedInitialization_1.test | 19 ++++ .../Ieee80211AmpduCapabilityEncoding_1.test | 68 ++++++++++++ 8 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 tests/module/Ieee80211AmpduCapabilities_1.test create mode 100644 tests/unit/Ieee80211AmpduCapabilityEncoding_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h index fb62285460e..acc0c2e6fb8 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h @@ -23,6 +23,7 @@ inline Ieee80211HtCapabilitiesElement makeHtCapabilitiesElement(const Ieee80211H element.shortGi20 = capabilities.shortGi20; element.shortGi40 = capabilities.shortGi40; element.maxAmpduLengthExponent = capabilities.maxAmpduLengthExponent; + element.minimumMpduStartSpacing = capabilities.minimumMpduStartSpacing; for (int i = 0; i < 77; i++) element.rxMcsSupported[i] = capabilities.rxMcsSupported[i]; element.txMcsSetDefined = capabilities.txMcsSetDefined; @@ -46,6 +47,8 @@ inline Ieee80211HtCapabilities makeHtCapabilities(const Ieee80211HtCapabilitiesE { if (element.maxAmpduLengthExponent < 0 || element.maxAmpduLengthExponent > 3) throw cRuntimeError("Invalid Maximum A-MPDU Length Exponent: %d", element.maxAmpduLengthExponent); + if (element.minimumMpduStartSpacing < 0 || element.minimumMpduStartSpacing > 7) + throw cRuntimeError("Invalid Minimum MPDU Start Spacing: %d", element.minimumMpduStartSpacing); if (!element.txMcsSetDefined && (element.txRxMcsSetNotEqual || element.txMaxNss != 0 || element.txUnequalModulation)) throw cRuntimeError("Malformed undefined HT Tx MCS Set"); if (element.txMcsSetDefined && !element.txRxMcsSetNotEqual && @@ -63,6 +66,7 @@ inline Ieee80211HtCapabilities makeHtCapabilities(const Ieee80211HtCapabilitiesE capabilities.shortGi20 = element.shortGi20; capabilities.shortGi40 = element.shortGi40; capabilities.maxAmpduLengthExponent = element.maxAmpduLengthExponent; + capabilities.minimumMpduStartSpacing = element.minimumMpduStartSpacing; capabilities.txMcsSetDefined = element.txMcsSetDefined; capabilities.txRxMcsSetNotEqual = element.txRxMcsSetNotEqual; capabilities.txMaxNss = element.txMaxNss; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index 26a27315c11..86699066b71 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -53,13 +53,15 @@ struct Ieee80211HtCapabilities bool shortGi20 = false; bool shortGi40 = false; int maxAmpduLengthExponent = 0; + int minimumMpduStartSpacing = 0; // Encoded receiver limit, 0..7 (IEEE Std 802.11-2024, Table 9-225). bool operator==(const Ieee80211HtCapabilities& other) const { return supportedChannelWidths == other.supportedChannelWidths && rxMcsSupported == other.rxMcsSupported && txMcsSetDefined == other.txMcsSetDefined && txRxMcsSetNotEqual == other.txRxMcsSetNotEqual && txMaxNss == other.txMaxNss && txUnequalModulation == other.txUnequalModulation && txMcsNss.maxMcsPerNss == other.txMcsNss.maxMcsPerNss && ldpc == other.ldpc && greenfield == other.greenfield && - shortGi20 == other.shortGi20 && shortGi40 == other.shortGi40 && maxAmpduLengthExponent == other.maxAmpduLengthExponent; + shortGi20 == other.shortGi20 && shortGi40 == other.shortGi40 && maxAmpduLengthExponent == other.maxAmpduLengthExponent && + minimumMpduStartSpacing == other.minimumMpduStartSpacing; } }; @@ -90,6 +92,7 @@ struct Ieee80211HtDirectionalCapabilities bool receiverShortGi20 = false; bool receiverShortGi40 = false; int receiverMaxAmpduLengthExponent = 0; + int receiverMinimumMpduStartSpacing = 0; // Encoded receiver limit, 0..7. }; struct Ieee80211NegotiatedHtCapabilities @@ -140,6 +143,8 @@ inline Ieee80211NegotiatedHtCapabilities negotiateHtCapabilities(const Ieee80211 negotiated.localRxPeerTx.receiverShortGi40 = local.shortGi40; negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent = peer.maxAmpduLengthExponent; negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent = local.maxAmpduLengthExponent; + negotiated.localTxPeerRx.receiverMinimumMpduStartSpacing = peer.minimumMpduStartSpacing; + negotiated.localRxPeerTx.receiverMinimumMpduStartSpacing = local.minimumMpduStartSpacing; // The HT Operation width is the BSS maximum. A 20 MHz-only STA may join a // 20/40 MHz BSS, so validity requires a common usable width, not equality // with the advertised BSS width (IEEE Std 802.11-2024, 11.15.2). diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index f57837295d2..e25d07a2ece 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -188,6 +188,9 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS localHtCapabilities.maxAmpduLengthExponent = par("htMaxAmpduLengthExponent"); if (localHtCapabilities.maxAmpduLengthExponent < 0 || localHtCapabilities.maxAmpduLengthExponent > 3) throw cRuntimeError("htMaxAmpduLengthExponent must be between 0 and 3"); + localHtCapabilities.minimumMpduStartSpacing = par("htMinimumMpduStartSpacing"); + if (localHtCapabilities.minimumMpduStartSpacing < 0 || localHtCapabilities.minimumMpduStartSpacing > 7) + throw cRuntimeError("htMinimumMpduStartSpacing must be between 0 and 7"); configuredSecondaryChannelOffset = par("htSecondaryChannelOffset"); if (configuredSecondaryChannelOffset != 0 && configuredSecondaryChannelOffset != 1 && configuredSecondaryChannelOffset != 3) diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 8430c1e17fc..557e5dd412c 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -25,6 +25,7 @@ simple Ieee80211Mib extends SimpleModule @signal[rateStateChanged](type=bool); // Model-backed subset of IEEE Std 802.11-2024 HT capability/operation state; not a full Annex C MIB. int htMaxAmpduLengthExponent = default(0); // maximum received A-MPDU length exponent (0..3) + int htMinimumMpduStartSpacing = default(0); // encoded receiver limit: 0=no restriction, 1=0.25us, 2=0.5us, 3=1us, 4=2us, 5=4us, 6=8us, 7=16us int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; bounded by the mode set int htProtectionMode = default(0); // 0=none, 1=nonmember, 2=20 MHz, 3=non-HT mixed displayStringTextFormat = default("Address: {address}{ssidStr}\n{modeStr}{stationTypeStr}{qosStr}{associatedStr}"); diff --git a/tests/module/Ieee80211AmpduCapabilities_1.test b/tests/module/Ieee80211AmpduCapabilities_1.test new file mode 100644 index 00000000000..409ac762eb9 --- /dev/null +++ b/tests/module/Ieee80211AmpduCapabilities_1.test @@ -0,0 +1,102 @@ +%description: +Production simplified management preserves all four HT length exponents and eight spacing encodings. +Opposite receiver limits must reach the local transmit direction. +Peer cleanup must remove the old limits before a replacement peer state arrives. +The separate full association fixture checks management frame exchange. + +%extraargs: -r 0..31 + +%file: AmpduCapabilities.cc +#include +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +using namespace inet; +using namespace inet::ieee80211; + +class AmpduCapabilities : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + auto ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + auto sta = check_and_cast(getModuleByPath("^.sta.wlan[0].mib")); + ASSERT(ap->findPeerHtState(sta->address) != nullptr); + ASSERT(sta->findPeerHtState(ap->address) != nullptr); + const auto& tx = sta->findPeerHtState(ap->address)->negotiatedCapabilities.localTxPeerRx; + const auto& rx = sta->findPeerHtState(ap->address)->negotiatedCapabilities.localRxPeerTx; + ASSERT(tx.receiverMinimumMpduStartSpacing == ap->par("htMinimumMpduStartSpacing").intValue()); + ASSERT(rx.receiverMinimumMpduStartSpacing == sta->par("htMinimumMpduStartSpacing").intValue()); + ASSERT(tx.receiverMaxAmpduLengthExponent == ap->par("htMaxAmpduLengthExponent").intValue()); + ASSERT(rx.receiverMaxAmpduLengthExponent == sta->par("htMaxAmpduLengthExponent").intValue()); + auto saved = ap->localHtCapabilities; + auto operation = ap->getHtOperation(); + sta->clearBssAndPeerState(ap->address); + ASSERT(sta->findPeerHtState(ap->address) == nullptr); + saved.minimumMpduStartSpacing = (saved.minimumMpduStartSpacing + 1) % 8; + sta->setPeerHtCapabilities(ap->address, saved, operation); + ASSERT(sta->findPeerHtState(ap->address)->negotiatedCapabilities.localTxPeerRx.receiverMinimumMpduStartSpacing == + saved.minimumMpduStartSpacing); + std::cout << "HT aggregate receiver limits passed\n"; + } +}; + +Define_Module(AmpduCapabilities); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple AmpduCapabilities +{ + parameters: + @class(::AmpduCapabilities); +} + +network AmpduCapabilitiesNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta: WirelessHost; + checker: AmpduCapabilities; +} + +%inifile: omnetpp.ini +[General] +network = AmpduCapabilitiesNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 0s +seed-set = 0 +cmdenv-express-mode = true +**.vector-recording = false +**.scalar-recording = false +*.ap.wlan[0].mgmt.typename = "Ieee80211MgmtApSimplified" +*.sta.wlan[0].mgmt.typename = "Ieee80211MgmtStaSimplified" +**.wlan[0].agent.typename = "" +*.ap.wlan[0].address = "02:00:00:00:00:01" +*.sta.wlan[0].address = "02:00:00:00:00:02" +*.sta.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" +*.ap.wlan[0].mib.htMaxAmpduLengthExponent = ${exponent=0..3} +*.sta.wlan[0].mib.htMaxAmpduLengthExponent = 3 - ${exponent} +*.ap.wlan[0].mib.htMinimumMpduStartSpacing = ${spacing=0..7} +*.sta.wlan[0].mib.htMinimumMpduStartSpacing = 7 - ${spacing} +**.wlan[0].opMode = "n(mixed-2.4Ghz)" +**.wlan[0].bitrate = 65Mbps +**.wlan[0].radio.bandName = "2.4 GHz" +**.wlan[0].radio.channelNumber = 1 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +HT aggregate receiver limits passed diff --git a/tests/module/Ieee80211HtAssociation_1.test b/tests/module/Ieee80211HtAssociation_1.test index aeee34722e2..a9cf55dc583 100644 --- a/tests/module/Ieee80211HtAssociation_1.test +++ b/tests/module/Ieee80211HtAssociation_1.test @@ -136,6 +136,16 @@ class Ieee80211HtAssociationChecker : public SimpleModule, public cListener // cannot operate a primary/secondary compound channel and must not // advertise that width. ASSERT(staMib->localHtCapabilities.shortGi20); + ASSERT(staMib->localHtCapabilities.minimumMpduStartSpacing == 1); + ASSERT(apMib->localHtCapabilities.minimumMpduStartSpacing == 7); + const auto& apLimits = apMib->findPeerHtState(staAddress)->negotiatedCapabilities; + const auto& staLimits = staMib->findPeerHtState(apMib->address)->negotiatedCapabilities; + ASSERT(apLimits.localTxPeerRx.receiverMinimumMpduStartSpacing == 1); + ASSERT(apLimits.localRxPeerTx.receiverMinimumMpduStartSpacing == 7); + ASSERT(staLimits.localTxPeerRx.receiverMinimumMpduStartSpacing == 7); + ASSERT(staLimits.localRxPeerTx.receiverMinimumMpduStartSpacing == 1); + ASSERT(apLimits.localTxPeerRx.receiverMaxAmpduLengthExponent == 1); + ASSERT(staLimits.localTxPeerRx.receiverMaxAmpduLengthExponent == 3); ASSERT(!staMib->localHtCapabilities.shortGi40); ASSERT(apMib->localHtCapabilities.shortGi20); ASSERT(!apMib->localHtCapabilities.shortGi40); @@ -231,6 +241,10 @@ record-scalar-results = false **.mobility.initialZ = 0m **.wlan[*].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[0].mib.htMinimumMpduStartSpacing = 7 +*.sta.wlan[0].mib.htMinimumMpduStartSpacing = 1 +*.ap.wlan[0].mib.htMaxAmpduLengthExponent = 3 +*.sta.wlan[0].mib.htMaxAmpduLengthExponent = 1 **.wlan[*].bitrate = 65Mbps *.sta.wlan[0].radio.channelNumber = 6 **.wlan[*].radio.bandName = "2.4 GHz" diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test index acc48310936..8cefba0b7c0 100644 --- a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -28,6 +28,18 @@ class TestInitializationObserver : public cSimpleModule protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } + void checkReceiverLimits(ieee80211::Ieee80211Mib *apMib, ieee80211::Ieee80211Mib *staMib) + { + const auto& ap = apMib->findPeerHtState(staMib->address)->negotiatedCapabilities; + const auto& sta = staMib->findPeerHtState(apMib->address)->negotiatedCapabilities; + ASSERT(ap.localTxPeerRx.receiverMinimumMpduStartSpacing == 1); + ASSERT(ap.localRxPeerTx.receiverMinimumMpduStartSpacing == 7); + ASSERT(sta.localTxPeerRx.receiverMinimumMpduStartSpacing == 7); + ASSERT(sta.localRxPeerTx.receiverMinimumMpduStartSpacing == 1); + ASSERT(ap.localTxPeerRx.receiverMaxAmpduLengthExponent == 1); + ASSERT(sta.localTxPeerRx.receiverMaxAmpduLengthExponent == 3); + } + virtual void initialize(int stage) override { if (stage == INITSTAGE_NETWORK_CONFIGURATION || stage == INITSTAGE_LAST) { @@ -54,6 +66,7 @@ class TestInitializationObserver : public cSimpleModule else { ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + checkReceiverLimits(apMib, staMib); ASSERT(apMib->findPeerHtState(staMib->address)->negotiatedCapabilities.operation.primaryChannel == 6); ASSERT(staMib->findPeerHtState(apMib->address)->negotiatedCapabilities.operation.primaryChannel == 6); std::cout << "Simplified STA HT peer state installed at last initialization stage.\n"; @@ -81,6 +94,7 @@ class TestInitializationObserver : public cSimpleModule std::cout << "Simplified STA AP-side association and HT peer state removed after shutdown.\n"; } else if (!strcmp(message->getName(), "checkShutdownRestart")) { + checkReceiverLimits(apMib, staMib); ASSERT(staMib->bssStationData.isAssociated); ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); @@ -96,6 +110,7 @@ class TestInitializationObserver : public cSimpleModule std::cout << "Simplified STA AP-side association and HT peer state removed after resolvable crash.\n"; } else if (!strcmp(message->getName(), "checkCrashRestart")) { + checkReceiverLimits(apMib, staMib); ASSERT(staMib->bssStationData.isAssociated); ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); @@ -187,6 +202,10 @@ record-scalar-results = false *.ap.wlan[0].radio.channelNumber = 6 *.sta.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" **.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[0].mib.htMinimumMpduStartSpacing = 7 +*.sta.wlan[0].mib.htMinimumMpduStartSpacing = 1 +*.ap.wlan[0].mib.htMaxAmpduLengthExponent = 3 +*.sta.wlan[0].mib.htMaxAmpduLengthExponent = 1 **.hasStatus = true **.scenarioManager.script = xmldoc("scenario.xml") **.wlan[0].bitrate = 65Mbps diff --git a/tests/unit/Ieee80211AmpduCapabilityEncoding_1.test b/tests/unit/Ieee80211AmpduCapabilityEncoding_1.test new file mode 100644 index 00000000000..0731250f456 --- /dev/null +++ b/tests/unit/Ieee80211AmpduCapabilityEncoding_1.test @@ -0,0 +1,68 @@ +%description: +Verify all HT receive length exponents and MPDU spacing encodings through management conversion and bytes. +Asymmetric negotiation preserves the receiver's limits in each direction. +Invalid encoded spacing cannot enter capability state through management conversion. + +%includes: +#include "inet/common/packet/Packet.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" + +using namespace inet; +using namespace inet::ieee80211; + +%activity: +for (int exponent = 0; exponent < 4; exponent++) { + for (int spacing = 0; spacing < 8; spacing++) { + Ieee80211HtCapabilities local; + local.supportedChannelWidths.insert(MHz(20)); + local.rxMcsSupported[0] = true; + local.txMcsNss.maxMcsPerNss[0] = 0; + local.maxAmpduLengthExponent = exponent; + local.minimumMpduStartSpacing = spacing; + auto peer = local; + peer.maxAmpduLengthExponent = 3 - exponent; + peer.minimumMpduStartSpacing = 7 - spacing; + ASSERT(!(local == peer)); + auto element = makeHtCapabilitiesElement(local); + ASSERT(element.minimumMpduStartSpacing == spacing); + ASSERT(makeHtCapabilities(element) == local); + auto response = makeShared(); + response->setStatusCode(SC_SUCCESSFUL); + response->setAid(1); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + response->setSupportedRates(rates); + setHtCapabilities(response, local); + response->setChunkLength(B(37)); + Packet output("response", response); + auto bytes = output.peekAllAsBytes()->getBytes(); + ASSERT(bytes.size() == 37); + ASSERT(bytes[13] == (exponent | (spacing << 2))); + Packet input("fresh", makeShared(bytes)); + auto decoded = input.popAtFront(B(37)); + ASSERT(makeHtCapabilities(decoded->getHtCapabilities()) == local); + auto negotiated = negotiateHtCapabilities(local, peer, Ieee80211HtOperation()); + ASSERT(negotiated.localTxPeerRx.valid && negotiated.localRxPeerTx.valid); + ASSERT(negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent == 3 - exponent); + ASSERT(negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent == exponent); + ASSERT(negotiated.localTxPeerRx.receiverMinimumMpduStartSpacing == 7 - spacing); + ASSERT(negotiated.localRxPeerTx.receiverMinimumMpduStartSpacing == spacing); + auto changed = local; + changed.minimumMpduStartSpacing = (spacing + 1) % 8; + ASSERT(!(local == changed)); + } +} +for (int spacing : {-1, 8}) { + Ieee80211HtCapabilitiesElement element; + element.minimumMpduStartSpacing = spacing; + Ieee80211HtCapabilities result; + std::string reason; + ASSERT(!decodeHtCapabilities(element, result, reason)); + ASSERT(reason.find("Minimum MPDU Start Spacing") != std::string::npos); +} +EV << "HT aggregate capability checks passed\n"; + +%contains: stdout +HT aggregate capability checks passed From 739127da1ec0bda9a16a910ff4c0f45011afcf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 18 Oct 2026 02:29:59 +0200 Subject: [PATCH 12/24] ieee80211: add+change: construct HT aggregates within receiver limits Before this commit, aggregate construction cannot preview spacing at the selected DATA mode while it preserves the source MPDUs. It also lacks the complete HT duration admission calculation. After this commit, the builder borrows source references and copies their data and region tags. Alignment padding and zero-length delimiters satisfy negotiated spacing. Construction checks the receiver byte limit, PPDU duration, and L-SIG length. Singleton aggregates retain their delimiter. Construction tests use the existing chunk walker without an intermediate layout. The wire, traversal, and construction fixtures check serialization, spacing boundaries, source ownership, and malformed input. These operations prepare the later exchange without enabling the default MPDU policy. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.add+change | test whatsnew migration | ieee80211-implicit-blockack --- .../mac/aggregation/MpduAggregation.cc | 77 +++++-- .../mac/aggregation/MpduAggregation.h | 7 + .../ieee80211/mac/contract/IMpduAggregation.h | 10 + .../ieee80211/mode/Ieee80211HtMode.cc | 30 +++ .../wireless/ieee80211/mode/Ieee80211HtMode.h | 9 + .../module/Ieee80211AmpduCapabilities_1.test | 22 ++ tests/unit/Ieee80211AmpduConstruction_1.test | 196 ++++++++++++++++++ 7 files changed, 339 insertions(+), 12 deletions(-) create mode 100644 tests/unit/Ieee80211AmpduConstruction_1.test diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.cc b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.cc index a0c8384d539..f1906962eb0 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" namespace inet { namespace ieee80211 { @@ -16,30 +17,82 @@ Register_Class(MpduAggregation); Packet *MpduAggregation::aggregateFrames(std::vector *frames) { - EV_DEBUG << "Aggregating " << frames->size() << " packets into A-MPDU.\n"; - auto aggregatedFrame = new Packet(); + auto aggregate = buildAggregate(*frames, B(0)); + for (auto frame : *frames) + delete frame; + return aggregate; +} + +B MpduAggregation::computeAggregateLength(const std::vector& frames, const physicallayer::IIeee80211Mode *mode, + int minimumMpduStartSpacing) const +{ + auto htMode = dynamic_cast(mode); + if (htMode == nullptr) + return B(0); + B minimumDistance = htMode->getMinimumMpduStartDistance(minimumMpduStartSpacing); + B length(0); + for (size_t i = 0; i < frames.size(); i++) { + auto bits = frames[i]->getDataLength(); + // IEEE Std 802.11-2024, 9.7.1: each nonzero HT member has a 12-bit octet length. + if (bits <= b(0) || bits.get() % 8 != 0 || bits > B(4095)) + return B(0); + B subframeLength = B(4) + B(bits); + if (i + 1 != frames.size()) + subframeLength = B((std::max(subframeLength, minimumDistance).get() + 3) / 4 * 4); + length += subframeLength; + if (length > B(65535)) + return B(0); + } + return length; +} + +Packet *MpduAggregation::aggregateFrames(const std::vector& frames, const physicallayer::IIeee80211Mode *mode, + int minimumMpduStartSpacing, B maxAggregateLength) +{ + if (maxAggregateLength < B(0)) + throw cRuntimeError("Maximum aggregate length must be nonnegative"); + B length = computeAggregateLength(frames, mode, minimumMpduStartSpacing); + if (length == B(0) || length > maxAggregateLength) + return nullptr; + auto htMode = check_and_cast(mode); + if (!htMode->isPpduDurationValid(length)) + return nullptr; + return buildAggregate(frames, htMode->getMinimumMpduStartDistance(minimumMpduStartSpacing)); +} + +Packet *MpduAggregation::buildAggregate(const std::vector& frames, B minimumStartDistance) const +{ + EV_DEBUG << "Aggregating " << frames.size() << " packets into A-MPDU.\n"; + auto aggregatedFrame = std::make_unique(); std::string aggregatedName; - for (size_t i = 0; i < frames->size(); i++) { + for (size_t i = 0; i < frames.size(); i++) { auto mpduSubframeHeader = makeShared(); - auto frame = frames->at(i); - auto mpdu = frame->peekAll(); + auto frame = frames.at(i); + auto mpdu = frame->peekData(); mpduSubframeHeader->setLength(mpdu->getChunkLength().get()); aggregatedFrame->insertAtBack(mpduSubframeHeader); aggregatedFrame->insertAtBack(mpdu); - aggregatedFrame->getRegionTags().copyTags(frame->getRegionTags(), B(0), aggregatedFrame->getFrontOffset() - frame->getDataLength(), frame->getDataLength()); - int paddingLength = 4 - (mpduSubframeHeader->getChunkLength() + mpdu->getChunkLength()).get() % 4; - if (i + 1 != frames->size() && paddingLength != 4) { - auto padding = makeShared(B(paddingLength)); - aggregatedFrame->insertAtBack(padding); + aggregatedFrame->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset(), + aggregatedFrame->getBackOffset() - frame->getDataLength(), frame->getDataLength()); + if (i + 1 != frames.size()) { + // IEEE Std 802.11-2024, 9.7.1 and 10.12.3: alignment precedes zero-length delimiters. + B subframeLength = B(4) + B(mpdu->getChunkLength()); + B alignedLength((subframeLength.get() + 3) / 4 * 4); + B paddingLength = alignedLength - subframeLength; + if (paddingLength != B(0)) + aggregatedFrame->insertAtBack(makeShared(paddingLength, 0)); + while (alignedLength < minimumStartDistance) { + aggregatedFrame->insertAtBack(makeShared()); + alignedLength += B(4); + } } if (i != 0) aggregatedName.append("+"); aggregatedName.append(frame->getName()); - delete frame; } aggregatedFrame->setName(aggregatedName.c_str()); EV_TRACE << "Created A-MPDU " << *aggregatedFrame << ".\n"; - return aggregatedFrame; + return aggregatedFrame.release(); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h index 6828d518e60..64bc3fa4a96 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h @@ -15,8 +15,15 @@ namespace ieee80211 { class INET_API MpduAggregation : public IMpduAggregation, public cObject { + protected: + Packet *buildAggregate(const std::vector& frames, B minimumStartDistance) const; + public: virtual Packet *aggregateFrames(std::vector *frames) override; + virtual B computeAggregateLength(const std::vector& frames, const physicallayer::IIeee80211Mode *mode, + int minimumMpduStartSpacing) const override; + virtual Packet *aggregateFrames(const std::vector& frames, const physicallayer::IIeee80211Mode *mode, + int minimumMpduStartSpacing, B maxAggregateLength) override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregation.h b/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregation.h index 62d812498ba..8a7d457a643 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregation.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregation.h @@ -12,6 +12,9 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { +namespace physicallayer { +class IIeee80211Mode; +} namespace ieee80211 { class INET_API IMpduAggregation @@ -20,6 +23,13 @@ class INET_API IMpduAggregation virtual ~IMpduAggregation() {} virtual Packet *aggregateFrames(std::vector *frames) = 0; + + // These HT operations borrow the sources. Zero length or nullptr means refusal without source changes. + // Spacing is the receiver's encoded value (0..7). The byte limit includes delimiters and padding. + virtual B computeAggregateLength(const std::vector& frames, const physicallayer::IIeee80211Mode *mode, + int minimumMpduStartSpacing) const = 0; + virtual Packet *aggregateFrames(const std::vector& frames, const physicallayer::IIeee80211Mode *mode, + int minimumMpduStartSpacing, B maxAggregateLength) = 0; }; } // namespace ieee80211 diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index 5f86f6ad39c..86b76c1a9b3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -19,6 +19,36 @@ namespace inet { namespace physicallayer { +B Ieee80211HtMode::getMinimumMpduStartDistance(int minimumMpduStartSpacing) const +{ + // IEEE Std 802.11-2024, Table 9-225 and Equation (10-12). + static const int SPACING_NS[] = {0, 250, 500, 1000, 2000, 4000, 8000, 16000}; + if (minimumMpduStartSpacing < 0 || minimumMpduStartSpacing > 7) + throw cRuntimeError("Minimum MPDU Start Spacing must be between 0 and 7"); + return B(int64_t(ceil(dataMode->getNetBitrate().get() * SPACING_NS[minimumMpduStartSpacing] / 8e9))); +} + +int Ieee80211HtMode::getLSigLength(b dataLength) const +{ + if (preambleMode->getPreambleFormat() != Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED) + throw cRuntimeError("L-SIG length requires HT mixed format"); + // IEEE Std 802.11-2024, 10.27.4, Equation (10-17). Exact ticks preserve the symbol boundary. + auto remaining = getDuration(dataLength) - getSignalExtensionTime() - SimTime(20, SIMTIME_US); + auto symbol = SimTime(4, SIMTIME_US); + return (remaining.raw() + symbol.raw() - 1) / symbol.raw() * 3 - 3; +} + +bool Ieee80211HtMode::isPpduDurationWithinLimit(b dataLength) const +{ + return getDuration(dataLength) <= SimTime(10, SIMTIME_MS); +} + +bool Ieee80211HtMode::isPpduDurationValid(b dataLength) const +{ + return isPpduDurationWithinLimit(dataLength) && + (preambleMode->getPreambleFormat() != Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED || getLSigLength(dataLength) <= 4095); +} + OPP_THREAD_LOCAL const Ieee80211HtCompliantModes Ieee80211HtCompliantModes::singleton; bps Ieee80211HtMode::getNonHtReferenceRate() const diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index ee6c0a76685..27b0162ba9c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -268,6 +268,15 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } + // IEEE Std 802.11-2024, 10.12.3 and Table 9-225. The input is the encoded receiver limit (0..7). + B getMinimumMpduStartDistance(int minimumMpduStartSpacing) const; + // IEEE Std 802.11-2024, 10.27.4. The result is the L-SIG LENGTH field in octets, before its 4095 limit. + int getLSigLength(b dataLength) const; + // IEEE Std 802.11-2024, Table 19-25. This predicate checks the separate 10 ms bound. + bool isPpduDurationWithinLimit(b dataLength) const; + // Both the 10 ms bound and the mixed-format L-SIG bound apply. + bool isPpduDurationValid(b dataLength) const; + virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength) + getSignalExtensionTime(); } }; diff --git a/tests/module/Ieee80211AmpduCapabilities_1.test b/tests/module/Ieee80211AmpduCapabilities_1.test index 409ac762eb9..67dec4d175e 100644 --- a/tests/module/Ieee80211AmpduCapabilities_1.test +++ b/tests/module/Ieee80211AmpduCapabilities_1.test @@ -9,7 +9,10 @@ The separate full association fixture checks management frame exchange. %file: AmpduCapabilities.cc #include #include "inet/common/InitStages.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" using namespace inet; using namespace inet::ieee80211; @@ -33,6 +36,25 @@ class AmpduCapabilities : public cSimpleModule ASSERT(rx.receiverMinimumMpduStartSpacing == sta->par("htMinimumMpduStartSpacing").intValue()); ASSERT(tx.receiverMaxAmpduLengthExponent == ap->par("htMaxAmpduLengthExponent").intValue()); ASSERT(rx.receiverMaxAmpduLengthExponent == sta->par("htMaxAmpduLengthExponent").intValue()); + Packet first("first", makeShared(std::vector(30, 0))); + Packet second("second", makeShared(std::vector(30, 0))); + std::vector sources = {&first, &second}; + const physicallayer::IIeee80211Mode *mode = nullptr; + auto modes = physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < modes->getNumModes(); i++) { + auto candidate = modes->getMode(i); + if (candidate->getHtMcsIndex() == 7 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + mode = candidate; + } + ASSERT(mode != nullptr); + MpduAggregation builder; + auto envelope = builder.aggregateFrames(sources, mode, tx.receiverMinimumMpduStartSpacing, + B((1 << (13 + tx.receiverMaxAmpduLengthExponent)) - 1)); + ASSERT(envelope != nullptr); + const int EXPECTED_DISTANCE[] = {36, 36, 36, 36, 36, 36, 68, 132}; + ASSERT(envelope->getDataLength() == B(EXPECTED_DISTANCE[ap->par("htMinimumMpduStartSpacing").intValue()] + 34)); + delete envelope; auto saved = ap->localHtCapabilities; auto operation = ap->getHtOperation(); sta->clearBssAndPeerState(ap->address); diff --git a/tests/unit/Ieee80211AmpduConstruction_1.test b/tests/unit/Ieee80211AmpduConstruction_1.test new file mode 100644 index 00000000000..d258ee8b3d8 --- /dev/null +++ b/tests/unit/Ieee80211AmpduConstruction_1.test @@ -0,0 +1,196 @@ +%description: +Verify HT aggregate construction with all receiver spacing encodings at two DATA modes. +Independent bytes prove alignment, zero-length delimiters, and the absence of final padding. +Source Packets retain their content and region tags after construction and refusal. +HT-SIG byte conversion preserves singleton identity through the P1 representation. + +%includes: +#include "inet/common/TimeTag_m.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211HtSignalField.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +%global: +static const Ieee80211HtMode *findMode(int mcs, bool use24Ghz = false) +{ + const Ieee80211Htmcs *parameters = mcs == 0 ? &Ieee80211HtmcsTable::htMcs0BW20MHz : + mcs == 7 ? &Ieee80211HtmcsTable::htMcs7BW20MHz : &Ieee80211HtmcsTable::htMcs15BW20MHz; + return Ieee80211HtCompliantModes::getCompliantMode(parameters, + use24Ghz ? Ieee80211HtMode::BAND_2_4GHZ : Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +} + +static Packet *makeMember(int length, int prefix = 0) +{ + auto packet = new Packet("member", makeShared(std::vector(length + prefix, 0xA5))); + packet->addRegionTag(B(prefix + 2), B(3)); + if (prefix) + packet->popAtFront(B(prefix)); + return packet; +} + +%activity: +MpduAggregation builder; +const int spacingNanoseconds[] = {0, 250, 500, 1000, 2000, 4000, 8000, 16000}; +for (int mcs : {0, 7}) { + auto mode = findMode(mcs); + const int rate = mcs == 0 ? 6500000 : 65000000; + for (int spacing = 0; spacing < 8; spacing++) { + for (int length : {29, 30, 31, 32}) { + std::vector frames = {makeMember(length, 6), makeMember(33)}; + int alignedDistance = ((length + 4 + 3) / 4) * 4; + int minimumDistance = (int64_t(spacingNanoseconds[spacing]) * rate + 7999999999LL) / 8000000000LL; + int distance = std::max(alignedDistance, ((minimumDistance + 3) / 4) * 4); + int extraDelimiters = (distance - alignedDistance) / 4; + ASSERT(mode->getMinimumMpduStartDistance(spacing) == B(minimumDistance)); + B expectedLength(distance + 4 + 33); + ASSERT(builder.computeAggregateLength(frames, mode, spacing) == expectedLength); + auto aggregate = builder.aggregateFrames(frames, mode, spacing, B(65535)); + ASSERT(aggregate != nullptr && aggregate->getDataLength() == expectedLength); + auto tags = aggregate->getAllRegionTags(); + ASSERT(tags.size() == 2); + ASSERT(tags[0].getStartOffset() == B(6) && tags[0].getLength() == B(3)); + ASSERT(tags[1].getStartOffset() == B(distance + 6) && tags[1].getLength() == B(3)); + auto bytes = aggregate->peekAllAsBytes()->getBytes(); + ASSERT(bytes.size() == expectedLength.get()); + ASSERT(bytes[0] == uint8_t(length << 4) && bytes[1] == (length >> 4) && bytes[3] == 0x4E); + for (int i = 0; i < length; i++) + ASSERT(bytes[4 + i] == 0xA5); + for (int i = length + 4; i < alignedDistance; i++) + ASSERT(bytes[i] == 0); + for (int i = 0; i < extraDelimiters; i++) { + int offset = alignedDistance + 4 * i; + ASSERT(bytes[offset] == 0 && bytes[offset + 1] == 0 && bytes[offset + 3] == 0x4E); + } + Packet independent("wire", makeShared(bytes)); + int delimiters = 0; + int members = 0; + bool complete = MpduDeaggregation::visitSubframes(&independent, B(0), expectedLength, + [&](const auto&, B, bool valid) { ASSERT(valid); delimiters++; }, + [&](const auto& mpdu, B offset) { + if (++members == 2) { + ASSERT(offset == B(distance + 4)); + ASSERT(mpdu->getChunkLength() == B(33)); + } + }); + ASSERT(complete && delimiters == 2 + extraDelimiters && members == 2); + auto refused = builder.aggregateFrames(frames, mode, spacing, expectedLength - B(1)); + ASSERT(refused == nullptr); + MpduDeaggregation deaggregation; + auto extracted = deaggregation.deaggregateFrame(aggregate->dup()); + ASSERT(extracted->size() == 2); + for (auto frame : *extracted) { + auto memberTags = frame->getAllRegionTags(); + ASSERT(memberTags.size() == 1); + ASSERT(memberTags.front().getStartOffset() == B(2)); + ASSERT(memberTags.front().getLength() == B(3)); + delete frame; + } + delete extracted; + for (auto frame : frames) { + ASSERT(frame->peekAllAsBytes()->getBytes().front() == 0xA5); + ASSERT(frame->getAllRegionTags().size() == 1); + delete frame; + } + // The envelope retains immutable member content after source disposal. + ASSERT(aggregate->peekAllAsBytes()->getBytes() == bytes); + delete aggregate; + } + } +} +auto mode = findMode(7); +std::vector singleton = {makeMember(31)}; +auto aggregate = builder.aggregateFrames(singleton, mode, 7, B(35)); +ASSERT(aggregate != nullptr && aggregate->getDataLength() == B(35)); +auto ht = makeShared(); +ht->setLengthField(B(35)); +ht->setAggregation(true); +ht->setMcs(7); +ht->setCrc(computeIeee80211HtSignalCrc(*ht)); +aggregate->insertAtFront(ht); +Packet fresh("fresh", makeShared(aggregate->peekAllAsBytes()->getBytes())); +auto decoded = fresh.popAtFront(B(6)); +ASSERT(decoded->getAggregation() && decoded->getLengthField() == B(35)); +ASSERT(isIeee80211HtSignalValid(*decoded, mode)); +ASSERT(MpduDeaggregation::visitSubframes(&fresh, B(0), B(35), + [](const auto&, B, bool valid) { ASSERT(valid); }, [](const auto&, B) {})); +delete aggregate; +delete singleton.front(); +std::vector empty; +ASSERT(builder.aggregateFrames(empty, mode, 0, B(65535)) == nullptr); +for (int length : {4095, 4096}) { + std::vector frames = {makeMember(length)}; + auto result = builder.aggregateFrames(frames, mode, 0, B(65535)); + ASSERT((result != nullptr) == (length == 4095)); + delete result; + delete frames.front(); +} +// 15 * (4 + 4092) + (4 + 4091) = 65535 octets, without final padding. +std::vector maximum; +for (int i = 0; i < 15; i++) + maximum.push_back(makeMember(4092)); +maximum.push_back(makeMember(4091)); +ASSERT(builder.computeAggregateLength(maximum, mode, 0) == B(65535)); +auto maximumAggregate = builder.aggregateFrames(maximum, findMode(15), 0, B(65535)); +ASSERT(maximumAggregate != nullptr); +delete maximumAggregate; +maximum.push_back(makeMember(30)); +ASSERT(builder.aggregateFrames(maximum, mode, 0, B(65535)) == nullptr); +for (auto frame : maximum) + delete frame; +// A low DATA rate reaches the L-SIG limit before the separate 10 ms limit. +auto slow = findMode(0); +auto slow24 = findMode(0, true); +bool sawLastValid = false; +for (int length = 4000; length < 5000; length++) { + auto duration = slow->getDuration(B(length)) - slow->getSignalExtensionTime(); + ASSERT(slow->isPpduDurationValid(B(length)) == (duration <= SimTime(5484, SIMTIME_US))); + ASSERT(slow24->getDuration(B(length)) == slow->getDuration(B(length)) + SimTime(6, SIMTIME_US)); + ASSERT(slow24->getLSigLength(B(length)) == slow->getLSigLength(B(length))); + ASSERT(slow24->isPpduDurationValid(B(length)) == slow->isPpduDurationValid(B(length))); + if (duration == SimTime(5484, SIMTIME_US)) { + ASSERT(slow->getLSigLength(B(length)) == 4095); + sawLastValid = true; + } +} +ASSERT(sawLastValid); +bool sawTenMilliseconds = false; +for (int length = 8000; length < 8200; length++) { + auto duration = slow->getDuration(B(length)); + ASSERT(slow->isPpduDurationWithinLimit(B(length)) == (duration <= SimTime(10, SIMTIME_MS))); + if (duration == SimTime(10, SIMTIME_MS)) + sawTenMilliseconds = true; +} +ASSERT(sawTenMilliseconds); +std::vector slowBatch = {makeMember(4000), makeMember(1000)}; +ASSERT(slow->getDuration(B(5008)) < SimTime(10, SIMTIME_MS)); +ASSERT(builder.aggregateFrames(slowBatch, slow, 0, B(65535)) == nullptr); +for (auto frame : slowBatch) + delete frame; +for (int spacing : {-1, 8}) { + bool rejected = false; + try { mode->getMinimumMpduStartDistance(spacing); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); +} +std::vector consumed = {makeMember(29, 6), makeMember(31)}; +auto legacy = builder.aggregateFrames(&consumed); +ASSERT(legacy->getDataLength() == B(71)); +ASSERT(MpduDeaggregation::visitSubframes(legacy, B(0), B(71), + [](const auto&, B, bool valid) { ASSERT(valid); }, [](const auto&, B) {})); +auto legacyTags = legacy->getAllRegionTags(); +ASSERT(legacyTags.size() == 2); +ASSERT(legacyTags[0].getStartOffset() == B(6)); +ASSERT(legacyTags[1].getStartOffset() == B(42)); +delete legacy; +EV << "HT aggregate construction checks passed\n"; + +%contains: stdout +HT aggregate construction checks passed From a08d7df9698a3f208191b4fd7ebd35b6363ca404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 18 Oct 2026 02:29:59 +0200 Subject: [PATCH 13/24] ieee80211: fix: refuse failed PHY containers before MAC peeks Before this commit, Rx excludes a decoded aggregate from standalone FCS checks. A failed common header can lack that decoded property, so its container can reach an unsafe MAC peek. After this commit, Rx also checks common-header success and complete content before standalone parsing. The boundary fixture supplies malformed containers and proves refusal before the FCS predicate. Correct standalone reception retains its existing path. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.change.fix | test | ieee80211-implicit-blockack --- src/inet/linklayer/ieee80211/mac/Rx.cc | 7 +- tests/module/Ieee80211AmpduRxBoundary_1.test | 148 +++++++++++++++++++ 2 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 tests/module/Ieee80211AmpduRxBoundary_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index 23f4993f8d4..169d208c9ae 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -121,10 +121,11 @@ bool Rx::lowerFrameReceived(Packet *packet) Enter_Method("lowerFrameReceived(\"%s\")", packet->getName()); take(packet); - // Aggregate containers need member acceptance before standalone FCS or header checks. - // Reject them until MAC aggregate reception exists; chunk flags retain PHY errors. + // A failed HT header supplies no decoded Aggregation property. Refuse its container before MAC peeks. + // Aggregate containers use the separate member acceptance path. auto aggregate = packet->findTag(); - bool isFrameOk = !(aggregate && aggregate->getAggregation()) && isFcsOk(packet); + bool isFrameOk = !(aggregate && (!aggregate->getHeaderSuccessful() || !aggregate->getContentComplete() || + aggregate->getAggregation())) && isFcsOk(packet); attachInterframeResult(packet, isFrameOk); if (isFrameOk) { EV_INFO << "Received frame from PHY: " << packet << endl; diff --git a/tests/module/Ieee80211AmpduRxBoundary_1.test b/tests/module/Ieee80211AmpduRxBoundary_1.test new file mode 100644 index 00000000000..53730f8b291 --- /dev/null +++ b/tests/module/Ieee80211AmpduRxBoundary_1.test @@ -0,0 +1,148 @@ +%description: +Rx rejects an unsuccessful PHY header or incomplete content before the standalone FCS check. +The rejection applies when the PHY clears the aggregation flag after header failure. +Seven PHY flag combinations use one-byte bodies with a clear Packet bit-error flag. +Each rejection emits one drop with unchanged copied PHY tags and one corruption notification. +A valid standalone CTS reaches the production FCS check, with and without a nonaggregate PHY indication. +This fixture tests the Rx entry point. The PHY reception fixture tests the real radio path. + +%file: AmpduRxBoundary.cc +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class RxBoundaryContention : public IContention +{ + public: + int corruptions = 0; + void startContention(int, simtime_t, simtime_t, simtime_t, ICallback *) override { ASSERT(false); } + bool isContentionInProgress() override { return false; } + void mediumStateChanged(bool) override {} + void corruptedFrameReceived() override { corruptions++; } +}; + +class AmpduRxBoundary : public Rx +{ + RxBoundaryContention contention; + mutable int fcsCalls = 0; + int drops = 0; + bool expectedHeader = false; + bool expectedComplete = false; + bool expectedAggregation = false; + Ptr retained; + + protected: + int numInitStages() const override { return 1; } + void initialize(int stage) override { + endNavTimer = new cMessage("NAV"); + endTxnavTimer = new cMessage("endTxnavTimer"); + address = MacAddress("02:00:00:00:00:01"); + registerContention(&contention); + subscribe(packetDroppedSignal, this); + scheduleAt(SIMTIME_ZERO, new cMessage("exercise")); + } + bool isFcsOk(Packet *packet) const override { + fcsCalls++; + return Rx::isFcsOk(packet); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *details) override { + ASSERT(signal == packetDroppedSignal); + auto packet = check_and_cast(object); + ASSERT(check_and_cast(details)->getReason() == INCORRECTLY_RECEIVED); + ASSERT(packet->getDataLength() == B(1) && !packet->hasBitError()); + ASSERT(fcsCalls == 0); + auto indication = packet->getTag(); + ASSERT(indication->getHeaderSuccessful() == expectedHeader); + ASSERT(indication->getContentComplete() == expectedComplete); + ASSERT(indication->getAggregation() == expectedAggregation); + ASSERT(packet->getTag()->getPower() == W(0.001)); + ASSERT(packet->getTag()->getMinimumSnir() == 42); + auto time = packet->getTag(); + ASSERT(time->getStartTime() == SIMTIME_ZERO && time->getEndTime() == SimTime(1, SIMTIME_US)); + retained = makeShared(*indication); + drops++; + } + void handleMessage(cMessage *message) override { + delete message; + for (int flags = 0; flags < 8; flags++) { + expectedHeader = flags & 1; + expectedComplete = flags & 2; + expectedAggregation = flags & 4; + if (expectedHeader && expectedComplete && !expectedAggregation) + continue; + auto source = new Packet("short-phy-result", makeShared(B(1))); + auto indication = source->addTag(); + indication->setHeaderSuccessful(expectedHeader); + indication->setContentComplete(expectedComplete); + indication->setAggregation(expectedAggregation); + source->addTag()->setPower(W(0.001)); + source->addTag()->setMinimumSnir(42); + auto time = source->addTag(); + time->setStartTime(SIMTIME_ZERO); + time->setEndTime(SimTime(1, SIMTIME_US)); + auto input = source->dup(); + delete source; + int previousDrops = drops; + int previousCorruptions = contention.corruptions; + ASSERT(!lowerFrameReceived(input)); + ASSERT(drops == previousDrops + 1 && contention.corruptions == previousCorruptions + 1); + ASSERT(fcsCalls == 0); + ASSERT(retained); + ASSERT(retained->getHeaderSuccessful() == expectedHeader); + } + for (bool tagged : {false, true}) { + auto frame = new Packet("valid-standalone"); + auto header = makeShared(); + header->setReceiverAddress(address); + frame->insertAtBack(header); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_DECLARED_CORRECT); + frame->insertAtBack(trailer); + if (tagged) { + auto indication = frame->addTag(); + indication->setHeaderSuccessful(true); + indication->setContentComplete(true); + indication->setAggregation(false); + } + int previousFcsCalls = fcsCalls; + ASSERT(lowerFrameReceived(frame)); + ASSERT(fcsCalls == previousFcsCalls + 1); + ASSERT(drops == 7 && contention.corruptions == 7); + delete frame; + } + EV << "Rx PHY boundary checks passed: seven rejections and two standalone acceptances\n"; + } +}; +Define_Module(AmpduRxBoundary); + +%file: test.ned +simple AmpduRxBoundary +{ + parameters: + @class(::AmpduRxBoundary); +} +network Test +{ + submodules: + rx: AmpduRxBoundary; +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = false +**.scalar-recording = false +**.vector-recording = false + +%contains: stdout +Rx PHY boundary checks passed: seven rejections and two standalone acceptances From 2ab47c31fa9fa499ca9a65ada0ef78f8776df5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 18 Oct 2026 02:29:59 +0200 Subject: [PATCH 14/24] ieee80211: add: prepare independent HT aggregate members Before this commit, the QoS service can prepare one DATA transmission but cannot retain independent previews beside canonical retry members. An aggregate selection must preserve sources until actual submission. After this commit, the HT policy selects borrowed members under supplied peer, window, mode, spacing, and length limits. The service gives each queued member an independent preview. Batch commitment validates every source before extraction. Canonical members retain their sequence numbers and deadlines. Forwarded previews use the local transmitter address without changing queued headers or ordinary A-MSDU construction. HCF rejects aggregate opt-in with a rate-control submodule at initialization. The default Basic policy remains disabled. Preparation fixtures check refusal, wrap, deadlines, limits, retained members, and both AARF rejection configurations. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-implicit-blockack --- .../aggregation/BasicMpduAggregationPolicy.h | 2 + .../aggregation/HtMpduAggregationPolicy.cc | 46 +++ .../mac/aggregation/HtMpduAggregationPolicy.h | 24 ++ .../aggregation/HtMpduAggregationPolicy.ned | 14 + .../mac/contract/IMpduAggregationPolicy.h | 16 + .../contract/IOriginatorQosMacDataService.h | 15 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 2 + .../originator/OriginatorQosMacDataService.cc | 95 ++++- .../originator/OriginatorQosMacDataService.h | 8 + .../ieee80211/mac/queue/InProgressFrames.cc | 9 + .../ieee80211/mac/queue/InProgressFrames.h | 1 + tests/module/Ieee80211AmpduPreparation_1.test | 373 ++++++++++++++++++ 12 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.cc create mode 100644 src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.h create mode 100644 src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.ned create mode 100644 tests/module/Ieee80211AmpduPreparation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMpduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMpduAggregationPolicy.h index 782034b515c..125c2c35dc5 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMpduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMpduAggregationPolicy.h @@ -21,6 +21,8 @@ class INET_API BasicMpduAggregationPolicy : public IMpduAggregationPolicy, publi public: virtual std::vector *computeAggregateFrames(std::vector *frames) override; + virtual bool isEnabled() const override { return false; } + virtual std::vector selectFrames(const std::vector& candidates, const MpduAggregationLimits& limits) const override { return {}; } }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.cc new file mode 100644 index 00000000000..1dc290d83a2 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.cc @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.h" + +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" + +namespace inet { +namespace ieee80211 { + +Define_Module(HtMpduAggregationPolicy); + +std::vector HtMpduAggregationPolicy::selectFrames(const std::vector& candidates, + const MpduAggregationLimits& limits) const +{ + auto mode = dynamic_cast(limits.mode); + if (!mode || mode->getDataMode()->getBandwidth() != MHz(20) || mode->isHtShortGuardInterval() || + mode->getPreambleMode()->getPreambleFormat() != physicallayer::Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED) + return {}; + std::vector selected; + MpduAggregation builder; + std::set sequences; + for (auto packet : candidates) { + auto header = dynamicPtrCast(packet->peekAtFront()); + if (!header || header->getType() != ST_DATA_WITH_QOS || header->getReceiverAddress().isMulticast() || + header->getReceiverAddress() != limits.receiver || header->getTransmitterAddress() != limits.transmitter || + header->getTid() != limits.tid || header->getAMsduPresent() || header->getMoreFragments() || + header->getFragmentNumber() != 0 || !header->getSequenceNumber().isSpecified()) + break; + int distance = OriginatorBlockAckAgreement::computeSequenceDistance(limits.windowStart, header->getSequenceNumber()); + if (distance >= limits.windowSize || !sequences.insert(header->getSequenceNumber().get()).second || + selected.size() >= size_t(std::min(64, limits.windowSize))) + break; + selected.push_back(packet); + B length = builder.computeAggregateLength(selected, mode, limits.minimumMpduStartSpacing); + if (length == B(0) || length > limits.maxLength || !mode->isPpduDurationValid(length)) { + selected.pop_back(); + break; + } + } + return selected; +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.h new file mode 100644 index 00000000000..e6d1e00ae85 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_HTMPDUAGGREGATIONPOLICY_H +#define __INET_HTMPDUAGGREGATIONPOLICY_H + +#include "inet/common/SimpleModule.h" +#include "inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h" + +namespace inet { +namespace ieee80211 { + +class INET_API HtMpduAggregationPolicy : public SimpleModule, public IMpduAggregationPolicy +{ + public: + virtual bool isEnabled() const override { return true; } + // The legacy consuming path has no negotiated context and cannot enable HT aggregation. + virtual std::vector *computeAggregateFrames(std::vector *frames) override { return nullptr; } + virtual std::vector selectFrames(const std::vector& candidates, const MpduAggregationLimits& limits) const override; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.ned b/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.ned new file mode 100644 index 00000000000..fe1492cbf09 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/aggregation/HtMpduAggregationPolicy.ned @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +package inet.linklayer.ieee80211.mac.aggregation; + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.contract.IMpduAggregationPolicy; + +// Selects independent HT20 long-GI members within the caller's negotiated receiver limits. +simple HtMpduAggregationPolicy extends SimpleModule like IMpduAggregationPolicy +{ + parameters: + @class(HtMpduAggregationPolicy); + @display("i=block/control"); +} diff --git a/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h index 0f2f8529bdd..ed29db863c5 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h @@ -12,14 +12,30 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { +namespace physicallayer { class IIeee80211Mode; } namespace ieee80211 { +struct INET_API MpduAggregationLimits +{ + MacAddress receiver; + MacAddress transmitter; + Tid tid = -1; + SequenceNumberCyclic windowStart; + int windowSize = 0; + B maxLength = B(0); + int minimumMpduStartSpacing = 0; // Encoded peer receiver limit, 0..7. + const physicallayer::IIeee80211Mode *mode = nullptr; +}; + class INET_API IMpduAggregationPolicy { public: virtual ~IMpduAggregationPolicy() {} virtual std::vector *computeAggregateFrames(std::vector *frames) = 0; + virtual bool isEnabled() const = 0; + // The result borrows an eligible prefix of the supplied candidates. No source state changes. + virtual std::vector selectFrames(const std::vector& candidates, const MpduAggregationLimits& limits) const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h index b27292f2a62..a1aa5359edc 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h @@ -12,6 +12,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h" namespace inet { namespace ieee80211 { @@ -29,6 +30,13 @@ struct INET_API PreparedQosData : public cObject Packet *releasePacket(); }; +/** Owns queued previews and borrows canonical members. The order is the proposed transmit order. */ +struct INET_API PreparedQosBatch +{ + std::vector> queued; + std::vector members; +}; + class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService { public: @@ -39,6 +47,13 @@ class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const = 0; virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const = 0; virtual bool hasAMsduAggregation() const = 0; + virtual bool hasAMpduAggregation() const = 0; + virtual std::unique_ptr prepareBatch(queueing::IPacketQueue *queue, + const std::vector& retained, const MpduAggregationLimits& limits) = 0; + virtual bool isBatchValid(const PreparedQosBatch& batch, simtime_t start) const = 0; + // Success transfers each new member to the caller; canonical members remain borrowed. + virtual bool commitPreparedBatch(PreparedQosBatch& batch, std::vector& committed) = 0; + virtual Packet *buildAggregate(const PreparedQosBatch& batch, const MpduAggregationLimits& limits) const = 0; // These QoS DATA methods separate reversible preparation from extraction and sequence assignment. virtual std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) = 0; virtual bool isPreparationValid(const PreparedQosData& preparation, simtime_t start) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 9f108f733c5..662f47d6aa8 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -83,6 +83,8 @@ void Hcf::initialize(int stage) } } else if (stage == INITSTAGE_LAST) { + if (check_and_cast(originatorDataService)->hasAMpduAggregation() && dataAndMgmtRateControl) + throw cRuntimeError("HT A-MPDU transmission requires fixed DATA rates without a rate-control submodule"); if (par("isBlockAckSupported").boolValue()) { EV_INFO << "Block Ack profile: " << (blockAckVariant == BlockAckVariant::BASIC ? "basic" : "compressed") << endl; if (blockAckVariant == BlockAckVariant::COMPRESSED) { diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index 80d2478f42d..0ff5f1e5fb4 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -12,6 +12,7 @@ #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/blockack/OriginatorBlockAckAgreement.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" @@ -108,11 +109,20 @@ std::unique_ptr OriginatorQosMacDataService::prepareFrame(queue } bool OriginatorQosMacDataService::isPreparationValid(const PreparedQosData& preparation, simtime_t start) const +{ + if (!preparation.packet) + return false; + auto header = preparation.packet->peekAtFront(); + return isPreparationValidForSequence(preparation, start, getNextSequenceNumber(header->getReceiverAddress(), header->getTid())); +} + +bool OriginatorQosMacDataService::isPreparationValidForSequence(const PreparedQosData& preparation, + simtime_t start, SequenceNumberCyclic sequence) const { if (!preparation.packet || !preparation.queue || preparation.sourceIds.empty()) return false; auto header = preparation.packet->peekAtFront(); - if (header->getSequenceNumber() != getNextSequenceNumber(header->getReceiverAddress(), header->getTid())) + if (header->getSequenceNumber() != sequence) return false; for (auto id : preparation.sourceIds) { Packet *source = nullptr; @@ -136,6 +146,11 @@ Packet *OriginatorQosMacDataService::commitPreparedFrame(PreparedQosData& prepar Enter_Method("commitPreparedFrame"); if (!isPreparationValid(preparation, simTime())) return nullptr; + return commitValidatedFrame(preparation); +} + +Packet *OriginatorQosMacDataService::commitValidatedFrame(PreparedQosData& preparation) +{ std::vector sources; std::vector trackedIds; simtime_t deadline = SIMTIME_MAX; @@ -191,6 +206,84 @@ Packet *OriginatorQosMacDataService::commitPreparedFrame(PreparedQosData& prepar return packet; } +std::unique_ptr OriginatorQosMacDataService::prepareBatch(queueing::IPacketQueue *queue, + const std::vector& retained, const MpduAggregationLimits& limits) +{ + Enter_Method("prepareBatch"); + if (!hasAMpduAggregation()) + return nullptr; + auto batch = std::make_unique(); + std::vector candidates = retained; + auto sequence = getNextSequenceNumber(limits.receiver, limits.tid); + for (int i = 0; i < queue->getNumPackets() && candidates.size() < size_t(std::min(64, limits.windowSize)); i++) { + auto source = queue->getPacket(i); + auto header = dynamicPtrCast(source->peekAtFront()); + if (!header || header->getType() != ST_DATA_WITH_QOS || header->getReceiverAddress() != limits.receiver || + header->getTid() != limits.tid || + header->getAMsduPresent() || header->getMoreFragments() || header->getFragmentNumber() != 0 || + source->getDataLength() > B(4095) || + (lifetimeHandler && lifetimeHandler->isExpired(source)) || (extractionEligible && !extractionEligible(source))) + continue; + int distance = OriginatorBlockAckAgreement::computeSequenceDistance(limits.windowStart, sequence); + if (distance >= limits.windowSize) + break; + auto preview = std::make_unique(); + preview->queue = queue; + preview->sourceIds.push_back(source->getId()); + preview->setPacket(source->dup()); + auto updated = preview->packet->removeAtFront(); + // IEEE Std 802.11-2024, 9.3.2.1.2: preview the local TA that Tx assigns before its FCS calculation. + updated->setTransmitterAddress(limits.transmitter); + updated->setSequenceNumber(sequence); + updated->setRetry(false); + updated->setAckPolicy(NORMAL_ACK); + preview->packet->insertAtFront(updated); + candidates.push_back(preview->packet.get()); + batch->queued.push_back(std::move(preview)); + sequence = sequence + 1; + } + batch->members = aMpduAggregationPolicy->selectFrames(candidates, limits); + batch->queued.erase(std::remove_if(batch->queued.begin(), batch->queued.end(), [&batch](const auto& preview) { + return std::find(batch->members.begin(), batch->members.end(), preview->packet.get()) == batch->members.end(); + }), batch->queued.end()); + return batch->members.empty() ? nullptr : std::move(batch); +} + +bool OriginatorQosMacDataService::isBatchValid(const PreparedQosBatch& batch, simtime_t start) const +{ + if (batch.members.empty()) + return false; + if (batch.queued.empty()) + return true; + auto first = batch.queued.front()->packet->peekAtFront(); + auto sequence = getNextSequenceNumber(first->getReceiverAddress(), first->getTid()); + for (const auto& preview : batch.queued) { + if (!preview->packet || std::find(batch.members.begin(), batch.members.end(), preview->packet.get()) == batch.members.end() || + !isPreparationValidForSequence(*preview, start, sequence)) + return false; + sequence = sequence + 1; + } + return true; +} + +bool OriginatorQosMacDataService::commitPreparedBatch(PreparedQosBatch& batch, std::vector& committed) +{ + Enter_Method("commitPreparedBatch"); + if (!isBatchValid(batch, simTime())) + return false; + // One synchronous validation precedes all extraction. Each member retains its own lifetime entry. + for (auto& preview : batch.queued) + committed.push_back(commitValidatedFrame(*preview)); + batch.queued.clear(); + return true; +} + +Packet *OriginatorQosMacDataService::buildAggregate(const PreparedQosBatch& batch, const MpduAggregationLimits& limits) const +{ + return aMpduAggregation ? aMpduAggregation->aggregateFrames(batch.members, limits.mode, + limits.minimumMpduStartSpacing, limits.maxLength) : nullptr; +} + Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected) { auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 3ef8ed28cec..4dcf2e4996e 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -53,6 +53,8 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService virtual std::vector *fragmentIfNeeded(Packet *frame); virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected); virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); + bool isPreparationValidForSequence(const PreparedQosData& preparation, simtime_t start, SequenceNumberCyclic sequence) const; + Packet *commitValidatedFrame(PreparedQosData& preparation); public: virtual void setFragmentationPermission(std::function permission) override { fragmentationPermitted = permission; } @@ -63,6 +65,12 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const override; virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const override; virtual bool hasAMsduAggregation() const override { return aMsduAggregationPolicy != nullptr; } + virtual bool hasAMpduAggregation() const override { return aMpduAggregationPolicy && aMpduAggregationPolicy->isEnabled(); } + virtual std::unique_ptr prepareBatch(queueing::IPacketQueue *queue, + const std::vector& retained, const MpduAggregationLimits& limits) override; + virtual bool isBatchValid(const PreparedQosBatch& batch, simtime_t start) const override; + virtual bool commitPreparedBatch(PreparedQosBatch& batch, std::vector& committed) override; + virtual Packet *buildAggregate(const PreparedQosBatch& batch, const MpduAggregationLimits& limits) const override; virtual std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) override; virtual bool isPreparationValid(const PreparedQosData& preparation, simtime_t start) const override; virtual Packet *commitPreparedFrame(PreparedQosData& preparation) override; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 190afe1d5c6..dd63aa884cb 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -69,6 +69,15 @@ bool InProgressFrames::isEligibleToTransmit(Packet *packet) ackHandler->isEligibleToTransmit(packet->peekAtFront()); } +std::vector InProgressFrames::getEligibleFrames() +{ + std::vector eligible; + for (auto packet : inProgressFrames) + if (isEligibleToTransmit(packet)) + eligible.push_back(packet); + return eligible; +} + void InProgressFrames::ensureHasFrameToTransmit() { if (!hasEligibleFrameToTransmit()) { diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 4804e562628..bd7264fffd3 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -63,6 +63,7 @@ class INET_API InProgressFrames : public SimpleModule virtual bool hasInProgressFrames() { ensureHasFrameToTransmit(); return hasEligibleFrameToTransmit(); } virtual std::vector getOutstandingFrames(); + std::vector getEligibleFrames(); virtual void clearDroppedFrames(); }; diff --git a/tests/module/Ieee80211AmpduPreparation_1.test b/tests/module/Ieee80211AmpduPreparation_1.test new file mode 100644 index 00000000000..93290338310 --- /dev/null +++ b/tests/module/Ieee80211AmpduPreparation_1.test @@ -0,0 +1,373 @@ +%description: +The production QoS data service prepares independent MPDUs through a production PacketQueue and HT aggregation policy. +Twelve deterministic cases check cancellation, refusal, commitment, sequence wrap, retained members, aggregate limits, and forwarded DATA previews. +Preparation preserves queued identities, owners, headers, deadlines, and sequence state. +Commitment preserves each deadline. Retained unsent and retry members preserve their numbers and Retry values. +Production InProgressFrames and QosAckHandler supply eligible retained members. +The fixture tests the service boundary. It does not test HCF aggregate arbitration or internal collision disposal. +Two postrun controls require HCF initialization to reject AARF with aggregate transmission, with and without fixed-rate overrides. + +%extraargs: -c Preparation -r 0..11 + +%postrun-command: python3 check-preparation.py + +%file: AmpduPreparation.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/queueing/queue/PacketQueue.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class AmpduPreparationService : public OriginatorQosMacDataService +{ + std::unique_ptr ownedLifetime; + protected: + void initialize() override { + OriginatorQosMacDataService::initialize(); + auto lifetime = SimTime(1, SIMTIME_S); + ownedLifetime = std::make_unique(lifetime, lifetime, lifetime, lifetime); + setLifetimeHandler(ownedLifetime.get(), {}); + setEligibility([](const Packet *) { return true; }, [](Packet *) { return true; }); + } + public: + EdcaTransmitLifetimeHandler *lifetime() const { return ownedLifetime.get(); } + int64_t allocations() const { return nextDataAllocationIndex; } + // Advance the real sequence owner without a queue extraction or allocation. + void advanceTo(MacAddress peer, Tid tid, int next) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(tid); + while (getNextSequenceNumber(peer, tid) != SequenceNumberCyclic(next)) + assignSequenceNumber(header); + } +}; +Define_Module(AmpduPreparationService); + +class AmpduPreparationTest : public cSimpleModule +{ + const MacAddress peer{"02:00:00:00:00:02"}; + const MacAddress sender{"02:00:00:00:00:01"}; + AmpduPreparationService *service = nullptr; + queueing::PacketQueue *queue = nullptr; + InProgressFrames *inProgress = nullptr; + QosAckHandler *ack = nullptr; + struct Source { + Packet *packet; + int64_t id; + cObject *owner; + simtime_t deadline; + }; + std::vector sources; + std::string operation() const { return par("operation").stdstringValue(); } + + Packet *enqueue(int index, bool nested = false) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(peer); + header->setTransmitterAddress(operation() == "forwarded" ? MacAddress("02:00:00:00:00:03") : sender); + header->setAddress3(peer); + header->setTid(0); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setAMsduPresent(nested); + auto packet = new Packet("queued-msdu", header); + packet->insertAtBack(makeShared(B(100))); + packet->insertAtBack(makeShared()); + packet->setArrivalTime(simTime()); + queue->enqueuePacket(packet); + auto deadline = operation() == "expired" ? SimTime(2, SIMTIME_MS) : SimTime(100 + index, SIMTIME_MS); + service->lifetime()->registerFragments({packet}, deadline); + sources.push_back({packet, packet->getId(), packet->getOwner(), deadline}); + return packet; + } + void checkSources(int nextSequence, int64_t allocations) const { + ASSERT(queue->getNumPackets() == int(sources.size())); + ASSERT(service->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(nextSequence)); + ASSERT(service->allocations() == allocations); + for (size_t i = 0; i < sources.size(); i++) { + const auto& source = sources[i]; + ASSERT(queue->getPacket(i) == source.packet && source.packet->getId() == source.id); + ASSERT(source.packet->getOwner() == source.owner); + ASSERT(source.packet->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(source.packet->peekAtFront()->getTransmitterAddress() == + (operation() == "forwarded" ? MacAddress("02:00:00:00:00:03") : sender)); + ASSERT(!source.packet->findTag()); + ASSERT(service->lifetime()->findFrame(source.id) == source.packet); + ASSERT(service->lifetime()->getDeadline(source.packet) == source.deadline); + } + } + void dispose(Packet *packet) { + service->lifetime()->forgetFrame(packet->getId()); + take(packet); + delete packet; + } + MpduAggregationLimits limits() const { + MpduAggregationLimits result; + result.receiver = peer; + result.transmitter = sender; + result.tid = 0; + result.windowStart = SequenceNumberCyclic(0); + result.windowSize = 64; + result.maxLength = B(65535); + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() == 7 && mode->getDataMode()->getBandwidth() == MHz(20) && !mode->isHtShortGuardInterval()) + result.mode = mode; + } + ASSERT(result.mode); + return result; + } + std::vector retainTwo() { + for (int i = 0; i < 2; i++) { + enqueue(i); + auto preview = service->prepareFrame(queue, false); + ASSERT(preview); + auto frame = service->commitPreparedFrame(*preview); + ASSERT(frame); + sources.clear(); + inProgress->adoptPreparedFrame(frame); + if (i == 1) { + ack->processTransmittedDataOrMgmtFrame(frame->peekAtFront()); + ack->processFailedFrame(frame->peekAtFront()); + auto header = frame->removeAtFront(); + header->setRetry(true); + frame->insertAtFront(header); + } + } + auto retained = inProgress->getEligibleFrames(); + ASSERT(retained.size() == 2); + ASSERT(ack->getQoSDataAckStatus(retained[0]->peekAtFront()) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ack->getQoSDataAckStatus(retained[1]->peekAtFront()) == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED); + return retained; + } + + public: + AmpduPreparationTest() : cSimpleModule(65536) {} + protected: + void activity() override { + wait(SimTime(1, SIMTIME_MS)); + service = check_and_cast(getModuleByPath("^.service")); + queue = check_and_cast(getModuleByPath("^.queue")); + inProgress = check_and_cast(getModuleByPath("^.inProgress")); + ack = check_and_cast(getModuleByPath("^.ack")); + inProgress->setLifetimeHandler(service->lifetime()); + ASSERT(service->hasAMpduAggregation() && service->hasAMsduAggregation()); + std::vector retained; + if (operation() == "retained") + retained = retainTwo(); + int start = operation() == "wrap" ? 4095 : (retained.empty() ? 0 : 2); + if (operation() == "wrap") + service->advanceTo(peer, 0, start); + auto bounds = limits(); + bounds.windowStart = SequenceNumberCyclic(operation() == "wrap" ? 4095 : 0); + int count = operation() == "count" ? 66 : 3; + for (int i = 0; i < count; i++) + enqueue(10 + i, operation() == "nested" && i == 1); + int expected = count; + if (operation() == "window") { + bounds.windowSize = 2; + expected = 2; + } + if (operation() == "count") expected = 64; + if (operation() == "nested") expected = 2; + if (operation() == "bytes") { + MpduAggregation builder; + bounds.maxLength = builder.computeAggregateLength({sources[0].packet, sources[1].packet}, bounds.mode, 0) - B(1); + expected = 1; + } + auto allocationBase = service->allocations(); + auto noFit = bounds; + noFit.maxLength = B(0); + ASSERT(!service->prepareBatch(queue, retained, noFit)); + checkSources(start, allocationBase); + auto batch = service->prepareBatch(queue, retained, bounds); + ASSERT(batch && batch->queued.size() == size_t(expected)); + ASSERT(batch->members.size() == retained.size() + expected); + for (size_t i = 0; i < retained.size(); i++) ASSERT(batch->members[i] == retained[i]); + for (int i = 0; i < expected; i++) { + auto& preview = batch->queued[i]; + ASSERT(!preview->aggregate && preview->sourceIds.size() == 1); + ASSERT(preview->sourceIds[0] == sources[operation() == "nested" && i == 1 ? 2 : i].id); + auto header = preview->packet->peekAtFront(); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic((start + i) % 4096)); + ASSERT(header->getTransmitterAddress() == sender); + ASSERT(!header->getAMsduPresent() && !header->getRetry()); + } + checkSources(start, allocationBase); + auto aggregate = service->buildAggregate(*batch, bounds); + ASSERT(aggregate && aggregate->getDataLength() <= bounds.maxLength); + ASSERT(check_and_cast(bounds.mode)->isPpduDurationValid(aggregate->getDataLength())); + delete aggregate; + ASSERT(service->isBatchValid(*batch, simTime())); + wait(SimTime(2, SIMTIME_MS)); + checkSources(start, allocationBase); + std::vector committed; + if (operation() == "staleSource") { + auto packet = sources[0].packet; + queue->removePacket(packet); + dispose(packet); + sources.erase(sources.begin()); + } + if (operation() == "staleSequence") { + service->advanceTo(peer, 0, start + 1); + start++; + } + bool refused = operation() == "staleSource" || operation() == "staleSequence" || operation() == "expired"; + if (refused) { + ASSERT(!service->isBatchValid(*batch, simTime())); + ASSERT(!service->commitPreparedBatch(*batch, committed)); + ASSERT(committed.empty()); + checkSources(start, allocationBase); + } + else if (operation() != "cancel") { + std::vector deadlines; + std::vector previews; + for (const auto& preview : batch->queued) { + previews.push_back(preview->packet.get()); + deadlines.push_back(service->lifetime()->getDeadline(service->lifetime()->findFrame(preview->sourceIds[0]))); + } + ASSERT(service->commitPreparedBatch(*batch, committed)); + ASSERT(batch->queued.empty() && committed == previews); + ASSERT(committed.size() == size_t(expected)); + for (int i = 0; i < expected; i++) { + auto frame = committed[i]; + ASSERT(frame->getOwner() == service); + ASSERT(frame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic((start + i) % 4096)); + ASSERT(frame->peekAtFront()->getTransmitterAddress() == sender); + ASSERT(frame->getTag()->getAllocationIndex() == allocationBase + i); + ASSERT(service->lifetime()->getDeadline(frame) == deadlines[i]); + } + ASSERT(service->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic((start + expected) % 4096)); + ASSERT(queue->getNumPackets() == count - expected); + if (operation() == "nested") ASSERT(queue->getPacket(0)->peekAtFront()->getAMsduPresent()); + } + batch.reset(); + if (operation() == "cancel") checkSources(start, allocationBase); + for (size_t i = 0; i < retained.size(); i++) { + auto frame = retained[i]; + ASSERT(inProgress->getFrames(i) == frame); + auto header = frame->peekAtFront(); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic(i)); + ASSERT(header->getRetry() == (i == 1)); + ASSERT(frame->getTag()->getAllocationIndex() == int64_t(i)); + ASSERT(service->lifetime()->getDeadline(frame) == SimTime(100 + i, SIMTIME_MS)); + ASSERT(ack->getQoSDataAckStatus(header) == (i == 0 ? QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED : QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED)); + service->lifetime()->forgetFrame(frame->getId()); + } + for (auto frame : committed) dispose(frame); + while (!queue->isEmpty()) { + auto frame = queue->dequeuePacket(); + dispose(frame); + } + ASSERT(service->lifetime()->getNumFrames() == 0); + std::cout << "A-MPDU preparation passed: " << operation() << "\n"; + endSimulation(); + } +}; +Define_Module(AmpduPreparationTest); + +%file: test.ned +import inet.linklayer.ieee80211.mac.originator.OriginatorQosMacDataService; +import inet.linklayer.ieee80211.mac.originator.QosAckHandler; +import inet.linklayer.ieee80211.mac.queue.InProgressFrames; +import inet.queueing.queue.PacketQueue; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module AmpduPreparationService extends OriginatorQosMacDataService { parameters: @class(::AmpduPreparationService); } +simple AmpduPreparationTest { parameters: string operation; @class(::AmpduPreparationTest); } +network PreparationNetwork { + submodules: + service: AmpduPreparationService; + queue: PacketQueue; + ack: QosAckHandler; + inProgress: InProgressFrames { + pendingQueueModule = "^.queue"; + originatorMacDataServiceModule = "^.service"; + ackHandlerModule = "^.ack"; + } + test: AmpduPreparationTest; + connections allowunconnected: +} +network AarfNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; +} + +%inifile: omnetpp.ini +[General] +ned-path = .;../../../../src;../../lib +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false + +[Config Preparation] +network = PreparationNetwork +*.test.operation = ${operation="cancel","commit","staleSource","staleSequence","expired","wrap","retained","bytes","window","count","nested","forwarded"} +*.service.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +*.service.msduAggregationPolicy.subframeNumThreshold = 2 +*.service.msduAggregationPolicy.maxAMsduSize = 1000 +*.service.fragmentationPolicy.typename = "" + +[Config Aarf] +network = AarfNetwork +*.host.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.host.wlan[0].mac.qosStation = true +*.host.wlan[0].mac.hcf.rateControl.typename = "AarfRateControl" +*.host.wlan[0].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +*.host.wlan[0].mgmt.typename = "Ieee80211MgmtAdhoc" +*.host.wlan[0].agent.typename = "" +*.host.wlan[0].radio.receiver.bandwidth = 20MHz +*.host.mobility.initFromDisplayString = false +*.host.mobility.initialX = 0m +*.host.mobility.initialY = 0m +*.host.mobility.initialZ = 0m +*.host.mobility.constraintAreaMinX = 0m +*.host.mobility.constraintAreaMinY = 0m +*.host.mobility.constraintAreaMinZ = 0m +*.host.mobility.constraintAreaMaxX = 100m +*.host.mobility.constraintAreaMaxY = 100m +*.host.mobility.constraintAreaMaxZ = 0m + +[Config AarfFixedOverride] +extends = Aarf +*.host.wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host.wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +*.host.wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 + +%file: check-preparation.py +from pathlib import Path +import os +import subprocess + +text = Path("test.out").read_text() +assert "Run statistics: total 12, successful 12" in text +operations = ("cancel", "commit", "staleSource", "staleSequence", "expired", "wrap", "retained", "bytes", "window", "count", "nested", "forwarded") +for operation in operations: + assert text.count(f"A-MPDU preparation passed: {operation}\n") == 1, operation +diagnostic = "HT A-MPDU transmission requires fixed DATA rates without a rate-control submodule" +executables = [path for path in Path.cwd().glob("Ieee80211AmpduPreparation_1*") if path.is_file() and os.access(path, os.X_OK)] +assert len(executables) == 1, executables +for config in ("Aarf", "AarfFixedOverride"): + command = [str(executables[0]), "-u", "Cmdenv", "--check-signals=false", "-n", "../../../../src:.:../../lib", "omnetpp.ini", "_defaults.ini", "-c", config, "-r", "0"] + result = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + Path(f"control-{config}.out").write_text(result.stdout) + assert result.returncode != 0, (config, result.stdout) + assert diagnostic in result.stdout, (config, result.stdout) +print("Eleven preparation cases and both AARF initialization controls passed.") + +%contains: stdout +A-MPDU preparation passed: From c94778c89c8bf8b1060ab25e9e72a2461a6fb909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 13:46:14 +0100 Subject: [PATCH 15/24] doc: comment: explain HT aggregate preparation contracts Before this commit, extension authors lack documentation for HT construction and reversible batch preparation. The MAC still cannot transmit aggregates. After this commit, the migration guide describes receiver spacing, builders that retain sources, policy selection, batch commitment, and safe Rx rejection. The release note states the available capabilities and limits. This commit changes no runtime behavior. Change: doc | comment | - | ieee80211-implicit-blockack --- .../development/ieee80211-implicit-blockack.rst | 17 +++++++++++++++++ .../ieee80211-implicit-blockack.txt | 7 +++++++ 2 files changed, 24 insertions(+) create mode 100644 doc/src/migration-guide/development/ieee80211-implicit-blockack.rst create mode 100644 doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt diff --git a/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst b/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst new file mode 100644 index 00000000000..f4b6b7f05a9 --- /dev/null +++ b/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst @@ -0,0 +1,17 @@ +IEEE 802.11 implicit Block Ack extension points +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default MPDU policy leaves aggregate transmission disabled. ``HtMpduAggregationPolicy`` supplies selection for independent HT20 long-GI members with fixed DATA rates. Local aggregate transmission remains unavailable. Custom MPDU policies implement ``isEnabled()`` and ``selectFrames()``. The caller supplies receiver limits and one retained DATA mode. A policy returns borrowed member references without queue extraction or state changes. + +Custom QoS data services implement batch preparation, validation, commitment, and construction through ``IOriginatorQosMacDataService``. Each new member owns one reversible preview. The batch borrows canonical members from the in-progress queue. Commitment validates every queued source before the first extraction. Failed preparation preserves sequence state and independent deadlines. + +IEEE 802.11 HT aggregate construction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The MIB parameter ``htMinimumMpduStartSpacing`` supplies the encoded receiver limit from zero through seven. Zero preserves the default with no spacing restriction. The remaining values represent 0.25, 0.5, 1, 2, 4, 8, and 16 microseconds. Full and simplified management preserve the receiver's value in each negotiated direction. The negotiated transmit direction stores the peer receiver's value. + +Custom ``IMpduAggregation`` implementations must supply two HT operations. ``computeAggregateLength()`` previews the PSDU length at the supplied mode and spacing. Zero means an empty batch, unsupported mode, invalid member length, or excessive HT PSDU length. The new ``aggregateFrames()`` overload accepts source references, the mode, encoded spacing, and the peer byte limit. It returns ``nullptr`` when construction exceeds a limit, including either HT duration limit. These operations retain all source Packets and their state. + +The existing pointer overload retains its consuming contract. The supplied builder copies region tags from each source's data range to the member's position. It inserts alignment padding and zero-length delimiters between members as necessary. The final member has no padding. A singleton retains its delimiter, so HT-SIG byte conversion preserves its aggregate identity. These construction operations do not enable aggregate selection or implicit Block Ack responses. + +The default ``Rx`` refuses aggregate containers before standalone FCS or MAC header checks. Custom receive modules must keep these containers separate from standalone frame parsing. Member acceptance and implicit responses remain unavailable at this boundary. diff --git a/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt b/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt new file mode 100644 index 00000000000..f2e0e97d295 --- /dev/null +++ b/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt @@ -0,0 +1,7 @@ +IEEE 802.11 HT aggregate preparation + +HT capability exchange now retains the peer receiver's minimum MPDU start spacing. The MIB parameter htMinimumMpduStartSpacing selects the encoded local receiver limit from zero through seven. Zero preserves the default with no spacing restriction. + +The HT aggregate builder checks receiver byte limits, spacing, PPDU duration, and L-SIG length. It retains each source Packet. The QoS service can prepare independent members before it extracts queued sources. Each member retains its sequence number and deadline. Failed preparation preserves queued sources. + +Custom aggregation policies, aggregate builders, and QoS data services require the migration changes. The default MPDU policy leaves aggregation disabled. Local aggregate transmission and implicit Block Ack responses remain unavailable at this boundary. Rx refuses failed aggregate containers before standalone MAC access. From c0fda558b65a0c7960cda7afc67f9561233ade4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 00:29:28 +0100 Subject: [PATCH 16/24] ieee80211: fix: preserve PHY errors through MPDU extraction Before this commit, byte conversion can omit original chunk error flags during A-MPDU traversal. A converted delimiter or member can therefore appear correct despite a cached PHY failure in its original region. After this commit, traversal checks the original chunk region before it accepts a delimiter. It marks an extracted member incorrect when the original region failed. The checks follow nested sequence and slice chunks without another PHY error draw. For example, an incorrect raw DATA region contains bytes that resemble valid delimiters. The original region must prevent their acceptance. The added Ieee80211MpduDeaggregation_1 assertions fail on the parent and check this boundary after the fix. They also cover front offsets and errors across chunk boundaries. These checks establish extraction semantics, not a complete MAC exchange. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.change.fix | test | ieee80211-implicit-blockack --- .../mac/aggregation/MpduDeaggregation.cc | 41 ++++++++++++++++++- tests/unit/Ieee80211MpduDeaggregation_1.test | 34 +++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc index ca3a0cf6f77..ea6c5458b76 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc @@ -7,11 +7,37 @@ #include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" +#include "inet/common/packet/chunk/SequenceChunk.h" +#include "inet/common/packet/chunk/SliceChunk.h" + namespace inet { namespace ieee80211 { Register_Class(MpduDeaggregation); +static bool isRegionCorrect(const Ptr& chunk, b offset, b length) +{ + if (chunk->Chunk::isIncorrect()) + return false; + if (auto sequence = dynamicPtrCast(chunk)) { + b position(0); + for (const auto& child : sequence->getChunks()) { + b end = position + child->getChunkLength(); + b first = std::max(offset, position); + b last = std::min(offset + length, end); + if (first < last && !isRegionCorrect(child, first - position, last - first)) + return false; + position = end; + if (position >= offset + length) + break; + } + return true; + } + if (auto slice = dynamicPtrCast(chunk)) + return isRegionCorrect(slice->getChunk(), slice->getOffset() + offset, length); + return chunk->isCorrect(); +} + bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, const std::function&, B, bool)>& visitDelimiter, const std::function&, B)>& visitMpdu) @@ -21,6 +47,10 @@ bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, if (length == B(0)) return true; int flags = Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED; + // Byte conversion can omit source flags. Keep the original region as the error authority. + const auto& original = packet->peekAll(flags); + b originalOffset = packet->getFrontOffset() + offset; + bool allCorrect = original->isCorrect(); Packet aggregate("mpdu-aggregate", packet->peekDataAt(offset, length, flags)); bool complete = true; // IEEE Std 802.11-2024, 9.7.1: HT uses a 12-bit MPDU length and four-octet subframe alignment. @@ -35,7 +65,8 @@ bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, Chunk::PF_ALLOW_REINTERPRETATION); bool valid = delimiter->isCorrect() && delimiter->isComplete() && delimiter->isProperlyRepresented() && delimiter->getLength() >= 0 && delimiter->getLength() <= 4095 && - B(delimiter->getLength()) <= aggregate.getDataLength(); + B(delimiter->getLength()) <= aggregate.getDataLength() && + (allCorrect || isRegionCorrect(original, originalOffset + position, B(4))); visitDelimiter(delimiter, position, valid); if (!valid) { // IEEE Std 802.11-2024, informative Annex O.2: examine candidates four octets apart. @@ -45,7 +76,13 @@ bool MpduDeaggregation::visitSubframes(const Packet *packet, B offset, B length, B memberLength(delimiter->getLength()); if (memberLength == B(0)) continue; - const auto& mpdu = aggregate.popAtFront(memberLength, flags); + Ptr mpdu = aggregate.popAtFront(memberLength, flags); + if (mpdu->isCorrect() && !allCorrect && + !isRegionCorrect(original, originalOffset + position + B(4), memberLength)) { + auto incorrect = mpdu->dupShared(); + incorrect->markIncorrect(); + mpdu = incorrect; + } visitMpdu(mpdu, position + B(4)); if (aggregate.getDataLength() == b(0)) break; diff --git a/tests/unit/Ieee80211MpduDeaggregation_1.test b/tests/unit/Ieee80211MpduDeaggregation_1.test index 3502416f3a9..6190a375fbc 100644 --- a/tests/unit/Ieee80211MpduDeaggregation_1.test +++ b/tests/unit/Ieee80211MpduDeaggregation_1.test @@ -5,6 +5,7 @@ Cover lengths, alignment, truncation, final padding, and delimiter error flags. %includes: #include #include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/chunk/SequenceChunk.h" #include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" using namespace inet; @@ -122,6 +123,39 @@ for (int prefixLength : {0, 6}) { ASSERT(!std::get<0>(last) && std::get<3>(last) && std::get<1>(last) == B(108) && std::get<2>(last) == B(20)); } ASSERT(!std::get<0>(incorrectResult.second.back()) && std::get<3>(incorrectResult.second.back())); +Packet originalRaw("original-raw"); +appendMember(originalRaw, 20); +appendMember(originalRaw, 20); +auto rawBytes = originalRaw.peekAllAsBytes()->getBytes(); +auto failedBytes = makeShared(rawBytes); +failedBytes->markIncorrect(); +Packet failedRaw("failed-raw", failedBytes); +for (int front : {0, 4}) { + failedRaw.setFrontOffset(B(front)); + auto result = inspect(&failedRaw, B(0), B(failedRaw.getDataLength())); + ASSERT(!result.first); + for (const auto& entry : result.second) + ASSERT(std::get<0>(entry) && !std::get<3>(entry)); +} +Packet mixed("mixed", failedBytes); +appendMember(mixed, 20); +auto mixedResult = inspect(&mixed, B(0), B(mixed.getDataLength())); +ASSERT(!mixedResult.first); +ASSERT(!std::get<0>(mixedResult.second.back()) && std::get<1>(mixedResult.second.back()) == B(52)); +auto badRegion = makeShared(std::vector(rawBytes.begin() + 4, rawBytes.begin() + 28)); +badRegion->markIncorrect(); +auto regions = makeShared(); +regions->setChunks({makeShared(std::vector(rawBytes.begin(), rawBytes.begin() + 4)), + badRegion, makeShared(std::vector(rawBytes.begin() + 28, rawBytes.end()))}); +Packet crossing("crossing", regions); +int failedMembers = 0; +bool complete = MpduDeaggregation::visitSubframes(&crossing, B(0), B(48), + [](const auto&, B, bool) {}, + [&](const auto& mpdu, B offset) { + ASSERT(offset == B(4) && mpdu->isIncorrect()); + failedMembers++; + }); +ASSERT(!complete && failedMembers == 1); EV << "MPDU traversal checks passed\n"; %contains: stdout From 6c6cc1809b29a7fe41df0f6dbcf114818454f383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 00:31:07 +0100 Subject: [PATCH 17/24] ieee80211: refactor: construct Compressed responses without a BAR Before this commit, the bitmap builder and response-mode calculation require a received BAR, although their calculations use the agreement and eliciting mode. The later implicit response needs those calculations without an explicit BAR. After this commit, the recipient procedure builds the bitmap from the agreement, peer, and TID. Rate selection calculates the Compressed response mode from the eliciting mode. Existing explicit BAR paths call these helpers with the same inputs as before. For example, an explicit Compressed BAR still produces the same receipt bitmap and response mode. The Compressed record, rate, response-mode, and exchange fixtures serve as compatibility controls. This commit enables no aggregate sender or recipient receive path. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | refactor | test migration | ieee80211-implicit-blockack --- .../blockack/RecipientBlockAckProcedure.cc | 30 ++++--- .../mac/blockack/RecipientBlockAckProcedure.h | 1 + .../mac/contract/IQosRateSelection.h | 1 + .../contract/IRecipientBlockAckProcedure.h | 1 + .../mac/rateselection/QosRateSelection.cc | 84 ++++++++++--------- .../mac/rateselection/QosRateSelection.h | 1 + .../Ieee80211HcfMultipleProtection_1.test | 3 + .../Ieee80211HcfSingleProtection_1.test | 3 + .../Ieee80211SingleProtectionGroup_1.test | 1 + tests/module/Ieee80211Txnav_1.test | 1 + tests/module/Ieee80211VhtHtSupport_1.test | 3 + tests/unit/Ieee80211ResponseTimeout_1.test | 1 + 12 files changed, 78 insertions(+), 52 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index 61d1213020c..9771a17cc3c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -74,23 +74,29 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr return blockAck; } else if (auto compressed = dynamicPtrCast(blockAckReq)) { - ASSERT(agreement != nullptr); - auto blockAck = makeShared(); - auto record = agreement->getBlockAckRecord(); - auto start = record->getWindowStart(); - BitVector bitmap(0, 64); - for (int i = 0; i < 64; i++) - bitmap.setBit(i, record->getCompressedAckState(start + i)); - blockAck->setBlockAckBitmap(bitmap); - blockAck->setReceiverAddress(compressed->getTransmitterAddress()); - blockAck->setStartingSequenceNumber(start); - blockAck->setTidInfo(compressed->getTidInfo()); - return blockAck; + return buildCompressedBlockAck(compressed->getTransmitterAddress(), compressed->getTidInfo(), agreement); } else throw cRuntimeError("Unsupported Block Ack Request"); } +Ptr RecipientBlockAckProcedure::buildCompressedBlockAck(MacAddress peer, Tid tid, + RecipientBlockAckAgreement *agreement) +{ + ASSERT(agreement != nullptr); + auto blockAck = makeShared(); + auto record = agreement->getBlockAckRecord(); + auto start = record->getWindowStart(); + BitVector bitmap(0, 64); + for (int i = 0; i < 64; i++) + bitmap.setBit(i, record->getCompressedAckState(start + i)); + blockAck->setBlockAckBitmap(bitmap); + blockAck->setReceiverAddress(peer); + blockAck->setStartingSequenceNumber(start); + blockAck->setTidInfo(tid); + return blockAck; +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h index 3d0e2899f1f..5af43038836 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h @@ -26,6 +26,7 @@ class INET_API RecipientBlockAckProcedure : public IRecipientBlockAckProcedure virtual const Ptr buildBlockAck(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement); public: + Ptr buildCompressedBlockAck(MacAddress peer, Tid tid, RecipientBlockAckAgreement *agreement) override; virtual void processReceivedBlockAckReq(Packet *blockAckPacket, const Ptr& blockAckReq, IRecipientQosAckPolicy *ackPolicy, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, IProcedureCallback *callback) override; virtual void processTransmittedBlockAck(const Ptr& blockAck) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h index 10c35f613a0..59f75f69e7e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h @@ -32,6 +32,7 @@ class INET_API IQosRateSelection virtual const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame, ResponseQueryRole role) = 0; virtual const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& dataOrMgmtHeader, ResponseQueryRole role) = 0; virtual const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq, ResponseQueryRole role) = 0; + virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *elicitingMode) = 0; // HCF derives startsTxop from the current frame sequence before this query. virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& blockAckReq, bool startsTxop) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckProcedure.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckProcedure.h index 9669adfbf60..b347b95c622 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckProcedure.h @@ -23,6 +23,7 @@ class INET_API IRecipientBlockAckProcedure virtual void processReceivedBlockAckReq(Packet *packet, const Ptr& blockAckReq, IRecipientQosAckPolicy *ackPolicy, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, IProcedureCallback *callback) = 0; virtual void processTransmittedBlockAck(const Ptr& blockAck) = 0; + virtual Ptr buildCompressedBlockAck(MacAddress peer, Tid tid, RecipientBlockAckAgreement *agreement) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 5c65ba9d116..283bbe42d6b 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -145,51 +145,55 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet return getPeerCompatibleResponseMode(blockAckReq, mode, role); } else if (dynamicPtrCast(blockAckReq)) { - auto received = getMode(packet, blockAckReq); - auto referenceRate = received->getNonHtReferenceRate(); - auto receivedClass = received->getModulationClass(); - bool highThroughput = receivedClass == IIeee80211Mode::ModulationClass::HT || receivedClass == IIeee80211Mode::ModulationClass::VHT; - if (!std::isfinite(referenceRate.get())) - throw cRuntimeError("Compressed Block Ack has no non-HT reference rate for the BAR mode"); - const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; - auto matchesClass = [received, receivedClass, highThroughput](const IIeee80211Mode *candidate) { - auto modulation = candidate->getModulationClass(); - return highThroughput ? modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM : - modulation == receivedClass && candidate->getLegacyPreambleType() == received->getLegacyPreambleType(); - }; - const IIeee80211Mode *primary = nullptr; - // IEEE Std 802.11-2024, 10.6.6.5.2: prefer a qualifying BSS basic rate, then a mandatory rate. - for (bool useBasic : {true, false}) { - if (useBasic && (!basic || !basic->known)) - continue; // INET fallback for unknown BSS state uses the mandatory rate set. - for (int i = 0; i < modeSet->getNumModes(); i++) { - auto candidate = modeSet->getMode(i); - auto rate = candidate->getDataMode()->getNetBitrate(); - bool eligible = useBasic ? basic->legacyRates.count(rate) != 0 : modeSet->isMandatory(i); - if (eligible && matchesClass(candidate) && rate <= referenceRate && - (!primary || rate > primary->getDataMode()->getNetBitrate())) - primary = candidate; - } - if (primary) - break; - } - if (!primary) - throw cRuntimeError("Compressed Block Ack has no legal non-HT response mode"); - if (!responseBlockAckFrameMode || responseBlockAckFrameMode == primary) - return primary; - auto alternate = responseBlockAckFrameMode; - auto rate = alternate->getDataMode()->getNetBitrate(); - bool basicOrMandatory = (basic && basic->known && basic->legacyRates.count(rate)) || modeSet->getIsMandatory(alternate); - // IEEE Std 802.11-2024, 10.6.6.5.4: an alternate preserves modulation and actual response duration. - if (!matchesClass(alternate) || alternate->getModulationClass() != primary->getModulationClass() || !basicOrMandatory || - alternate->getDuration(LENGTH_COMPRESSED_BLOCKACK) != primary->getDuration(LENGTH_COMPRESSED_BLOCKACK)) - throw cRuntimeError("responseBlockAckFrameBitrate is not a legal alternate Compressed Block Ack response rate"); - return alternate; + return computeCompressedBlockAckResponseMode(getMode(packet, blockAckReq)); } else throw cRuntimeError("Unknown BlockAckReq frame type"); } +const IIeee80211Mode *QosRateSelection::computeCompressedBlockAckResponseMode(const IIeee80211Mode *received) +{ + auto referenceRate = received->getNonHtReferenceRate(); + auto receivedClass = received->getModulationClass(); + bool highThroughput = receivedClass == IIeee80211Mode::ModulationClass::HT || receivedClass == IIeee80211Mode::ModulationClass::VHT; + if (!std::isfinite(referenceRate.get())) + throw cRuntimeError("Compressed Block Ack has no non-HT reference rate for the BAR mode"); + const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; + auto matchesClass = [received, receivedClass, highThroughput](const IIeee80211Mode *candidate) { + auto modulation = candidate->getModulationClass(); + return highThroughput ? modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM : + modulation == receivedClass && candidate->getLegacyPreambleType() == received->getLegacyPreambleType(); + }; + const IIeee80211Mode *primary = nullptr; + // IEEE Std 802.11-2024, 10.6.6.5.2: prefer a qualifying BSS basic rate, then a mandatory rate. + for (bool useBasic : {true, false}) { + if (useBasic && (!basic || !basic->known)) + continue; // INET fallback for unknown BSS state uses the mandatory rate set. + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto candidate = modeSet->getMode(i); + auto rate = candidate->getDataMode()->getNetBitrate(); + bool eligible = useBasic ? basic->legacyRates.count(rate) != 0 : modeSet->isMandatory(i); + if (eligible && matchesClass(candidate) && rate <= referenceRate && + (!primary || rate > primary->getDataMode()->getNetBitrate())) + primary = candidate; + } + if (primary) + break; + } + if (!primary) + throw cRuntimeError("Compressed Block Ack has no legal non-HT response mode"); + if (!responseBlockAckFrameMode || responseBlockAckFrameMode == primary) + return primary; + auto alternate = responseBlockAckFrameMode; + auto rate = alternate->getDataMode()->getNetBitrate(); + bool basicOrMandatory = (basic && basic->known && basic->legacyRates.count(rate)) || modeSet->getIsMandatory(alternate); + // IEEE Std 802.11-2024, 10.6.6.5.4: an alternate preserves modulation and actual response duration. + if (!matchesClass(alternate) || alternate->getModulationClass() != primary->getModulationClass() || !basicOrMandatory || + alternate->getDuration(LENGTH_COMPRESSED_BLOCKACK) != primary->getDuration(LENGTH_COMPRESSED_BLOCKACK)) + throw cRuntimeError("responseBlockAckFrameBitrate is not a legal alternate Compressed Block Ack response rate"); + return alternate; +} + const IIeee80211Mode *QosRateSelection::computeCompressedBlockAckReqMode(Packet *packet, const Ptr& blockAckReq, bool startsTxop) { diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index c0ecc61f8a1..ff499b97274 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -76,6 +76,7 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual bool isControlResponseFrame(const Ptr& header, TxopProcedure *txopProcedure); public: + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *elicitingMode) override; // A control response frame is a control frame that is transmitted as a response to the reception of a frame a SIFS // time after the PPDU containing the frame that elicited the response, e.g. a CTS in response to an RTS // reception, an ACK in response to a DATA reception, a BlockAck in response to a BlockAckReq reception. In diff --git a/tests/module/Ieee80211HcfMultipleProtection_1.test b/tests/module/Ieee80211HcfMultipleProtection_1.test index 852333eb985..a2ea0cc1346 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_1.test +++ b/tests/module/Ieee80211HcfMultipleProtection_1.test @@ -112,6 +112,9 @@ class ProtectionRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } }; class ProtectionHcf : public Hcf { diff --git a/tests/module/Ieee80211HcfSingleProtection_1.test b/tests/module/Ieee80211HcfSingleProtection_1.test index 1ef31e67f3d..fe64ee85701 100644 --- a/tests/module/Ieee80211HcfSingleProtection_1.test +++ b/tests/module/Ieee80211HcfSingleProtection_1.test @@ -94,6 +94,9 @@ class SingleRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } }; class SingleRateControl : public IRateControl { public: diff --git a/tests/module/Ieee80211SingleProtectionGroup_1.test b/tests/module/Ieee80211SingleProtectionGroup_1.test index 0bb495f62cf..76d8e3e744e 100644 --- a/tests/module/Ieee80211SingleProtectionGroup_1.test +++ b/tests/module/Ieee80211SingleProtectionGroup_1.test @@ -29,6 +29,7 @@ class GroupRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *mode) override { return delegate->computeCompressedBlockAckResponseMode(mode); } }; class GroupMechanism : public SingleProtectionMechanism { public: diff --git a/tests/module/Ieee80211Txnav_1.test b/tests/module/Ieee80211Txnav_1.test index 0e23937594f..5a740809038 100644 --- a/tests/module/Ieee80211Txnav_1.test +++ b/tests/module/Ieee80211Txnav_1.test @@ -33,6 +33,7 @@ class TxnavSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseAckFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *mode) override { return delegate->computeCompressedBlockAckResponseMode(mode); } }; class TxnavRx : public Rx { public: diff --git a/tests/module/Ieee80211VhtHtSupport_1.test b/tests/module/Ieee80211VhtHtSupport_1.test index f9b9501aada..72d90237935 100644 --- a/tests/module/Ieee80211VhtHtSupport_1.test +++ b/tests/module/Ieee80211VhtHtSupport_1.test @@ -71,6 +71,9 @@ class VhtHtSelection : public IQosRateSelection { const IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + const IIeee80211Mode *computeCompressedBlockAckResponseMode(const IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } }; class VhtHtRx : public Rx { public: diff --git a/tests/unit/Ieee80211ResponseTimeout_1.test b/tests/unit/Ieee80211ResponseTimeout_1.test index c55f286bb21..c486ec99abd 100644 --- a/tests/unit/Ieee80211ResponseTimeout_1.test +++ b/tests/unit/Ieee80211ResponseTimeout_1.test @@ -50,6 +50,7 @@ class TestRateSelection : public SimpleModule, public IRateSelection, public IQo virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override { return responseMode; } const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *, const Ptr&, bool) override { return responseMode; } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *) override { return responseMode; } }; Define_Module(TestRateSelection); From 3c2a0edc5a69ffab283d0d5908efc4e764e0beca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 00:33:50 +0100 Subject: [PATCH 18/24] ieee80211: add: receive HT aggregates with implicit Block Ack Before this commit, Rx refuses aggregate content at the MAC boundary. Correct independent members cannot update receipt state, enter the reorder buffer, or request an implicit Compressed response. After this commit, Rx checks each member against cached PHY outcomes and its FCS. HCF validates addresses, TID, profile, and agreement before it updates receipt state. The recipient sends one Compressed Block Ack after SIFS. One failed member does not invalidate a correct neighbor or cause EIFS when another member is correct. For example, a fixture sends three MPDUs with a failed middle member through real radios. Rx accepts the two correct members independently. The recipient records those members and returns their bitmap when its agreement permits the implicit response. The reorder fixture also supplies physical SIFS intervals and delimiter gaps. Rx retains delimiter and interval facts across its local Block Ack so release uses actual reception evidence. The receipt record remains the acknowledgment-state owner. The boundary, reorder, and Compressed input module fixtures check this receiver without a production aggregate sender. WifiHtAmpduPartialReception preserves cached PHY assertions before member extraction. WifiLocalInterframeInputs checks ordinary interval evidence and clears stale aggregate context when timing evidence is absent. The partial-reception fixture uses invalid MAC content, so it establishes no successful member delivery by itself. Fresh parent execution of the new receive API remains unavailable because the parent lacks that API. The receive callback, receipt context, and reorder consumers enter together because each accepted member needs the complete release contract. Local aggregate selection, DATA submission, and sender recovery remain disabled until sender integration. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-implicit-blockack --- .../ieee80211/mac/Ieee80211InterframeInd.msg | 12 + .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 19 + src/inet/linklayer/ieee80211/mac/Rx.cc | 143 +++++- src/inet/linklayer/ieee80211/mac/Rx.h | 8 +- .../RecipientBlockAckAgreementHandler.cc | 13 +- .../RecipientBlockAckAgreementHandler.h | 1 + .../blockackreordering/BlockAckReordering.cc | 42 +- .../blockackreordering/BlockAckReordering.h | 5 +- .../ieee80211/mac/contract/AmpduReception.h | 60 +++ .../IRecipientBlockAckAgreementHandler.h | 1 + .../contract/IRecipientQosMacDataService.h | 3 +- .../linklayer/ieee80211/mac/contract/IRx.h | 3 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 107 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 7 +- .../recipient/RecipientQosMacDataService.cc | 4 +- .../recipient/RecipientQosMacDataService.h | 2 +- tests/module/Ieee80211AmpduMacBoundary_1.test | 106 +++- tests/module/Ieee80211AmpduReordering_1.test | 465 ++++++++++++++++++ .../Ieee80211CompressedBlockAckInput_1.test | 5 +- .../protocol/wifi/ampdu-partial-reception.md | 25 +- .../common/WifiHtAmpduPartialReception.test | 28 +- .../common/WifiLocalInterframeInputs.test | 12 + tests/unit/Ieee80211TxNavChoice_1.test | 3 + 23 files changed, 1001 insertions(+), 73 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h create mode 100644 tests/module/Ieee80211AmpduReordering_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211InterframeInd.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211InterframeInd.msg index b09eb388270..f2b533ceed3 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211InterframeInd.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211InterframeInd.msg @@ -2,11 +2,23 @@ import inet.common.TagBase; +cplusplus {{ +#include "inet/linklayer/ieee80211/mac/contract/AmpduReception.h" +}} + namespace inet::ieee80211; +class BlockAckReceiveContext +{ + @existingClass; + @descriptor(false); + @opaque; +} + // Rx replaces this local indication after FCS acceptance. // False means absent or rejected interval evidence, not frame rejection. class Ieee80211InterframeInd extends inet::TagBase { bool related = false; + BlockAckReceiveContext blockAckContext; } diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index cb9b7aed09f..020b256490c 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -209,6 +209,25 @@ void Ieee80211Mac::handleUpperPacket(Packet *packet) void Ieee80211Mac::handleLowerPacket(Packet *packet) { + auto aggregate = packet->findTag(); + if (aggregate && aggregate->getHeaderSuccessful() && aggregate->getContentComplete() && aggregate->getAggregation()) { + auto reception = rx->lowerAggregateReceived(packet); + take(packet); + for (auto& member : reception.members) + take(member.packet.get()); + if (!reception.anyCorrect) { + delete packet; + if (mib->qos) + hcf->corruptedFrameReceived(); + else + dcf->corruptedFrameReceived(); + } + else if (mib->qos) + hcf->processLowerAggregate(packet, std::move(reception)); + else + delete packet; + return; + } if (rx->lowerFrameReceived(packet)) { auto header = packet->peekAtFront(); processLowerFrame(packet, header); diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index 169d208c9ae..bae9c9960a2 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/checksum/Checksum.h" #include "inet/common/ProtocolTag_m.h" @@ -146,6 +148,76 @@ bool Rx::lowerFrameReceived(Packet *packet) } } +AmpduReception Rx::lowerAggregateReceived(Packet *packet) +{ + Enter_Method("lowerAggregateReceived"); + take(packet); + AmpduReception result; + auto indication = packet->findTag(); + auto topology = std::make_shared(); + result.topology = topology; + if (indication && indication->getHeaderSuccessful() && indication->getAggregation() && indication->getContentComplete()) { + topology->length = B(packet->getDataLength()); + bool first = true; + B parsedEnd(0); + bool complete = MpduDeaggregation::visitSubframes(packet, B(0), topology->length, + [&](const auto&, B offset, bool valid) { + topology->delimiters.push_back({offset, valid}); + parsedEnd = offset + B(4); + if (!valid) + first = false; + }, + [&](const Ptr& mpdu, B offset) { + B length(mpdu->getChunkLength()); + parsedEnd = offset + length; + topology->members.push_back({offset, {}, first}); + first = false; + auto member = std::make_unique(packet->getName(), mpdu); + member->getRegionTags().copyTags(packet->getRegionTags(), packet->getFrontOffset() + offset, + member->getFrontOffset(), length); + member->copyTags(*packet); + member->removeTagIfPresent(); + // Chunk flags retain the cached PHY outcome before every FCS mode. + bool correct = length >= B(4) && mpdu->isCorrect() && + member->hasAtBack(B(4)) && + isFcsOk(member.get()) && member->hasAtFront(); + if (!correct) { + PacketDropDetails details; + details.setReason(INCORRECTLY_RECEIVED); + emit(packetDroppedSignal, member.get(), &details); + return; + } + result.anyCorrect = true; + const auto& header = member->peekAtFront(); + if (header->getReceiverAddress() != address) + setOrExtendNav(header->getDurationField()); + if (auto data = dynamicPtrCast(header)) { + if (data->getSequenceNumber().isSpecified()) + topology->members.back().sequence = data->getSequenceNumber(); + } + result.members.push_back({std::move(member), offset}); + }); + if (!complete && parsedEnd < topology->length) + topology->delimiters.push_back({parsedEnd, false}); + } + Ptr header; + if (result.anyCorrect) { + header = result.members.front().packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + topology->transmitter = data->getTransmitterAddress(); + topology->receiver = data->getReceiverAddress(); + topology->tid = data->getTid(); + } + } + attachInterframeResult(packet, result.anyCorrect, header, topology); + // IEEE Std 802.11-2024, 10.3.2.3.7: any correct MPDU excludes EIFS for this PPDU. + if (!result.anyCorrect) { + for (auto contention : contentions) + contention->corruptedFrameReceived(); + } + return result; +} + void Rx::frameTransmitted(simtime_t durationField) { Enter_Method("frameTransmitted"); @@ -300,6 +372,7 @@ void Rx::invalidateInterframeHistory() invalidateFutureInterframeHistory(); completedReception.reset(); ambiguityEnd = -1; + transmittedBlockAckAggregate.reset(); } void Rx::observeReceptionStart(const IReception *reception) @@ -338,49 +411,60 @@ void Rx::observeReceptionEnd(const IReception *reception) } } -bool Rx::isCompletedAck(const ITransmission *transmission) const +Ptr Rx::getTransmittedMacHeader(const ITransmission *transmission) const { auto packet = transmission ? transmission->getPacket() : nullptr; if (!packet || !packet->findTag() || packet->getDataLength() <= b(0)) - return false; + return nullptr; auto protocol = packet->getTag()->getProtocol(); if (protocol != &Protocol::ieee80211FhssPhy && protocol != &Protocol::ieee80211IrPhy && protocol != &Protocol::ieee80211DsssPhy && protocol != &Protocol::ieee80211HrDsssPhy && protocol != &Protocol::ieee80211OfdmPhy && protocol != &Protocol::ieee80211ErpOfdmPhy && protocol != &Protocol::ieee80211HtPhy && protocol != &Protocol::ieee80211VhtPhy) - return false; + return nullptr; // Optional evidence must not turn an unsupported representation into a MAC error. try { int flags = Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED; auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet, b(-1), flags); if (!phy->isCorrect() || phy->isIncomplete() || phy->isImproperlyRepresented() || phy->getChunkLength() <= b(0) || phy->getChunkLength() + LENGTH_ACK > packet->getDataLength()) - return false; + return nullptr; auto mac = packet->peekAt(phy->getChunkLength(), b(-1), flags); - return mac->isCorrect() && !mac->isIncomplete() && !mac->isImproperlyRepresented() && mac->getType() == ST_ACK; + return mac->isCorrect() && !mac->isIncomplete() && !mac->isImproperlyRepresented() ? mac : nullptr; } catch (const cRuntimeError&) { - return false; + return nullptr; } } -void Rx::attachInterframeResult(Packet *packet, bool accepted) +bool Rx::isCompletedAck(const ITransmission *transmission) const +{ + auto header = getTransmittedMacHeader(transmission); + return header && header->getType() == ST_ACK; +} + +void Rx::attachInterframeResult(Packet *packet, bool accepted, + const Ptr& acceptedHeader, std::shared_ptr topology) { discardExpiredReception(); auto time = packet->findTag(); if (!time || time->getStartTime() < SIMTIME_ZERO || time->getEndTime() <= time->getStartTime()) { invalidateFutureInterframeHistory(); - if (accepted) - packet->addTagIfAbsent()->setRelated(false); + if (accepted) { + auto interval = packet->addTagIfAbsent(); + interval->setRelated(false); + interval->setBlockAckContext({}); + } return; } bool related = false; + BlockAckReceiveContext context; if (completedReception && completedReception->start == time->getStartTime() && completedReception->end == time->getEndTime()) { auto evidence = std::move(*completedReception); completedReception.reset(); if (accepted) { - auto header = packet->peekAtFront(); + auto header = acceptedHeader ? acceptedHeader : packet->peekAtFront(); auto twoAddress = dynamicPtrCast(header); auto indication = packet->findTag(); bool ht = indication && indication->getMode() && @@ -400,6 +484,11 @@ void Rx::attachInterframeResult(Packet *packet, bool accepted) correctedGap <= evidence.rifs + evidence.rifs / 10; } } + if (related && evidence.preceding) { + context.previousAggregate = evidence.preceding->aggregate; + context.followsAggregate = bool(context.previousAggregate); + context.followsIndividual = !context.followsAggregate; + } if (evidence.mayBecomePredecessor) { PrecedingFrame preceding{evidence.end, false}; preceding.ht = ht; @@ -409,12 +498,18 @@ void Rx::attachInterframeResult(Packet *packet, bool accepted) auto dataOrMgmt = dynamicPtrCast(header); preceding.noSifsResponse = recipientAckPolicy && (dataOrMgmt ? !recipientAckPolicy->isAckNeeded(dataOrMgmt) : !dynamicPtrCast(header) && !dynamicPtrCast(header)); + preceding.aggregate = topology; + if (topology) + preceding.noSifsResponse = false; precedingFrame = preceding; } } } - if (accepted) - packet->addTagIfAbsent()->setRelated(related); + if (accepted) { + auto interval = packet->addTagIfAbsent(); + interval->setRelated(related); + interval->setBlockAckContext(context); + } } void Rx::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) @@ -426,16 +521,29 @@ void Rx::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, c observeReceptionStart(check_and_cast(obj)); else if (signalID == receptionEndedSignal) observeReceptionEnd(check_and_cast(obj)); - else if (signalID == transmissionStartedSignal) + else if (signalID == transmissionStartedSignal) { + auto header = getTransmittedMacHeader(dynamic_cast(obj)); + auto blockAck = dynamicPtrCast(header); + auto topology = precedingFrame ? precedingFrame->aggregate : transmittedBlockAckAggregate; + transmittedBlockAckAggregate = blockAck && topology && + blockAck->getReceiverAddress() == topology->transmitter && blockAck->getTidInfo() == topology->tid ? + topology : nullptr; invalidateFutureInterframeHistory(); + } else if (signalID == transmissionEndedSignal) { invalidateFutureInterframeHistory(); if (isCompletedAck(dynamic_cast(obj))) precedingFrame = PrecedingFrame{simTime(), true}; + else if (transmittedBlockAckAggregate) { + precedingFrame = PrecedingFrame{simTime(), false}; + precedingFrame->aggregate = std::move(transmittedBlockAckAggregate); + } + transmittedBlockAckAggregate.reset(); } } if (signalID == modesetChangedSignal) { invalidateFutureInterframeHistory(); + transmittedBlockAckAggregate.reset(); ModeSetListener::receiveSignal(source, signalID, obj, details); } } @@ -451,8 +559,15 @@ void Rx::receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, else if (signalID == IRadio::radioModeChangedSignal) { if (value == IRadio::RADIO_MODE_OFF) invalidateInterframeHistory(); - else if (value == IRadio::RADIO_MODE_SLEEP || value == IRadio::RADIO_MODE_TRANSMITTER) + else if (value == IRadio::RADIO_MODE_TRANSMITTER) { + // The radio changes mode before it reports the actual response transmission. + transmittedBlockAckAggregate = precedingFrame ? precedingFrame->aggregate : nullptr; invalidateFutureInterframeHistory(); + } + else if (value == IRadio::RADIO_MODE_SLEEP) { + transmittedBlockAckAggregate.reset(); + invalidateFutureInterframeHistory(); + } } } diff --git a/src/inet/linklayer/ieee80211/mac/Rx.h b/src/inet/linklayer/ieee80211/mac/Rx.h index 351bfde175b..0d0850fc705 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.h +++ b/src/inet/linklayer/ieee80211/mac/Rx.h @@ -55,6 +55,7 @@ class INET_API Rx : public ModeSetListener, public IRx MacAddress receiver; bool noSifsResponse = false; simtime_t extension = -1; + std::shared_ptr aggregate; }; struct ReceptionEvidence { simtime_t start; @@ -68,6 +69,7 @@ class INET_API Rx : public ModeSetListener, public IRx }; opp_component_ptr networkInterface; std::optional precedingFrame; + std::shared_ptr transmittedBlockAckAggregate; std::optional activeReception; std::optional completedReception; simtime_t ambiguityEnd = -1; @@ -87,7 +89,10 @@ class INET_API Rx : public ModeSetListener, public IRx void observeReceptionStart(const physicallayer::IReception *reception); void observeReceptionEnd(const physicallayer::IReception *reception); bool isCompletedAck(const physicallayer::ITransmission *transmission) const; - void attachInterframeResult(Packet *packet, bool accepted); + Ptr getTransmittedMacHeader(const physicallayer::ITransmission *transmission) const; + void attachInterframeResult(Packet *packet, bool accepted, + const Ptr& header = nullptr, + std::shared_ptr topology = nullptr); virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; @@ -102,6 +107,7 @@ class INET_API Rx : public ModeSetListener, public IRx virtual void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart part) override; virtual bool lowerFrameReceived(Packet *packet) override; virtual void invalidateInterframeHistory() override; + virtual AmpduReception lowerAggregateReceived(Packet *packet) override; virtual void frameTransmitted(simtime_t durationField) override; virtual simtime_t getTxnavRemaining() const override; virtual void successfulFrameTransmitted(simtime_t ppduEnd, simtime_t durationField) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 717368a5a46..7c63aea9ec2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -35,7 +35,7 @@ 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 + if (qosHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { Tid tid = qosHeader->getTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); auto agreement = getAgreement(tid, originatorAddr); @@ -46,6 +46,17 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& qosHeader, + IBlockAckAgreementHandlerCallback *callback) +{ + // IEEE Std 802.11-2024, 11.5.4: valid Implicit BAR DATA also refreshes inactivity, including duplicates. + auto agreement = getAgreement(qosHeader->getTid(), qosHeader->getTransmitterAddress()); + if (agreement && agreement->getVariant() == BlockAckVariant::COMPRESSED && qosHeader->getAckPolicy() == NORMAL_ACK) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } +} + void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index ac93c8366e6..21b3eabc7e5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -49,6 +49,7 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; + virtual void implicitBlockAckRequested(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; virtual void processTransmittedDelba(const Ptr& delba) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 78f06a2d725..5702e3baf78 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -50,7 +50,44 @@ void BlockAckReordering::clearPeer(MacAddress peer) } } -BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe) +bool BlockAckReordering::isMandatoryRelease(RecipientBlockAckAgreement *agreement, + const Ptr& header, bool interframe, const BlockAckReceiveContext *context) const +{ + auto record = agreement->getBlockAckRecord(); + auto priorSequence = record->getWindowStart() - 1; + if (!record->hasCompleteReceipt(priorSequence)) + return false; + if (!context) + return interframe; + auto hasDelimiterChain = [&](const std::shared_ptr& aggregate, B endpoint) { + if (!aggregate || aggregate->transmitter != header->getTransmitterAddress() || + aggregate->receiver != header->getReceiverAddress() || aggregate->tid != header->getTid()) + return false; + for (const auto& member : aggregate->members) { + if (!member.sequence || *member.sequence != priorSequence || !(member.offset < endpoint)) + continue; + bool valid = true; + for (const auto& delimiter : aggregate->delimiters) { + if (member.offset < delimiter.offset && delimiter.offset < endpoint) + valid &= delimiter.valid; + } + if (valid) + return true; + } + return false; + }; + // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2)(i): a later member needs the complete delimiter chain. + if (context->aggregate && !context->firstMember) + return hasDelimiterChain(context->aggregate, context->memberOffset); + // Conditions (ii) and (iii) require the preceding aggregate's valid delimiter tail. + if (context->followsAggregate) + return hasDelimiterChain(context->previousAggregate, + context->previousAggregate ? context->previousAggregate->length : B(0)); + // Conditions (iv) and (v) follow an individual MPDU or a local ACK. + return context->followsIndividual; +} + +BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe, const BlockAckReceiveContext *context) { ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); auto sequenceNumber = dataHeader->getSequenceNumber(); @@ -72,10 +109,9 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(R if (receiveBuffer->insertFrame(dataPacket, dataHeader)) { if (dataHeader->getAckPolicy() == BLOCK_ACK && agreement->getVariant() == BlockAckVariant::BASIC) agreement->blockAckPolicyFrameReceived(dataHeader); - auto record = agreement->getBlockAckRecord(); // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2)(v) and 10.25.6.6.1: release the complete prefix in order. if (agreement->getVariant() == BlockAckVariant::COMPRESSED && insideWindow && windowStart < sequenceNumber && - interframe && record->hasCompleteReceipt(record->getWindowStart() - 1)) { + isMandatoryRelease(agreement, dataHeader, interframe, context)) { auto preceding = advanceReceiveBuffer(agreement, receiveBuffer, sequenceNumber); releasedFrames.insert(releasedFrames.end(), preceding.begin(), preceding.end()); } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index fdfbd29e226..9f090d790aa 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -10,6 +10,7 @@ #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h" +#include "inet/linklayer/ieee80211/mac/contract/AmpduReception.h" namespace inet { namespace ieee80211 { @@ -44,6 +45,8 @@ class INET_API BlockAckReordering void releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReleasedFrames& releasedFrames); ReceiveBuffer *createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement); bool addMsduIfComplete(ReceiveBuffer *receiveBuffer, ReleasedFrames& releasedFrames, SequenceNumberCyclic seqNum); + bool isMandatoryRelease(RecipientBlockAckAgreement *agreement, const Ptr& header, + bool interframe, const BlockAckReceiveContext *context) const; private: ReleasedFrames advanceReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); @@ -54,7 +57,7 @@ class INET_API BlockAckReordering void clearPeer(MacAddress peer); void processReceivedDelba(const Ptr& delba); - ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe = false); + ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe = false, const BlockAckReceiveContext *context = nullptr); ReleasedFrames processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h b/src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h new file mode 100644 index 00000000000..a0e828a90a9 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_AMPDURECEPTION_H +#define __INET_AMPDURECEPTION_H + +#include +#include + +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +// These facts describe one received PSDU. BlockAckRecord remains the receipt owner. +struct INET_API AmpduTopology +{ + struct Delimiter { + B offset; + bool valid; + }; + struct Member { + B offset; + std::optional sequence; + bool first = false; + }; + std::vector delimiters; + std::vector members; + B length = B(0); + MacAddress transmitter; + MacAddress receiver; + Tid tid = -1; +}; + +struct INET_API AmpduReception +{ + struct Member { + std::unique_ptr packet; + B offset; + }; + std::shared_ptr topology; + std::vector members; + bool anyCorrect = false; +}; + +// HCF supplies an immutable context for one member or one standalone MPDU. +struct INET_API BlockAckReceiveContext +{ + std::shared_ptr aggregate; + std::shared_ptr previousAggregate; + B memberOffset = B(0); + bool firstMember = false; + bool followsAggregate = false; + bool followsIndividual = false; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 5c4beb3fe04..c2a1463709e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -36,6 +36,7 @@ class INET_API IRecipientBlockAckAgreementHandler virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; virtual void processTransmittedDelba(const Ptr& delba) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void implicitBlockAckRequested(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 7aded713794..1d76ad5e13a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -10,6 +10,7 @@ #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/AmpduReception.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h" namespace inet { @@ -27,7 +28,7 @@ class INET_API IRecipientQosMacDataService virtual void clearPeer(MacAddress peer) = 0; // HCF supplies the observed SIFS or permitted RIFS relation for this frame. - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false) = 0; + virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false, const BlockAckReceiveContext *context = nullptr) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRx.h b/src/inet/linklayer/ieee80211/mac/contract/IRx.h index cdff3f5f12b..5f5eb2edbe9 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRx.h @@ -10,6 +10,7 @@ #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/AmpduReception.h" #include "inet/linklayer/ieee80211/mac/contract/IContention.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" @@ -54,6 +55,8 @@ class INET_API IRx virtual bool lowerFrameReceived(Packet *packet) = 0; // Discard local interval evidence when its semantic context is no longer valid. virtual void invalidateInterframeHistory() = 0; + // The caller retains the envelope. The result owns the correct, separate member Packets. + virtual AmpduReception lowerAggregateReceived(Packet *packet) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 662f47d6aa8..b9963023249 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -861,6 +861,103 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, bool value, cO updateContention(); } +void Hcf::processLowerAggregate(Packet *packet, AmpduReception received) +{ + Enter_Method("processLowerAggregate"); + auto reception = std::move(received); + take(packet); + std::unique_ptr envelope(packet); + for (auto& member : reception.members) + take(member.packet.get()); + if (edca->getChannelOwner() && frameSequenceHandler->isSequenceRunning() && + dynamic_cast(frameSequenceHandler->getContext()->getLastStep())) { + bool addressed = false; + for (const auto& member : reception.members) + addressed |= isForUs(member.packet->peekAtFront()); + // An aggregate cannot satisfy a control-response step. Resolve an elapsed or addressed wait first. + if (!startRxTimer->isScheduled() || addressed) { + frameSequenceHandler->handleStartRxTimeout(); + cancelEvent(startRxTimer); + } + } + if (!reception.anyCorrect || reception.members.empty() || !reception.topology) { + rx->invalidateInterframeHistory(); + return; + } + Ptr common; + // IEEE Std 802.11-2024, 9.7.1 and 10.25.6.5: validate the complete supported context before state changes. + for (const auto& member : reception.members) { + auto header = dynamicPtrCast(member.packet->peekAtFront()); + if (!header || header->getType() != ST_DATA_WITH_QOS || header->getAckPolicy() != NORMAL_ACK || + header->getTid() < 0 || header->getTid() > 15 || !header->getSequenceNumber().isSpecified() || + header->getReceiverAddress().isUnspecified() || header->getReceiverAddress().isMulticast() || + header->getTransmitterAddress().isUnspecified() || header->getTransmitterAddress().isMulticast() || + header->getFragmentNumber() != 0 || header->getMoreFragments() || header->getDurationField() < SIMTIME_ZERO || + (common && (common->getReceiverAddress() != header->getReceiverAddress() || + common->getTransmitterAddress() != header->getTransmitterAddress() || common->getTid() != header->getTid() || + common->getDurationField() != header->getDurationField()))) { + rx->invalidateInterframeHistory(); + return; + } + common = header; + } + if (!isForUs(common) || !recipientBlockAckAgreementHandler) { + rx->invalidateInterframeHistory(); + return; + } + auto agreement = recipientBlockAckAgreementHandler->getAgreement(common->getTid(), common->getTransmitterAddress()); + // ADDBA has no variant field. An active Basic agreement cannot accept an implicit Compressed request. + if (agreement && (blockAckVariant != BlockAckVariant::COMPRESSED || agreement->getVariant() != BlockAckVariant::COMPRESSED)) + throw cRuntimeError("Block Ack profile conflict at %s: peer=%s TID=%d local=basic received=implicit-compressed; ADDBA does not negotiate the profile. Configure matching blockAckVariant values and start a new run", + getFullPath().c_str(), common->getTransmitterAddress().str().c_str(), common->getTid()); + // INET policy: implicit DATA without an agreement causes silent discard and no receive state. + if (!agreement || recipientBlockAckAgreementHandler->isTeardownPending(common->getTransmitterAddress(), common->getTid()) || + !isProfileAllowed(common->getTransmitterAddress(), false)) { + rx->invalidateInterframeHistory(); + return; + } + auto topology = std::make_shared(*reception.topology); + topology->transmitter = common->getTransmitterAddress(); + topology->receiver = common->getReceiverAddress(); + topology->tid = common->getTid(); + BlockAckReceiveContext context; + auto interval = packet->findTag(); + if (interval) + context = interval->getBlockAckContext(); + context.aggregate = topology; + recipientBlockAckAgreementHandler->implicitBlockAckRequested(common, this); + for (auto& member : reception.members) { + auto header = member.packet->peekAtFront(); + context.memberOffset = member.offset; + context.firstMember = false; + for (const auto& position : topology->members) { + if (position.offset == member.offset) { + context.firstMember = position.first; + break; + } + } + take(member.packet.get()); + emit(packetReceivedFromPeerSignal, member.packet.get()); + sendUp(recipientDataService->dataFrameReceived(member.packet.release(), header, + recipientBlockAckAgreementHandler, false, &context)); + } + sendImplicitBlockAck(packet, common, agreement); +} + +void Hcf::sendImplicitBlockAck(Packet *envelope, const Ptr& header, + RecipientBlockAckAgreement *agreement) +{ + auto mode = rateSelection->computeCompressedBlockAckResponseMode(envelope->getTag()->getMode()); + auto response = recipientBlockAckProcedure->buildCompressedBlockAck(header->getTransmitterAddress(), header->getTid(), agreement); + response->setDurationField(encodeDuration(std::max(SIMTIME_ZERO, + header->getDurationField() - modeSet->getSifsTime() - mode->getDuration(LENGTH_COMPRESSED_BLOCKACK)), false)); + auto packet = new Packet("ImplicitBlockAck", response); + packet->insertAtBack(makeShared()); + setFrameMode(packet, response, mode); + tx->transmitFrame(packet, response, modeSet->getSifsTime(), false, this); + delete packet; +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -874,6 +971,9 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& } auto interframeInd = packet->findTag(); bool interframe = interframeInd && interframeInd->getRelated(); + BlockAckReceiveContext receiveContext; + if (interframeInd) + receiveContext = interframeInd->getBlockAckContext(); EV_INFO << "Processing lower frame: " << packet->getName() << endl; auto edcaf = edca->getChannelOwner(); auto action = dynamicPtrCast(header); @@ -919,7 +1019,7 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& else if (hcca->isOwning()) throw cRuntimeError("Hcca is unimplemented!"); else if (isForUs(header)) - recipientProcessReceivedFrame(packet, header, interframe); + recipientProcessReceivedFrame(packet, header, interframe, &receiveContext); else { EV_INFO << "This frame is not for us" << std::endl; PacketDropDetails details; @@ -1120,7 +1220,8 @@ void Hcf::frameSequenceFinished() finishingSequence = false; } -void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe) +void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe, + const BlockAckReceiveContext *context) { EV_INFO << "Processing received frame " << packet->getName() << " as recipient.\n"; emit(packetReceivedFromPeerSignal, packet); @@ -1178,7 +1279,7 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { if (dataHeader->getType() == ST_DATA_WITH_QOS && recipientBlockAckAgreementHandler) recipientBlockAckAgreementHandler->qosFrameReceived(dataHeader, this); - sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler, interframe)); + sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler, interframe, context)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { auto accepted = recipientDataService->managementFrameReceived(packet, mgmtHeader); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index b54d76af3bb..068c93574ac 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -39,6 +39,7 @@ #include "inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/CtsProcedure.h" +#include "inet/linklayer/ieee80211/mac/contract/AmpduReception.h" namespace inet { namespace ieee80211 { @@ -182,7 +183,10 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: bool isProfileAllowed(MacAddress peer, bool originator) const; // Recipient - virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe = false); + virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe = false, + const BlockAckReceiveContext *context = nullptr); + void sendImplicitBlockAck(Packet *envelope, const Ptr& header, + RecipientBlockAckAgreement *agreement); virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); virtual void recipientProcessReceivedManagementFrame(const Ptr& header); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); @@ -232,6 +236,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; + virtual void processLowerAggregate(Packet *packet, AmpduReception reception); virtual void corruptedFrameReceived() override; }; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index e903ec9b62d..0bf39fdf48f 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -53,7 +53,7 @@ Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) return nullptr; } -std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe) +std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe, const BlockAckReceiveContext *context) { Enter_Method("dataFrameReceived"); take(dataPacket); @@ -81,7 +81,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader, interframe); + frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader, interframe, context); } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 066359162b1..4b07f244a07 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -48,7 +48,7 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, public: virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) override; virtual void clearPeer(MacAddress peer) override; - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false) override; + virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false, const BlockAckReceiveContext *context = nullptr) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; }; diff --git a/tests/module/Ieee80211AmpduMacBoundary_1.test b/tests/module/Ieee80211AmpduMacBoundary_1.test index 4028ae722be..b7253be5e0f 100644 --- a/tests/module/Ieee80211AmpduMacBoundary_1.test +++ b/tests/module/Ieee80211AmpduMacBoundary_1.test @@ -1,7 +1,8 @@ %description: The production radio sends aggregate containers to the production MAC and Rx. -Rx rejects successful, partially failed, and entirely failed aggregates before standalone FCS or header checks. -The drop signal retains incorrect chunks and the clear container bit-error flag. +Rx accepts correct members with valid FCS trailers and drops failed members. +The aggregate retains incorrect chunks and the clear container bit-error flag. +Separate members retain their own outcomes and omit the aggregate indication. A standalone CTS still sets NAV through the normal acceptance path. Scalar and dimensional radios cover whole-signal and separate-part reception with DCF and HCF. Malformed delimiters before typed MPDUs permit bounded recovery without a radio exception. @@ -33,51 +34,87 @@ class BoundaryErrorModel : public Ieee80211NistErrorModel double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } double computeChunkSuccessRate(const ISnir *snir, b length) const override { auto packet = snir->getReception()->getTransmission()->getPacket(); - bool failed = packet->isName("failed") || (packet->isName("partial") && (length == B(318) || length == B(101) - DATAFRAME_HEADER_MINLENGTH)); + bool failed = packet->isName("failed") || (packet->isName("partial") && + (length == B(318) || length == B(101) - DATAFRAME_HEADER_MINLENGTH - B(4))); return length != B(4) && length != B(3) && failed ? 0 : 1; } }; Define_Module(BoundaryErrorModel); +class BoundaryRx : public Rx +{ + public: + int received = 0; + AmpduReception lowerAggregateReceived(Packet *packet) override { + ASSERT(received < 4); + ASSERT(packet->isName(received == 0 ? "good" : received == 1 ? "partial" : received == 2 ? "failed" : "malformed")); + ASSERT(!packet->cPacket::hasBitError()); + auto indication = packet->getTag(); + ASSERT(indication->getAggregation() && indication->getHeaderSuccessful() && indication->getContentComplete()); + bool raw = getSimulation()->getSystemModule()->getSubmodule("sender")->getSubmodule("radio")->par("sendRawBytes"); + const int offsets[]{4, 108, 216}; + const int lengths[]{100, 101, 102}; + if (raw && (received == 1 || received == 2)) + ASSERT(packet->peekAll(Chunk::PF_ALLOW_INCORRECT)->isIncorrect()); + for (int i = 0; !raw && i < 3; i++) { + bool failed = received == 2 || (received == 1 && i == 1); + auto mpdu = packet->peekDataAt(B(offsets[i]), B(lengths[i]), Chunk::PF_ALLOW_ALL); + ASSERT(mpdu->isCorrect() == !failed); + } + auto reception = Rx::lowerAggregateReceived(packet); + std::vector expected = received == 0 ? std::vector{0, 1, 2} : + received == 1 ? std::vector{0, 2} : received == 2 ? std::vector{} : std::vector{1, 2}; + if (raw && (received == 1 || received == 2)) + expected.clear(); + ASSERT(reception.anyCorrect == !expected.empty()); + ASSERT(reception.members.size() == expected.size()); + ASSERT(reception.topology && reception.topology->length == B(318)); + ASSERT(reception.topology->members.size() == (raw && (received == 1 || received == 2) ? 0 : received == 3 ? 2 : 3)); + for (size_t i = 0; i < expected.size(); i++) { + const auto& member = reception.members[i]; + ASSERT(member.offset == (expected[i] == 0 ? B(4) : B(108 * expected[i]))); + ASSERT(member.packet->getDataLength() == B(100 + expected[i])); + ASSERT(member.packet->peekAtFront()->getSequenceNumber().get() == expected[i]); + ASSERT(!member.packet->hasBitError()); + ASSERT(!member.packet->findTag()); + } + received++; + return reception; + } +}; +Define_Module(BoundaryRx); + class BoundaryDriver : public cSimpleModule, public cListener { + BoundaryRx *rx = nullptr; int sent = 0; int dropped = 0; int navSet = 0; + int expectedDrops() const { return getModuleByPath("^.sender.radio")->par("sendRawBytes").boolValue() ? 0 : 4; } protected: void initialize() override { - auto rx = getModuleByPath("^.receiver.wlan[0].mac.rx"); + rx = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.rx")); rx->subscribe(packetDroppedSignal, this); rx->subscribe(Rx::navChangedSignal, this); scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); } - void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *details) override { ASSERT(signal == packetDroppedSignal); auto packet = check_and_cast(object); ASSERT(dropped < 4); - ASSERT(packet->isName(dropped == 0 ? "good" : dropped == 1 ? "partial" : dropped == 2 ? "failed" : "malformed")); - ASSERT(!packet->cPacket::hasBitError()); - ASSERT(packet->hasBitError() == (dropped == 1 || dropped == 2)); - auto indication = packet->getTag(); - ASSERT(indication->getAggregation() && indication->getHeaderSuccessful() && indication->getContentComplete()); - bool raw = getModuleByPath("^.sender.radio")->par("sendRawBytes"); - const int offsets[]{4, 108, 216}; - const int lengths[]{100, 101, 102}; - for (int i = 0; !raw && i < 3; i++) { - bool failed = dropped == 2 || (dropped == 1 && i == 1); - auto mpdu = packet->peekDataAt(B(offsets[i]), B(lengths[i]), Chunk::PF_ALLOW_ALL); - ASSERT(mpdu->isCorrect() == !failed); - } - if (dropped == 3) { - ASSERT(packet->peekDataAt(B(0), B(4), Chunk::PF_ALLOW_ALL)->isCorrect()); - ASSERT(packet->peekDataAt(B(0), B(4))->getLength() == 2000); - } + ASSERT(packet->isName(dropped == 0 ? "partial" : "failed")); + int sequence = dropped == 0 ? 1 : dropped - 1; + ASSERT(packet->getDataLength() == B(100 + sequence)); + ASSERT(packet->peekAtFront(b(-1), Chunk::PF_ALLOW_INCORRECT)->getSequenceNumber().get() == sequence); + ASSERT(packet->hasBitError()); + ASSERT(!packet->findTag()); + ASSERT(check_and_cast(details)->getReason() == INCORRECTLY_RECEIVED); dropped++; } void receiveSignal(cComponent *, simsignal_t signal, const SimTime& value, cObject *) override { ASSERT(signal == Rx::navChangedSignal); if (value > SIMTIME_ZERO) { - ASSERT(sent == 5 && dropped == 4); + ASSERT(sent == 5 && rx->received == 4 && dropped == expectedDrops()); ASSERT(value == SimTime(50, SIMTIME_US)); navSet++; } @@ -90,10 +127,18 @@ class BoundaryDriver : public cSimpleModule, public cListener auto delimiter = makeShared(); delimiter->setLength(sent == 3 && length == 100 ? 2000 : length); packet->insertAtBack(delimiter); + Packet mpdu("member"); auto member = makeShared(); - member->setSequenceNumber(SequenceNumberCyclic(0)); - packet->insertAtBack(member); - packet->insertAtBack(makeShared(std::vector(length - member->getChunkLength().get(), 0))); + member->setDurationField(SIMTIME_ZERO); + member->setSequenceNumber(SequenceNumberCyclic(length - 100)); + mpdu.insertAtBack(member); + mpdu.insertAtBack(makeShared(std::vector(length - member->getChunkLength().get() - 4, 0))); + auto bytes = mpdu.peekAllAsBytes()->getBytes(); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_COMPUTED); + trailer->setFcs(ethernetFcs(bytes.data(), bytes.size())); + mpdu.insertAtBack(trailer); + packet->insertAtBack(mpdu.peekData()); if (length != 102 && length % 4) packet->insertAtBack(makeShared(B(4 - length % 4))); } @@ -126,13 +171,14 @@ class BoundaryDriver : public cSimpleModule, public cListener } using cListener::finish; void finish() override { - ASSERT(sent == 5 && dropped == 4 && navSet == 1); + ASSERT(sent == 5 && rx->received == 4 && dropped == expectedDrops() && navSet == 1); EV << "MAC aggregate boundary checks passed\n"; } }; Define_Module(BoundaryDriver); %file: test.ned +import inet.linklayer.ieee80211.mac.Rx; import inet.mobility.static.StationaryMobility; import inet.node.inet.AdhocHost; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; @@ -144,6 +190,11 @@ module BoundaryErrorModel extends Ieee80211NistErrorModel parameters: @class(::BoundaryErrorModel); } +simple BoundaryRx extends Rx +{ + parameters: + @class(::BoundaryRx); +} simple BoundaryDriver { parameters: @@ -187,6 +238,7 @@ cmdenv-express-mode = false *.receiver.numWlanInterfaces = 1 *.receiver.wlan[0].opMode = "n(mixed-2.4Ghz)" *.receiver.wlan[0].mac.qosStation = ${qos=false,true} +*.receiver.wlan[0].mac.rx.typename = "BoundaryRx" *.receiver.wlan[0].radio.receiver.errorModel.typename = "BoundaryErrorModel" *.receiver.wlan[0].radio.separateReceptionParts = ${parts=false,true} *.sender.radio.separateReceptionParts = ${parts} diff --git a/tests/module/Ieee80211AmpduReordering_1.test b/tests/module/Ieee80211AmpduReordering_1.test new file mode 100644 index 00000000000..13169e08066 --- /dev/null +++ b/tests/module/Ieee80211AmpduReordering_1.test @@ -0,0 +1,465 @@ +%description: +Real radios send HT aggregates through the production MAC, Rx, HCF, and recipient data service. +Twenty-two scenarios check partial reception, all-member failure, absent agreements, duplicates, delimiter gaps, invalid FCS, and overheard DATA. +Physical SIFS intervals exercise aggregate and individual history for release conditions (i) through (v). +Negative history cases use a long interval, a failed delimiter tail, or an absent prior receipt. +The fixture supplies peer HT capabilities and accepted agreements. It does not test ADDBA negotiation. +A response probe suppresses Block Ack transmission in the first seventeen scenarios. +Three additional scenarios use actual Block Ack transmission and physical SIFS intervals to check retained or cleared aggregate history. +Two negative RIFS scenarios send standalone HT DATA with Block Ack policy before an aggregate or another standalone frame. +A 2 us gap after the 6 us signal extension does not meet the production RIFS condition. +HCF adds the preceding signal extension to the measured gap before it compares that sum with the RIFS duration. +Both scenarios keep the frame after the missing sequence number in the recipient buffer. +Scalar and dimensional radios use long and short GI, whole and separate reception parts, and seed zero. + +%extraargs: -r 0..175 +%postrun-command: python3 check-reordering.py + +%file: AmpduReordering.cc +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/Tx.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class ReorderPhysicalModel : public Ieee80211NistErrorModel +{ + mutable std::map delimiterCalls; + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeChunkSuccessRate(const ISnir *snir, b bitLength) const override { + B length(bitLength); + auto transmission = snir->getReception()->getTransmission(); + auto packet = transmission->getPacket(); + if (length == B(4)) { + int index = delimiterCalls[transmission->getId()]++; + return (packet->isName("delimiter-gap") || packet->isName("delimiter-tail")) && index == 2 ? 0 : 1; + } + return packet->isName("all-failed") || (packet->isName("partial") && + length == B(101) - DATAFRAME_HEADER_MINLENGTH - QOSCONTROL_PART_LENGTH - B(4)) ? 0 : 1; + } +}; +Define_Module(ReorderPhysicalModel); + +class ReorderAcceptedAgreements : public RecipientBlockAckAgreementHandler +{ + public: + void accept(MacAddress peer, int start) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 2, SIMTIME_ZERO); + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; + +class ReorderPhysicalHcf : public Hcf +{ + public: + std::vector delivered; + int aggregateCalls = 0; + int acceptedMembers = 0; + int normalResponses = 0; + int blockResponses = 0; + int corruptions = 0; + int delbas = 0; + bool actualResponses = false; + MacAddress address() const { return mac->getAddress(); } + RecipientBlockAckAgreement *agreement() { return recipientBlockAckAgreementHandler->getAgreement(5, MacAddress("02:00:00:00:00:02")); } + void seed(int scenario) { + Enter_Method("seed"); + actualResponses = scenario >= 17 && scenario < 20; + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:02"), mib->localHtCapabilities, mib->getHtOperation()); + delete recipientBlockAckAgreementHandler; + auto accepted = new ReorderAcceptedAgreements(); + recipientBlockAckAgreementHandler = accepted; + if (scenario != 3) + accepted->accept(MacAddress("02:00:00:00:00:02"), actualResponses ? 0 : (scenario == 14 ? 4095 : (scenario == 16 ? 4094 : 100))); + } + void processLowerAggregate(Packet *packet, AmpduReception reception) override { + aggregateCalls++; + acceptedMembers += reception.members.size(); + Hcf::processLowerAggregate(packet, std::move(reception)); + } + void corruptedFrameReceived() override { + corruptions++; + Hcf::corruptedFrameReceived(); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr& header, + Packet *, const Ptr&) override { + if (header->getType() == ST_ACK) normalResponses++; + if (dynamicPtrCast(header)) blockResponses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) delbas++; + Hcf::processMgmtFrame(packet, header); + } +}; +Define_Module(ReorderPhysicalHcf); + +class ReorderResponseTx : public Tx +{ + public: + void transmitFrame(Packet *packet, const Ptr& header, + simtime_t ifs, bool updateLocalNav, ITx::ICallback *callback) override { + auto hcf = check_and_cast(getModuleByPath("^.hcf")); + if (!hcf->actualResponses && dynamicPtrCast(header)) { + hcf->blockResponses++; + return; + } + Tx::transmitFrame(packet, header, ifs, updateLocalNav, callback); + } +}; +Define_Module(ReorderResponseTx); + +class ReorderContentionProbe : public IContention +{ + public: + int corruptions = 0; + void startContention(int, simtime_t, simtime_t, simtime_t, ICallback *) override { ASSERT(false); } + bool isContentionInProgress() override { return false; } + void mediumStateChanged(bool) override {} + void corruptedFrameReceived() override { corruptions++; } +}; + +class ReorderPhysicalDriver : public cSimpleModule, public cListener, public ITx::ICallback +{ + ReorderPhysicalHcf *hcf = nullptr; + ReorderContentionProbe contention; + int sent = 0; + int actualBlockAckTransmissions = 0; + int unrelatedTransmissions = 0; + int unrelatedCompletions = 0; + simtime_t expectedSecondStart; + const IIeee80211Mode *mode = nullptr; + int scenario() const { return par("scenario"); } + bool actualResponses() const { return scenario() >= 17 && scenario() < 20; } + bool rifsScenario() const { return scenario() >= 20; } + bool twoTransmissions() const { return scenario() == 5 || (scenario() >= 8 && scenario() <= 13) || scenario() >= 17; } + bool individual(bool second) const { + return second ? scenario() == 9 || scenario() == 11 || scenario() == 18 || scenario() == 21 : + scenario() == 10 || scenario() == 11 || rifsScenario(); + } + Packet *member(int sequence, int length, bool standalone) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(scenario() == 7 ? MacAddress("02:00:00:00:00:09") : hcf->address()); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setAddress3(MacAddress("02:00:00:00:00:02")); + header->setTid(5); + header->setAckPolicy(standalone ? BLOCK_ACK : NORMAL_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setDurationField(SIMTIME_ZERO); + auto frame = new Packet("member", header); + frame->insertAtBack(makeShared(B(length) - header->getChunkLength() - B(4))); + auto trailer = makeShared(); + auto bytes = frame->peekAllAsBytes()->getBytes(); + auto fcs = ethernetFcs(bytes.data(), bytes.size()); + trailer->setFcsMode(FCS_COMPUTED); + trailer->setFcs(scenario() == 6 && length == 101 ? fcs ^ 1 : fcs); + frame->insertAtBack(trailer); + return frame; + } + void transmit() { + bool second = sent != 0; + std::vector sequences; + if (actualResponses()) + sequences = {second ? 2 : 0}; + else if ((scenario() >= 8 && scenario() <= 13) || rifsScenario()) + sequences = {second ? 102 : 100}; + else if (scenario() == 14) + sequences = {4095, 1}; + else if (scenario() == 16) + sequences = {4094, 0}; + else if (scenario() == 15) + sequences = {102}; + else + sequences = {100, 101, 102}; + const char *name = scenario() == 1 ? "partial" : scenario() == 2 ? "all-failed" : scenario() == 4 ? "delimiter-gap" : + scenario() == 13 && !second ? "delimiter-tail" : "aggregate"; + Packet *packet = nullptr; + if (individual(second)) + packet = member(sequences.front(), 100, true); + else { + packet = new Packet(name); + for (size_t i = 0; i < sequences.size(); i++) { + int length = 100 + i; + auto delimiter = makeShared(); + delimiter->setLength(length); + packet->insertAtBack(delimiter); + auto frame = member(sequences[i], length, false); + packet->insertAtBack(frame->peekData()); + delete frame; + if (i + 1 < sequences.size() && length % 4) + packet->insertAtBack(makeShared(B(4 - length % 4))); + } + if (scenario() == 13 && !second) { + auto delimiter = makeShared(); + delimiter->setLength(0); + packet->insertAtBack(delimiter); + } + } + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + packet->addTag()->setMode(mode); + sent++; + send(packet, "out"); + } + void transmitUnrelatedControl() { + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:09")); + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet("unrelated-local-control", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(Ieee80211ModeSet::getModeSet("g(mixed)")->getMode(Mbps(6))); + auto tx = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.tx")); + ASSERT(!tx->hasTransmission()); + tx->transmitFrame(packet, header, false, this); + delete packet; + } + public: + void transmissionComplete(Packet *, const Ptr& header) override { + ASSERT(header && header->getType() == ST_CTS); + unrelatedCompletions++; + } + protected: + void initialize() override { + hcf = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); + check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.rx"))->registerContention(&contention); + getModuleByPath("^.sender.radio")->subscribe(transmissionStartedSignal, this); + getModuleByPath("^.receiver.wlan[0].radio")->subscribe(transmissionEndedSignal, this); + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < modes->getNumModes(); i++) { + auto candidate = modes->getMode(i); + if (candidate->getHtMcsIndex() == 0 && candidate->getDataMode()->getBandwidth() == MHz(20) && + candidate->isHtShortGuardInterval() == par("shortGi").boolValue()) + mode = candidate; + } + ASSERT(mode); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("first")); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + auto transmission = check_and_cast(object); + if (signal == transmissionEndedSignal) { + if (!actualResponses()) return; + auto packet = transmission->getPacket(); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto header = packet->peekDataAt(phy->getChunkLength()); + if (dynamicPtrCast(header)) { + actualBlockAckTransmissions++; + if (sent == 1) { + expectedSecondStart = transmission->getEndTime() + mode->getSifsTime(); + scheduleAt(expectedSecondStart, new cMessage(scenario() == 19 ? "unrelated" : "second")); + } + } + else if (header->getType() == ST_CTS) { + ASSERT(scenario() == 19 && sent == 1); + unrelatedTransmissions++; + expectedSecondStart = transmission->getEndTime() + mode->getSifsTime(); + scheduleAt(expectedSecondStart, new cMessage("second")); + } + return; + } + if (actualResponses()) { + if (sent == 2) ASSERT(transmission->getStartTime() == expectedSecondStart); + return; + } + if (sent == 1 && twoTransmissions()) { + auto interval = scenario() == 5 || scenario() == 12 ? SimTime(1, SIMTIME_MS) : mode->getSifsTime(); + if (rifsScenario()) { + ASSERT(mode->getRifsTime() == SimTime(2, SIMTIME_US)); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + auto actualExtension = transmission->getEndTime() - transmission->getStartTime() - + mode->getDuration(B(phy->getLengthField())) + mode->getSignalExtensionTime(); + ASSERT(actualExtension == SimTime(6, SIMTIME_US)); + interval = mode->getRifsTime(); + } + expectedSecondStart = transmission->getEndTime() + interval; + scheduleAt(expectedSecondStart, new cMessage("second")); + } + if (rifsScenario() && sent == 2) ASSERT(transmission->getStartTime() == expectedSecondStart); + } + void handleMessage(cMessage *message) override { + if (message->isName("unrelated")) { + delete message; + transmitUnrelatedControl(); + return; + } + if (message->isName("first")) hcf->seed(scenario()); + delete message; + transmit(); + } + using cListener::finish; + void finish() override { + std::vector expected; + if (scenario() == 0 || scenario() == 5) expected = {100, 101, 102}; + else if (scenario() == 1 || scenario() == 6 || (scenario() >= 8 && scenario() <= 11)) expected = {100, 102}; + else if (scenario() == 4 || scenario() == 12 || scenario() == 13) expected = {100}; + else if (scenario() == 14) expected = {4095, 1}; + else if (scenario() == 16) expected = {4094, 0}; + else if (scenario() == 17 || scenario() == 18) expected = {0, 2}; + else if (scenario() == 19) expected = {0}; + else if (rifsScenario()) expected = {100}; + ASSERT(hcf->delivered == expected); + ASSERT(sent == (twoTransmissions() ? 2 : 1)); + ASSERT(hcf->normalResponses == 0 && hcf->delbas == 0); + ASSERT(contention.corruptions == (scenario() == 2 ? 1 : 0)); + ASSERT(hcf->corruptions == (scenario() == 2 ? 1 : 0)); + if (actualResponses()) { + ASSERT(actualBlockAckTransmissions == (scenario() == 18 ? 1 : 2)); + ASSERT(unrelatedTransmissions == (scenario() == 19 ? 1 : 0)); + ASSERT(unrelatedCompletions == unrelatedTransmissions); + } + if (scenario() == 3) { + ASSERT(!hcf->agreement()); + ASSERT(hcf->blockResponses == 0); + } + else { + auto record = hcf->agreement()->getBlockAckRecord(); + if (rifsScenario()) { + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(100))); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(102))); + ASSERT(hcf->aggregateCalls == (individual(true) ? 0 : 1)); + } + if (scenario() == 2 || scenario() == 7) { + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(100))); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(101))); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(102))); + ASSERT(hcf->blockResponses == 0); + } + if (scenario() == 1 || scenario() == 4 || scenario() == 6) { + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(100))); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(101))); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(102))); + } + } + std::cout << "Physical aggregate reordering passed: scenario=" << scenario() << " delivered="; + for (int sequence : hcf->delivered) std::cout << sequence << ","; + std::cout << "\n"; + } +}; +Define_Module(ReorderPhysicalDriver); + +%file: test.ned +import inet.mobility.static.StationaryMobility; +import inet.node.inet.AdhocHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.Tx; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module ReorderPhysicalModel extends Ieee80211NistErrorModel { parameters: @class(::ReorderPhysicalModel); } +module ReorderPhysicalHcf extends Hcf { parameters: @class(::ReorderPhysicalHcf); } +module ReorderResponseTx extends Tx { parameters: @class(::ReorderResponseTx); } +simple ReorderPhysicalDriver { parameters: int scenario; bool shortGi; @class(::ReorderPhysicalDriver); gates: output out; } +module ReorderPhysicalSender { + parameters: @networkNode; + gates: input in; input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: Ieee80211Radio; + connections allowunconnected: + in --> radio.upperLayerIn; + radioIn --> radio.radioIn; +} +network Test { + submodules: + radioMedium: Ieee80211RadioMedium; + driver: ReorderPhysicalDriver; + sender: ReorderPhysicalSender; + receiver: AdhocHost; + connections: + driver.out --> sender.in; +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 7ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.driver.scenario = ${scenario=0..21} +*.driver.shortGi = ${shortGi=false,true} +*.receiver.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.receiver.wlan[0].mac.qosStation = true +*.receiver.wlan[0].mac.hcf.typename = "ReorderPhysicalHcf" +*.receiver.wlan[0].mac.hcf.isBlockAckSupported = true +*.receiver.wlan[0].mac.tx.typename = "ReorderResponseTx" +*.receiver.wlan[0].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +*.receiver.wlan[0].radio.receiver.errorModel.typename = "ReorderPhysicalModel" +*.receiver.wlan[0].radio.separateReceptionParts = ${parts=false,true} +*.sender.radio.separateReceptionParts = ${parts} +*.sender.radio.opMode = "n(mixed-2.4Ghz)" +*.sender.radio.initialRadioMode = "transmitter" +*.sender.radio.antenna.mobilityModule = "^.^.mobility" +*.radioMedium.signalAnalogRepresentation = ${analog="scalar","dimensional"} +**.radio.signalAnalogRepresentation = ${analog} +**.radio.receiver.bandwidth = 20MHz +*.radioMedium.backgroundNoise.power = -110dBm +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +Physical aggregate reordering passed: + +%file: check-reordering.py +from collections import Counter +from pathlib import Path +import re + +text = Path("test.out").read_text() +errors = Path("test.err").read_text() +assert "Warning:" not in text + errors +assert "Error:" not in text + errors +assert re.findall(r"Run statistics: total (\d+), successful (\d+)([^\n]*)", text) == [("176", "176", "")] +scenarios = re.findall(r"^Physical aggregate reordering passed: scenario=(\d+) delivered=", text, re.MULTILINE) +assert Counter(map(int, scenarios)) == Counter({scenario: 8 for scenario in range(22)}) +print("All 176 physical reordering cases passed without warnings or errors.") + +%contains: stdout +Run statistics: total 176, successful 176 diff --git a/tests/module/Ieee80211CompressedBlockAckInput_1.test b/tests/module/Ieee80211CompressedBlockAckInput_1.test index 25cb7ac198c..7b6ff71bef6 100644 --- a/tests/module/Ieee80211CompressedBlockAckInput_1.test +++ b/tests/module/Ieee80211CompressedBlockAckInput_1.test @@ -71,9 +71,10 @@ class InputDataSpy : public IRecipientQosMacDataService { } void clearPeer(MacAddress peer) override { delegate->clearPeer(peer); } std::vector dataFrameReceived(Packet *packet, const Ptr& header, - IRecipientBlockAckAgreementHandler *agreements, bool interframe = false) override { + IRecipientBlockAckAgreementHandler *agreements, bool interframe = false, + const BlockAckReceiveContext *context = nullptr) override { dataCalls++; - return delegate->dataFrameReceived(packet, header, agreements, interframe); + return delegate->dataFrameReceived(packet, header, agreements, interframe, context); } std::vector controlFrameReceived(Packet *packet, const Ptr& header, IRecipientBlockAckAgreementHandler *agreements) override { diff --git a/tests/protocol/wifi/ampdu-partial-reception.md b/tests/protocol/wifi/ampdu-partial-reception.md index ec040a6e584..800bf947700 100644 --- a/tests/protocol/wifi/ampdu-partial-reception.md +++ b/tests/protocol/wifi/ampdu-partial-reception.md @@ -1,8 +1,8 @@ -# PHY P2: Preserve partial outcomes at the MAC boundary +# Preserve partial PHY outcomes before member extraction This document belongs to PHY P2, **ieee80211: add+change: retain partial HT A-MPDU PHY outcomes**. Its protocol test is [WifiHtAmpduPartialReception.test](common/WifiHtAmpduPartialReception.test). The [PHY plan](../../../plan/pending/ieee80211-ampdu-phy-reception.md) defines the receiver contract and later MAC work. -Before this commit, the receiver samples the aggregate as one packet. It cannot retain a failed member beside successful members. Rx can also inspect an aggregate container as a standalone MAC frame. After this commit, cached PHY decisions retain chunks with error flags. The result packet preserves those flags. Rx rejects the container before standalone FCS or MAC header checks. +The PHY prerequisite retains cached outcomes in chunk flags. Before MAC integration, Rx rejects the complete aggregate before standalone frame checks. After MAC integration, a separate Rx method extracts members and checks their FCS. The fixture therefore observes the envelope before extraction. This change preserves the PHY assertions and the independent CTS control. ## Requirement and model boundary @@ -16,13 +16,13 @@ The fixture uses a production scalar transmitter, medium, receiver, MAC, and Rx. At 1 ms, the driver submits members of 100, 101, and 102 octets. Three alignment octets follow the 101-octet member. The complete PSDU contains 318 octets. The driver selects HT MCS 0 and the run's GI before production radio encapsulation. -At the production Rx drop signal, the observer checks the intact PSDU length and clear container bit-error flag. The indication must report a successful header and complete content. Chunk flags supply the error outcomes. Their offsets are 0, 4, 104, 108, 212, and 216 octets. For typed input, each selected chunk must be incorrect exactly when its length equals the configured failed length. Raw input marks the entire DATA view incorrect. The test uses a distinct padding chunk to preserve the typed boundaries. +An Rx subclass emits an observation signal before it calls production aggregate reception. The observer checks the intact PSDU length and clear container bit-error flag at that signal. The indication must report a successful header and complete content. Chunk flags supply the error outcomes at offsets 0, 4, 104, 108, 212, and 216 octets. For typed input, each selected chunk must be incorrect exactly when its length equals the configured failed length. Raw input marks the complete DATA view incorrect. -The observer reads the indication through OMNeT++ descriptors, which expose generated fields by name. The fixture therefore uses the generic corruption API from the preparation commit. It compiles on the receiver commit's parent without the new indication class. A missing indication fails at runtime. The observer reads each field without a change to protocol state. No conditional check accepts missing data. +The observer reads common indication fields through OMNeT++ descriptors. A missing indication fails at runtime. The observer changes no protocol state. The amended fixture requires the aggregate Rx method from MAC integration. It supplies no execution result for that method's parent. -Rx must reject that container once through its normal drop path. At 2 ms, the driver submits an independent CTS with computed FCS and a 50-microsecond Duration field. The observer requires one 50-microsecond NAV update after the aggregate drop. This control proves that the ordinary acceptance path remains usable after the rejection. +Production Rx must accept no member because these zero-filled members have no valid MAC FCS. At 2 ms, the driver submits an independent CTS with computed FCS and a 50-microsecond Duration field. The observer requires one 50-microsecond NAV update after the aggregate observation. This control checks ordinary reception after the aggregate. -The protocol tester requires `testHtAmpduPartial` followed by `testHtAmpduControl`. These events occur only after their assertions pass. A missing aggregate, missing drop, wrong outcome, or missing control result cannot pass through an empty observation. +The protocol tester requires `testHtAmpduPartial` followed by `testHtAmpduControl`. These events occur only after their assertions pass. A missing aggregate, wrong outcome, or missing control result cannot pass through an empty observation. The four loss patterns combine with both GIs, typed and raw-byte transmission, and whole-signal and separate-part reception. These combinations produce 32 runs. Every run uses seed zero and ends at 4 ms. The deterministic probabilities require no extra seeds. The existing module fixture separately retains dimensional-radio, DCF, malformed-input, and cached-query controls. @@ -35,16 +35,17 @@ sequenceDiagram participant Tester as Protocol tester Driver->>Tx: Three-member HT aggregate Tx->>Receiver: One PPDU through the medium - Receiver->>Receiver: Retain every delimiter and MPDU outcome - Receiver->>Rx: Intact container and copied ranges - Rx->>Tester: Drop with verified partial outcomes + Receiver->>Receiver: Retain cached chunk outcomes + Receiver->>Rx: Intact content with chunk error flags + Rx->>Tester: Observation after envelope assertions + Rx->>Rx: Extract members; reject invalid MAC content Driver->>Tx: Independent CTS with computed FCS Tx->>Receiver: Ordinary PPDU Receiver->>Rx: Decoded CTS Rx->>Tester: Verified NAV update ``` -The parent lacks the copied range indication and the aggregate-safe Rx boundary. Its whole-packet result cannot satisfy these assertions. This commit must retain the six outcomes and complete the later CTS control for every loss pattern. No expected-failure marker suppresses either requirement. +The amended fixture preserves the prerequisite's PHY assertions at the new aggregate Rx entry. Its parent result remains unverified. The comparison below describes an earlier fixture and source range. ## Verification and limits @@ -55,6 +56,6 @@ make MODE=debug -j8 opp_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f 'WifiHtAmpduPartialReception\.test$' --no-concurrent ``` -The installed runner can require the temporary compatible descriptor described in the [execution record](../../../plan/pending/ieee80211-ampdu-phy-execution.md). The expected result is 32 successful runs and `PROTOCOLTEST wifi_ht_ampdu_partial_reception: PASS`. The [comparison record](ampdu-partial-reception-comparison.md) reports the parent and commit executions with identical fixture bytes. +The installed runner can require the temporary compatible descriptor described in the [execution record](../../../plan/pending/ieee80211-ampdu-phy-execution.md). The expected result is 32 successful runs and `PROTOCOLTEST wifi_ht_ampdu_partial_reception: PASS`. The [comparison record](ampdu-partial-reception-comparison.md) reports historical executions of an earlier fixture. -This fixture verifies PHY outcomes at the current MAC boundary. It does not verify member acceptance, upper delivery, implicit Block Ack, or correlated FEC errors. The diagram follows the actual send, receive, drop, and NAV sequence. Diagram rendering remains unverified when no Mermaid renderer is available. +This fixture checks PHY outcomes before member extraction and checks rejection of invalid MAC content. It does not prove valid member acceptance, upper delivery, implicit Block Ack, or correlated FEC errors. The diagram describes the test sequence. Diagram rendering remains unverified. diff --git a/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test b/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test index fe32973a0b6..c77f97fd088 100644 --- a/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test +++ b/tests/protocol/wifi/common/WifiHtAmpduPartialReception.test @@ -1,6 +1,7 @@ %description: -Check copied partial HT A-MPDU outcomes through physical reception and production Rx rejection. -Typed input retains partial chunk errors. Byte input retains one incorrect DATA region. The aggregate bytes remain available at the drop. +Check copied partial HT A-MPDU outcomes before production Rx extracts members. +Typed input retains partial chunk errors. Byte input retains one incorrect DATA region. The fixture observes the complete aggregate at the Rx entry. +The zero-filled members have no valid MAC FCS, so Rx accepts no member. A later valid CTS must set NAV. The protocol tester requires both observations in order. Four loss patterns, both byte representations, both GIs, and both reception-part modes form 32 runs. @@ -33,6 +34,19 @@ class HtAmpduPartialErrorModel : public Ieee80211NistErrorModel }; Define_Module(HtAmpduPartialErrorModel); +class HtAmpduPartialRx : public Rx +{ + public: + AmpduReception lowerAggregateReceived(Packet *packet) override { + Enter_Method("lowerAggregateReceived"); + emit(registerSignal("testHtAmpduEnvelope"), packet); + auto result = Rx::lowerAggregateReceived(packet); + ASSERT(!result.anyCorrect && result.members.empty()); + return result; + } +}; +Define_Module(HtAmpduPartialRx); + class HtAmpduPartialDriver : public cSimpleModule, public cListener { int sent = 0; @@ -41,13 +55,13 @@ class HtAmpduPartialDriver : public cSimpleModule, public cListener protected: void initialize() override { auto rx = getModuleByPath("^.receiver.wlan[0].mac.rx"); - rx->subscribe(packetDroppedSignal, this); + rx->subscribe(registerSignal("testHtAmpduEnvelope"), this); rx->subscribe(Rx::navChangedSignal, this); scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("send")); } void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { Enter_Method_Silent(); - ASSERT(signal == packetDroppedSignal && !dropped); + ASSERT(signal == registerSignal("testHtAmpduEnvelope") && !dropped); auto packet = check_and_cast(object); ASSERT(packet->isName("partial") && !packet->cPacket::hasBitError() && packet->hasBitError()); ASSERT(packet->getDataLength() == B(318)); @@ -147,6 +161,11 @@ import inet.node.inet.AdhocHost; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.linklayer.ieee80211.mac.Rx; +simple HtAmpduPartialRx extends Rx { + parameters: + @class(::HtAmpduPartialRx); +} module HtAmpduPartialErrorModel extends Ieee80211NistErrorModel { parameters: @class(::HtAmpduPartialErrorModel); @@ -193,6 +212,7 @@ seed-set = 0 *.receiver.numWlanInterfaces = 1 *.receiver.wlan[0].opMode = "n(mixed-2.4Ghz)" *.receiver.wlan[0].mac.qosStation = true +*.receiver.wlan[0].mac.rx.typename = "HtAmpduPartialRx" *.receiver.wlan[0].radio.receiver.errorModel.typename = "HtAmpduPartialErrorModel" *.receiver.wlan[0].radio.receiver.errorModel.failedLength = ${failedLength} *.receiver.wlan[0].radio.separateReceptionParts = ${parts=false,true} diff --git a/tests/protocol/wifi/common/WifiLocalInterframeInputs.test b/tests/protocol/wifi/common/WifiLocalInterframeInputs.test index 7123461df5c..8f40bead894 100644 --- a/tests/protocol/wifi/common/WifiLocalInterframeInputs.test +++ b/tests/protocol/wifi/common/WifiLocalInterframeInputs.test @@ -98,12 +98,24 @@ class LocalInputRx : public Rx { } bool lowerFrameReceived(Packet *packet) override { auto time = packet->findTag(); + if (!time) { + BlockAckReceiveContext stale; + stale.previousAggregate = std::make_shared(); + stale.followsAggregate = true; + auto interval = packet->addTagIfAbsent(); + interval->setRelated(true); + interval->setBlockAckContext(stale); + } auto start = time ? time->getStartTime() : SimTime(-1); auto end = time ? time->getEndTime() : SimTime(-1); bool ok = Rx::lowerFrameReceived(packet); if (ok) { accepted++; related = packet->getTag()->getRelated(); + if (!time) { + const auto& context = packet->getTag()->getBlockAckContext(); + ASSERT(!related && !context.previousAggregate && !context.followsAggregate); + } relations.push_back(related); eligible.push_back(precedingFrame.has_value()); lastStart = start; diff --git a/tests/unit/Ieee80211TxNavChoice_1.test b/tests/unit/Ieee80211TxNavChoice_1.test index 242ef87191c..50942c6885a 100644 --- a/tests/unit/Ieee80211TxNavChoice_1.test +++ b/tests/unit/Ieee80211TxNavChoice_1.test @@ -24,6 +24,9 @@ class NavRx : public IRx { void transmissionStateChanged(physicallayer::IRadio::TransmissionState) override {} void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart) override {} bool lowerFrameReceived(Packet *) override { return false; } + AmpduReception lowerAggregateReceived(Packet *) override { + throw cRuntimeError("NavRx does not receive aggregate frames in this fixture"); + } }; class ForwardingTx : public Tx { From 015be5c07119a440b2245259118dc3606ac63d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 14:01:53 +0100 Subject: [PATCH 19/24] doc: comment: explain HT aggregate recipient contracts Before this commit, the documentation covers aggregate preparation but lacks the recipient contracts. Custom components lack instructions for member validation and an implicit response. After this commit, the user guide explains eligible recipient agreements, independent member acceptance, and the SIFS response. The migration guide describes Rx ownership, receipt context, agreement inactivity, response construction, and response rates. The release note distinguishes recipient support from local aggregate transmission, which remains unavailable. This commit changes no runtime behavior. Change: doc | comment | - | ieee80211-implicit-blockack --- .../ieee80211-implicit-blockack.rst | 8 +++++- doc/src/users-guide/ch-80211.rst | 26 +++++++++++++++---- .../ieee80211-implicit-blockack.txt | 6 +++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst b/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst index f4b6b7f05a9..9fee5959629 100644 --- a/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst +++ b/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst @@ -5,6 +5,12 @@ The default MPDU policy leaves aggregate transmission disabled. ``HtMpduAggregat Custom QoS data services implement batch preparation, validation, commitment, and construction through ``IOriginatorQosMacDataService``. Each new member owns one reversible preview. The batch borrows canonical members from the in-progress queue. Commitment validates every queued source before the first extraction. Failed preparation preserves sequence state and independent deadlines. +``IRx::lowerAggregateReceived()`` returns separately owned correct members and immutable delimiter history. The caller retains the envelope. Custom recipient services pass ``BlockAckReceiveContext`` to the reorder owner. This context supplies physical interval and delimiter facts; it does not replace receipt state. Custom agreement handlers implement ``implicitBlockAckRequested()`` to refresh inactivity for valid implicit DATA. + +Every custom rate selector implements ``computeCompressedBlockAckResponseMode()`` for implicit callers. A selector without this capability must throw a diagnostic from its concrete override. Explicit Compressed BAR and implicit DATA share this rate calculation without a fabricated BAR. + +Custom recipient procedures construct a Compressed response directly from peer, TID, and current agreement state. Local aggregate transmission remains unavailable. Custom Tx implementations need no aggregate submission operation at this boundary. + IEEE 802.11 HT aggregate construction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -14,4 +20,4 @@ Custom ``IMpduAggregation`` implementations must supply two HT operations. ``com The existing pointer overload retains its consuming contract. The supplied builder copies region tags from each source's data range to the member's position. It inserts alignment padding and zero-length delimiters between members as necessary. The final member has no padding. A singleton retains its delimiter, so HT-SIG byte conversion preserves its aggregate identity. These construction operations do not enable aggregate selection or implicit Block Ack responses. -The default ``Rx`` refuses aggregate containers before standalone FCS or MAC header checks. Custom receive modules must keep these containers separate from standalone frame parsing. Member acceptance and implicit responses remain unavailable at this boundary. +The default ``Rx`` sends aggregate containers through a separate member-validation path before standalone FCS or MAC header checks. A failed common header or incomplete content prevents member acceptance. Custom receive modules must keep aggregate containers separate from standalone frame parsing until they implement member acceptance. MPDU deaggregation can extract bounded recovered members from incomplete content. Extraction alone grants no MAC acceptance. diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index ef4e91539d5..04302eebf24 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -642,17 +642,33 @@ default policy is :ned:`BasicMsduAggregationPolicy` with these parameters: - :par:`aggregationLengthThreshold`: the smallest aggregated payload that makes an A-MSDU. The default, ``-1``, means no check. -Set ``msduAggregationPolicy.typename = ""`` to turn aggregation off. A-MSDU -aggregation needs packets in the queue; it has an effect only when the -traffic source is faster than the channel. A-MPDU aggregation is not -available: the ``mpduAggregationPolicy`` slot exists, but the default policy -never aggregates, and the MAC does not build A-MPDU frames. +Set ``msduAggregationPolicy.typename = ""`` to turn A-MSDU aggregation off. A-MSDU aggregation needs packets in the queue. The default MPDU policy leaves A-MPDU transmission disabled. The MAC can receive HT aggregates under an eligible Compressed Block Ack agreement. Local aggregate transmission remains unavailable. .. code-block:: ini **.wlan[*].mac.qosStation = true **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 3000 +.. _ug:sec:80211:htampdu: + +HT aggregate reception and implicit Block Ack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Configure an HT QoS station with Compressed Block Ack support. An accepted immediate Compressed Block Ack agreement must exist for the peer and TID before the recipient accepts aggregate DATA. + +.. code-block:: ini + + **.wlan[*].opMode = "n(mixed-2.4Ghz)" + **.wlan[*].mac.qosStation = true + **.wlan[*].mac.hcf.isBlockAckSupported = true + **.wlan[*].mac.hcf.blockAckVariant = "compressed" + +The receiver checks each member against its cached PHY outcome and FCS. One incorrect member does not invalidate a correct neighbor. The recipient records accepted members and sends one Compressed Block Ack after SIFS. Delimiter history and physical interval facts control reorder release. + +The receiver accepts HT20 long-GI aggregates and advertised HT20 short-GI aggregates. Both peers must select compatible Block Ack profiles because ADDBA does not negotiate the profile selector. An active Basic agreement causes a diagnostic for implicit Compressed DATA. DATA without an eligible agreement creates no receipt state or response. + +These changes add recipient support. They do not enable local aggregate transmission. + .. _ug:sec:80211:ack: Acknowledgement and retries diff --git a/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt b/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt index f2e0e97d295..c2f35a27674 100644 --- a/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt +++ b/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt @@ -1,7 +1,9 @@ -IEEE 802.11 HT aggregate preparation +IEEE 802.11 HT aggregate preparation and reception HT capability exchange now retains the peer receiver's minimum MPDU start spacing. The MIB parameter htMinimumMpduStartSpacing selects the encoded local receiver limit from zero through seven. Zero preserves the default with no spacing restriction. The HT aggregate builder checks receiver byte limits, spacing, PPDU duration, and L-SIG length. It retains each source Packet. The QoS service can prepare independent members before it extracts queued sources. Each member retains its sequence number and deadline. Failed preparation preserves queued sources. -Custom aggregation policies, aggregate builders, and QoS data services require the migration changes. The default MPDU policy leaves aggregation disabled. Local aggregate transmission and implicit Block Ack responses remain unavailable at this boundary. Rx refuses failed aggregate containers before standalone MAC access. +Rx now checks each aggregate member against its PHY outcome and FCS. Correct members can enter receipt and reorder state under an eligible Compressed agreement. The recipient sends one implicit Compressed Block Ack after SIFS. One correct member prevents EIFS for that PPDU. Delimiter history and physical interval facts control reorder release without a second receipt record. + +Custom aggregation policies, aggregate builders, QoS data services, Rx, recipient services, rate selectors, and response procedures require the migration changes. The default MPDU policy leaves aggregation disabled. Local aggregate transmission remains unavailable at this boundary. Implicit DATA without an eligible agreement creates no receipt state or response. From 1fdaa6333bd42a886d1b408e437f10db302ee87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 00:31:53 +0100 Subject: [PATCH 20/24] ieee80211: refactor: extract MPDU finalization from transmission Before this commit, Tx sets addresses and calculates the FCS inside the single-frame submission method. The later aggregate sender needs that same operation for each independent member before it constructs the aggregate. After this commit, Tx exposes that operation through finalizeMpdu. Ordinary transmission calls the helper at the same point as before. The frame copy, IFS timer, transmission start, completion callback, and NAV update retain their existing order. For example, an ordinary frame with computed FCS receives its local transmitter address before FCS calculation. The extracted helper preserves that result. The physical transmission-completion and Compressed exchange fixtures check the ordinary path. Aggregate submission and start callbacks remain outside this commit. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | refactor | test migration | ieee80211-implicit-blockack --- src/inet/linklayer/ieee80211/mac/Tx.cc | 24 ++++++++++++------- src/inet/linklayer/ieee80211/mac/Tx.h | 1 + .../linklayer/ieee80211/mac/contract/ITx.h | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Tx.cc b/src/inet/linklayer/ieee80211/mac/Tx.cc index 52f046613c0..53a9b3e0866 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.cc +++ b/src/inet/linklayer/ieee80211/mac/Tx.cc @@ -50,6 +50,21 @@ void Tx::transmitFrame(Packet *packet, const Ptr& head ASSERT(this->txCallback == nullptr); this->txCallback = txCallback; this->updateLocalNav = updateLocalNav; + finalizeMpdu(packet); + this->frame = packet->dup(); + ASSERT(!endIfsTimer->isScheduled() && !transmitting); // we are idle + if (ifs == 0) { + // do directly what handleMessage() would do + transmitting = true; + mac->sendDownFrame(frame->dup()); + } + else + scheduleAfter(ifs, endIfsTimer); +} + +void Tx::finalizeMpdu(Packet *packet) +{ + Enter_Method("finalizeMpdu"); auto macAddressInd = packet->addTagIfAbsent(); const auto& updatedHeader = packet->removeAtFront(); if (auto oneAddressHeader = dynamicPtrCast(updatedHeader)) { @@ -72,15 +87,6 @@ void Tx::transmitFrame(Packet *packet, const Ptr& head delete[] buffer; } packet->insertAtBack(updatedTrailer); - this->frame = packet->dup(); - ASSERT(!endIfsTimer->isScheduled() && !transmitting); // we are idle - if (ifs == 0) { - // do directly what handleMessage() would do - transmitting = true; - mac->sendDownFrame(frame->dup()); - } - else - scheduleAfter(ifs, endIfsTimer); } void Tx::radioTransmissionFinished() diff --git a/src/inet/linklayer/ieee80211/mac/Tx.h b/src/inet/linklayer/ieee80211/mac/Tx.h index 7e71e6a57ea..91d43d4013c 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.h +++ b/src/inet/linklayer/ieee80211/mac/Tx.h @@ -42,6 +42,7 @@ class INET_API Tx : public SimpleModule, public ITx virtual void transmitFrame(Packet *packet, const Ptr& header, bool updateLocalNav, ITx::ICallback *txCallback) override; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, bool updateLocalNav, ITx::ICallback *txCallback) override; + void finalizeMpdu(Packet *packet) override; [[nodiscard]] bool hasTransmission() const override { return txCallback != nullptr; } virtual void radioTransmissionFinished() override; }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index 63e16367533..b9d5a1c92be 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/ITx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/ITx.h @@ -56,6 +56,7 @@ class INET_API ITx */ virtual void transmitFrame(Packet *packet, const Ptr& header, bool updateLocalNav, ICallback *callback) = 0; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, bool updateLocalNav, ICallback *callback) = 0; + virtual void finalizeMpdu(Packet *packet) = 0; /** * The query returns true while Tx retains an accepted frame, * including any wait before transmission and the transmission itself. From f6605c321a42504926fe8dae012422a4a4efcbce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 00:36:01 +0100 Subject: [PATCH 21/24] ieee80211: add: transmit HT aggregates with complete recovery Before this commit, the recipient can receive injected aggregates and return an implicit response. The local originator can prepare members but cannot submit an aggregate or resolve its response. After this commit, HCF admits the complete exchange under agreement, deadline, mode, and airtime limits. RTS selection uses the complete PSDU after each batch reduction. At actual DATA submission, HCF validates the preparation, commits new members, and asks Tx to finalize every member. Tx submits the resulting aggregate. Cancellation before commitment preserves queued sources. Failed RTS updates contention without a DATA attempt or member retry charge. For example, loss of the middle member retains that member's sequence number and deadline for selective retry. A matched Compressed response completes the other members independently. If the implicit response fails, one BAR probe precedes further DATA. Actual BAR submission consumes the probe requirement. Probe failure permits DATA retry without duplicate DATA feedback. An absent recipient agreement can trigger DELBA, so continuous arrivals do not conceal a stale agreement. Admission, transmission-start, response-window, exchange, and recovery fixtures cover actual submission, cancellation, selective results, ownership, and probe bounds. N_AmpduImplicitBa uses real association and application traffic across both AP hops. Its assertions require sixty deliveries in each deterministic case and a retried aggregate member after partial loss. The same fixture compiles on the parent. Both parent cases deliver sixty payloads but miss the required source aggregate event. The protocol tester reports FAIL, and the post-run checker rejects the missing PASS markers. N_Ampdu now checks the complete supported exchange and removes its previously approved expected-failure marker. The transmit step, callbacks, response wait, cancellation, and recovery enter together because the first production sender needs complete terminal paths. Earlier commits isolate extraction, response helpers, finalization, and the recipient. Local aggregate transmission remains restricted to the fixed-rate HT20 long-GI mixed-preamble profile. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: src.ieee80211 | behavior.add | test expected whatsnew migration | ieee80211-implicit-blockack --- src/inet/linklayer/ieee80211/mac/Tx.cc | 63 +- src/inet/linklayer/ieee80211/mac/Tx.h | 6 +- .../blockack/OriginatorBlockAckAgreement.h | 3 + .../mac/contract/FrameTransmissionDetails.msg | 1 + .../mac/contract/IFrameSequenceHandler.h | 1 + .../mac/contract/IOriginatorQoSAckPolicy.h | 2 + .../linklayer/ieee80211/mac/contract/ITx.h | 11 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 5 + .../ieee80211/mac/coordinationfunction/Dcf.h | 1 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 346 ++++++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 9 +- .../mac/framesequence/FrameSequenceContext.cc | 22 + .../mac/framesequence/FrameSequenceContext.h | 8 +- .../mac/framesequence/FrameSequenceHandler.cc | 20 +- .../mac/framesequence/FrameSequenceHandler.h | 1 + .../mac/framesequence/FrameSequenceStep.h | 13 + .../framesequence/PrimitiveFrameSequences.cc | 40 +- .../framesequence/PrimitiveFrameSequences.h | 12 + .../ieee80211/mac/framesequence/TxOpFs.cc | 9 +- .../mac/originator/OriginatorQosAckPolicy.cc | 12 + .../mac/originator/OriginatorQosAckPolicy.h | 3 + .../ieee80211/mac/originator/QosAckHandler.cc | 22 +- .../ieee80211/mac/originator/QosAckHandler.h | 2 + .../mac/originator/QosRecoveryProcedure.cc | 5 + .../mac/originator/QosRecoveryProcedure.h | 1 + tests/module/Ieee80211AmpduAdmission_1.test | 701 ++++++++++++++++++ tests/module/Ieee80211AmpduExchange_1.test | 372 ++++++++++ tests/module/Ieee80211AmpduRecovery_1.test | 359 +++++++++ tests/module/Ieee80211AmpduReordering_1.test | 1 + .../Ieee80211AmpduResponseWindow_1.test | 349 +++++++++ tests/module/Ieee80211AmpduTxStart_1.test | 226 ++++++ .../Ieee80211RxTransmissionCompletion_1.test | 1 + tests/protocol/wifi/11n/N_Ampdu.test | 53 +- .../protocol/wifi/11n/N_AmpduImplicitBa.test | 154 ++++ .../WifiBlockAckBarWithoutAgreement.test | 1 + .../common/WifiLocalInterframeInputs.test | 1 + .../Ieee80211AmpduResponseValidation_1.test | 45 ++ tests/unit/Ieee80211BlockAckScheduling_1.test | 41 + tests/unit/Ieee80211TxNavChoice_1.test | 1 + 39 files changed, 2863 insertions(+), 60 deletions(-) create mode 100644 tests/module/Ieee80211AmpduAdmission_1.test create mode 100644 tests/module/Ieee80211AmpduExchange_1.test create mode 100644 tests/module/Ieee80211AmpduRecovery_1.test create mode 100644 tests/module/Ieee80211AmpduResponseWindow_1.test create mode 100644 tests/module/Ieee80211AmpduTxStart_1.test create mode 100644 tests/protocol/wifi/11n/N_AmpduImplicitBa.test create mode 100644 tests/unit/Ieee80211AmpduResponseValidation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Tx.cc b/src/inet/linklayer/ieee80211/mac/Tx.cc index 53a9b3e0866..1a3d8def863 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.cc +++ b/src/inet/linklayer/ieee80211/mac/Tx.cc @@ -47,17 +47,14 @@ void Tx::transmitFrame(Packet *packet, const Ptr& head void Tx::transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, bool updateLocalNav, ITx::ICallback *txCallback) { Enter_Method("transmitFrame(\"%s\")", packet->getName()); - ASSERT(this->txCallback == nullptr); + ASSERT(!hasTransmission()); this->txCallback = txCallback; this->updateLocalNav = updateLocalNav; finalizeMpdu(packet); this->frame = packet->dup(); - ASSERT(!endIfsTimer->isScheduled() && !transmitting); // we are idle - if (ifs == 0) { - // do directly what handleMessage() would do - transmitting = true; - mac->sendDownFrame(frame->dup()); - } + ASSERT(!endIfsTimer->isScheduled() && !transmitting); + if (ifs == 0) + startTransmission(); else scheduleAfter(ifs, endIfsTimer); } @@ -89,12 +86,61 @@ void Tx::finalizeMpdu(Packet *packet) packet->insertAtBack(updatedTrailer); } +void Tx::transmitAggregate(simtime_t ifs, IAggregateCallback *callback) +{ + Enter_Method("transmitAggregate"); + ASSERT(!hasTransmission() && !endIfsTimer->isScheduled() && !transmitting && !frame); + aggregateCallback = callback; + updateLocalNav = false; + if (ifs == 0) + startTransmission(); + else + scheduleAfter(ifs, endIfsTimer); +} + +bool Tx::cancelPendingAggregate(IAggregateCallback *callback) +{ + Enter_Method("cancelPendingAggregate"); + if (aggregateCallback != callback || transmitting) + return false; + cancelEvent(endIfsTimer); + aggregateCallback = nullptr; + return true; +} + +void Tx::startTransmission() +{ + if (aggregateCallback) { + auto packet = aggregateCallback->prepareAggregateTransmission(); + if (!packet) { + auto callback = aggregateCallback; + aggregateCallback = nullptr; + callback->aggregateTransmissionCancelled(); + return; + } + frame = packet->dup(); + } + transmitting = true; + if (txCallback) + txCallback->frameTransmissionStarted(frame); + mac->sendDownFrame(frame->dup()); +} + void Tx::radioTransmissionFinished() { Enter_Method("radioTransmissionFinished"); if (transmitting) { EV_DETAIL << "Tx: radioTransmissionFinished()\n"; transmitting = false; + if (aggregateCallback) { + auto completedFrame = frame; + auto callback = aggregateCallback; + frame = nullptr; + aggregateCallback = nullptr; + callback->transmissionComplete(completedFrame, nullptr); + delete completedFrame; + return; + } ASSERT(txCallback != nullptr); const auto& header = frame->peekAtFront(); auto duration = header->getDurationField(); @@ -115,8 +161,7 @@ void Tx::handleMessage(cMessage *msg) { if (msg == endIfsTimer) { EV_DETAIL << "Tx: endIfsTimer expired\n"; - transmitting = true; - mac->sendDownFrame(frame->dup()); + startTransmission(); } else ASSERT(false); diff --git a/src/inet/linklayer/ieee80211/mac/Tx.h b/src/inet/linklayer/ieee80211/mac/Tx.h index 91d43d4013c..91ff8d35555 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.h +++ b/src/inet/linklayer/ieee80211/mac/Tx.h @@ -24,6 +24,7 @@ class INET_API Tx : public SimpleModule, public ITx { protected: ITx::ICallback *txCallback = nullptr; + ITx::IAggregateCallback *aggregateCallback = nullptr; Ieee80211Mac *mac = nullptr; IRx *rx = nullptr; Packet *frame = nullptr; @@ -35,6 +36,7 @@ class INET_API Tx : public SimpleModule, public ITx virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void handleMessage(cMessage *msg) override; + void startTransmission(); public: Tx() {} @@ -42,8 +44,10 @@ class INET_API Tx : public SimpleModule, public ITx virtual void transmitFrame(Packet *packet, const Ptr& header, bool updateLocalNav, ITx::ICallback *txCallback) override; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, bool updateLocalNav, ITx::ICallback *txCallback) override; + void transmitAggregate(simtime_t ifs, IAggregateCallback *callback) override; + bool cancelPendingAggregate(IAggregateCallback *callback) override; void finalizeMpdu(Packet *packet) override; - [[nodiscard]] bool hasTransmission() const override { return txCallback != nullptr; } + [[nodiscard]] bool hasTransmission() const override { return txCallback != nullptr || aggregateCallback != nullptr; } virtual void radioTransmissionFinished() override; }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index c760205173a..a6363c5dded 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -34,8 +34,11 @@ class INET_API OriginatorBlockAckAgreement : public cObject bool isAddbaRequestSent = false; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; + bool pendingProbe = false; public: + bool hasPendingProbe() const { return pendingProbe; } + void setPendingProbe(bool value) { pendingProbe = value; } OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported) : receiverAddr(receiverAddr), tid(tid), diff --git a/src/inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails.msg b/src/inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails.msg index 383dbba4c5b..8b8b089c93d 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails.msg +++ b/src/inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails.msg @@ -13,6 +13,7 @@ enum FrameTransmissionStatus FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED = 0; FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED = 1; FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION = 2; + FRAME_TRANSMISSION_STATUS_UNACKNOWLEDGED = 3; } // diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index d2002a5ffdc..a7c54243d39 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -46,6 +46,7 @@ class INET_API IFrameSequenceHandler virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, ICallback *callback) = 0; virtual void processResponse(Packet *frame) = 0; virtual void transmissionComplete() = 0; + virtual void transmissionCancelled() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; // End a response wait after recipient processing. The sequence suppresses failure for retired DATA or management frames. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 641e21e8ecd..165dd477385 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -21,6 +21,8 @@ class INET_API IOriginatorQoSAckPolicy { public: virtual ~IOriginatorQoSAckPolicy() {} + virtual void setProbeEligibility(std::function predicate) = 0; + virtual simtime_t getImplicitBlockAckTimeout(const physicallayer::IIeee80211Mode *responseMode) const = 0; virtual bool isAckNeeded(const Ptr& header) const = 0; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index b9d5a1c92be..06dc26851c8 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/ITx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/ITx.h @@ -26,6 +26,14 @@ class INET_API ITx virtual ~ICallback() {} virtual void transmissionComplete(Packet *packet, const Ptr& header) = 0; + virtual void frameTransmissionStarted(Packet *packet) = 0; + }; + + class INET_API IAggregateCallback : public ICallback { + public: + // The caller validates and finalizes members at the actual submission boundary. + virtual Packet *prepareAggregateTransmission() = 0; + virtual void aggregateTransmissionCancelled() = 0; }; public: @@ -56,6 +64,9 @@ class INET_API ITx */ virtual void transmitFrame(Packet *packet, const Ptr& header, bool updateLocalNav, ICallback *callback) = 0; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, bool updateLocalNav, ICallback *callback) = 0; + virtual void transmitAggregate(simtime_t ifs, IAggregateCallback *callback) = 0; + // Cancellation removes only a delayed request. It cannot cancel a submitted PPDU. + virtual bool cancelPendingAggregate(IAggregateCallback *callback) = 0; virtual void finalizeMpdu(Packet *packet) = 0; /** * The query returns true while Tx retains an accepted frame, diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 17c1b34c928..11d78b932f2 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -268,6 +268,11 @@ FrameSequenceContext *Dcf::buildContext() return new FrameSequenceContext(mac->getAddress(), modeSet, channelAccess->getInProgressFrames(), rtsProcedure, rtsPolicy, nonQoSContext, nullptr); } +void Dcf::frameTransmissionStarted(Packet *packet) +{ + // DCF has no state transition at submission. +} + void Dcf::transmissionComplete(Packet *packet, const Ptr& header) { Enter_Method("transmissionComplete"); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 7a812ffafc1..db474721363 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -113,6 +113,7 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleStartRxTimer(simtime_t timeout) override; // ITx::ICallback + virtual void frameTransmissionStarted(Packet *packet) override; virtual void transmissionComplete(Packet *packet, const Ptr& header) override; // IProcedureCallback diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b9963023249..a8fab1643fb 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -18,6 +18,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -68,6 +69,12 @@ void Hcf::initialize(int stage) originatorDataService = check_and_cast(getSubmodule(("originatorMacDataService"))); recipientDataService = check_and_cast(getSubmodule("recipientMacDataService")); originatorAckPolicy = check_and_cast(getSubmodule("originatorAckPolicy")); + originatorAckPolicy->setProbeEligibility([this](InProgressFrames *frames, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setTid(tid); + return edca->getEdcaf(edca->classifyFrame(header))->getInProgressFrames() == frames; + }); recipientAckPolicy = check_and_cast(getSubmodule("recipientAckPolicy")); singleProtectionMechanism = check_and_cast(getSubmodule("singleProtectionMechanism")); rtsProcedure = new RtsProcedure(); @@ -160,7 +167,12 @@ std::set Hcf::getReferencedDataFrames() const auto context = frameSequenceHandler->getContext(); for (int i = 0; i < context->getNumSteps(); i++) { auto step = context->getStep(i); - if (auto transmit = dynamic_cast(step)) + if (auto aggregate = dynamic_cast(step)) { + for (auto member : aggregate->getPreparedData()->batch->members) + if (context->getInProgressFrames()->containsFrame(member)) + frames.insert(member); + } + else if (auto transmit = dynamic_cast(step)) frames.insert(transmit->getFrameToTransmit()); if (auto rts = dynamic_cast(step)) frames.insert(rts->getProtectedFrame()); @@ -583,12 +595,15 @@ void Hcf::completeTransfers() void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) { + bool cancelledAggregate = false; if (frameSequenceHandler->isSequenceRunning()) { auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); if (prepared) { auto header = prepared->frame->peekAtFront(); - if (header->getReceiverAddress() == peer && header->getTid() == tid) + if (header->getReceiverAddress() == peer && header->getTid() == tid) { prepared->cancelled = true; + cancelledAggregate = prepared->batch && tx->cancelPendingAggregate(this); + } } } for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { @@ -645,6 +660,8 @@ void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highe if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) frames->clearDroppedFrames(); } + if (cancelledAggregate) + aggregateTransmissionCancelled(); } void Hcf::firstDataDiscarded(const Ptr& header) @@ -1400,6 +1417,15 @@ bool Hcf::dispatchAgreementAction(const Ptr& header) void Hcf::transmissionComplete(Packet *packet, const Ptr& header) { Enter_Method("transmissionComplete"); + if (!header) { + auto owner = edca->getChannelOwner(); + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + auto step = check_and_cast(frameSequenceHandler->getContext()->getLastStep()); + step->setPpduEnd(simTime()); + owner->getTxopProcedure()->recordTransmittedDuration(prepared->duration); + frameSequenceHandler->transmissionComplete(); + return; + } auto edcaf = edca->getChannelOwner(); if (edcaf) { auto step = check_and_cast(frameSequenceHandler->getContext()->getLastStep()); @@ -1422,7 +1448,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) auto edcaf = edca->getChannelOwner(); auto context = frameSequenceHandler->getContext(); auto prepared = context->getQoSContext()->preparedData.get(); - if (prepared && prepared->frame == packet && prepared->preparation) { + if (prepared && prepared->frame == packet && (prepared->preparation || prepared->batch)) { // IEEE Std 802.11-2024, 10.23.2.2 and 10.23.2.12.1: BA RTS failure updates QSRC/CW. // The uncommitted sources retain their lifetimes. No provisional sequence owns an SRC entry. if (validatePreparedData(*prepared, simTime())) @@ -1479,6 +1505,18 @@ void Hcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); auto edcaf = edca->getChannelOwner(); + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + if (prepared && prepared->batch && prepared->envelope.get() == packet) { + if (prepared->cancelled) + return; + for (auto member : prepared->batch->members) { + if (!edcaf->getInProgressFrames()->containsFrame(member)) + continue; + edcaf->emit(packetSentToPeerSignal, member); + edcaf->getAckHandler()->processTransmittedImplicitBlockAckMember(member->peekAtFront()); + } + return; + } if (edcaf) edcaf->emit(packetSentToPeerSignal, packet); if (obsoleteActions.count(packet->getId()) || @@ -1575,6 +1613,11 @@ void Hcf::originatorProcessTransmittedControlFrame(const PtrgetContext()->getQoSContext()->preparedData.get(); + if (prepared && prepared->batch && prepared->envelope.get() == failedPacket) { + processAggregateOutcome(nullptr); + return; + } auto failedHeader = failedPacket->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (obsoleteActions.count(failedPacket->getId()) || @@ -1645,6 +1688,11 @@ void Hcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTra { Enter_Method("originatorProcessReceivedFrame"); emit(packetReceivedFromPeerSignal, receivedPacket); + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + if (prepared && prepared->batch && prepared->envelope.get() == lastTransmittedPacket) { + processAggregateOutcome(receivedPacket->peekAtFront()); + return; + } if (obsoleteActions.count(lastTransmittedPacket->getId()) || (dynamicPtrCast(lastTransmittedPacket->peekAtFront()) && !edca->getChannelOwner()->getInProgressFrames()->containsFrame(lastTransmittedPacket))) @@ -1727,6 +1775,11 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptrmembers.size() != 1 || !prepared.batch->queued.empty())) + return false; auto header = prepared.frame->peekAtFront(); // IEEE Std 802.11-2024, 10.23.2.9: the same-size single-MPDU retry exception also permits an A-MSDU. // Committed Compressed MPDUs retain their payload and length throughout retry; this path forms no A-MPDU. bool committedRetry = !prepared.preparation && header->getRetry(); + if (prepared.batch && !committedRetry) + return false; if (!committedRetry && (header->getAMsduPresent() || header->getRetry())) return false; for (int i = 0; i < context->getNumSteps(); i++) { auto step = dynamic_cast(context->getStep(i)); - if (step && step->getPpduEnd() >= SIMTIME_ZERO && - dynamicPtrCast(step->getFrameToTransmit()->peekAtFront())) + if (step && step->getPpduEnd() >= SIMTIME_ZERO && (dynamic_cast(step) || + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront()))) return false; } return true; @@ -1886,9 +1944,9 @@ bool Hcf::isInitialDataException(const FrameSequenceContext *context, const Prep simtime_t Hcf::computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const { auto sifs = modeSet->getSifsTime(); - auto cost = prepared.dataMode->getDuration(prepared.frame->getDataLength()); + auto cost = prepared.dataMode->getDuration(prepared.batch ? prepared.envelope->getDataLength() : prepared.frame->getDataLength()); if (prepared.ackMode) - cost += sifs + prepared.ackMode->getDuration(LENGTH_ACK); + cost += sifs + prepared.ackMode->getDuration(prepared.batch ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_ACK); if (protect) cost += prepared.rtsMode->getDuration(prepared.rtsPrediction->getDataLength()) + sifs + prepared.ctsMode->getDuration(LENGTH_CTS) + sifs; @@ -1896,15 +1954,15 @@ simtime_t Hcf::computePreparedDataCost(const PreparedDataTransmission& prepared, } bool Hcf::canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, - simtime_t ifs, bool protect) const + simtime_t ifs, bool protect, simtime_t elapsedIfs) const { auto txop = context->getQoSContext()->txopProcedure; auto cost = computePreparedDataCost(prepared, protect); int previousData = 0; for (int i = 0; i < context->getNumSteps(); i++) { auto step = dynamic_cast(context->getStep(i)); - if (step && step->getPpduEnd() >= SIMTIME_ZERO && - dynamicPtrCast(step->getFrameToTransmit()->peekAtFront())) + if (step && step->getPpduEnd() >= SIMTIME_ZERO && (dynamic_cast(step) || + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront()))) previousData++; } // IEEE Std 802.11-2024, 10.23.2.9: zero permits one SU DATA PPDU, including an A-MSDU. @@ -1912,8 +1970,15 @@ bool Hcf::canAdmitPreparedData(const FrameSequenceContext *context, const Prepar if (previousData != 0) return false; } - else if (ifs + cost > txop->getRemaining() && !isInitialDataException(context, prepared)) - return false; + else { + bool initialOverrun = ifs + cost > txop->getRemaining() && isInitialDataException(context, prepared); + if (ifs + cost > txop->getRemaining() && !initialOverrun) + return false; + // IEEE Std 802.11-2024, 9.2.5.2(b)(4): a rounded reservation can exceed the TXOP deadline. + if (txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION && + txop->getReservationEnd() > txop->getStart() + txop->getLimit() && !initialOverrun) + return false; + } bool later = context->getNumSteps() > 0; // The current TransmitStep is already present at the final submission check. if (later) { @@ -1921,7 +1986,7 @@ bool Hcf::canAdmitPreparedData(const FrameSequenceContext *context, const Prepar later = !last || last->getPpduEnd() >= SIMTIME_ZERO || context->getNumSteps() > 1; } // 10.23.2.8 samples TXNAV at exchange completion, before the leading IFS. - if (later && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()) + if (later && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining() + elapsedIfs) return false; if (later && protect && txop->getLimit() > 0 && txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION && (simTime() + ifs - txop->getStart() - txop->getLimit()).raw() % SimTime(1, SIMTIME_US).raw() != 0) @@ -1940,6 +2005,31 @@ bool Hcf::validatePreparedData(const PreparedDataTransmission& prepared, simtime !isProfileAllowed(header->getReceiverAddress(), true)) return false; auto service = check_and_cast(originatorDataService); + if (prepared.batch) { + if (!service->isBatchValid(*prepared.batch, start)) + return false; + auto frames = edca->getChannelOwner()->getInProgressFrames(); + for (auto member : prepared.batch->members) { + bool queued = false; + for (const auto& preview : prepared.batch->queued) + queued |= preview->packet.get() == member; + if (!queued && (!frames->containsFrame(member) || !isFrameEligible(member, true) || + edca->getLifetimeHandler()->isExpired(member, start))) + return false; + if (!agreement->containsSequence(member->peekAtFront()->getSequenceNumber())) + return false; + } + auto peer = mib->findPeerHtState(header->getReceiverAddress()); + if (!peer || !isPeerCompatibleMode(modeSet, peer, prepared.dataMode) || + (prepared.rtsMode && !isPeerCompatibleMode(modeSet, peer, prepared.rtsMode)) || + (prepared.ctsMode && rateSelection->computeResponseCtsFrameMode(prepared.rtsPrediction.get(), + prepared.rtsPrediction->peekAtFront(), ResponseQueryRole::PREDICT) != prepared.ctsMode)) + return false; + const auto& caps = peer->negotiatedCapabilities.localTxPeerRx; + return caps.valid && prepared.limits.maxLength == B((1 << (13 + caps.receiverMaxAmpduLengthExponent)) - 1) && + prepared.limits.minimumMpduStartSpacing == caps.receiverMinimumMpduStartSpacing && + rateSelection->computeCompressedBlockAckResponseMode(prepared.dataMode) == prepared.ackMode; + } if (prepared.preparation) { if (!service->isPreparationValid(*prepared.preparation, start)) return false; @@ -1983,7 +2073,7 @@ DataPreparationResult Hcf::prepareDataTransmission(FrameSequenceContext *context if (frames->containsFrame(candidate)) prepared->frame = candidate; else { - prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), true); + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), !service->hasAMpduAggregation()); if (!prepared->preparation) return DataPreparationResult::DEFERRED; prepared->frame = prepared->preparation->packet.get(); @@ -1994,6 +2084,8 @@ DataPreparationResult Hcf::prepareDataTransmission(FrameSequenceContext *context auto txop = context->getQoSContext()->txopProcedure; prepared->dataMode = rateSelection->computeMode(prepared->frame, prepared->frame->peekAtFront(), txop); + if (service->hasAMpduAggregation() && prepareAggregateData(context, *prepared)) + return DataPreparationResult::READY; const IIeee80211Mode *selectedRtsMode = nullptr; for (;;) { auto packet = prepared->frame; @@ -2040,6 +2132,217 @@ DataPreparationResult Hcf::prepareDataTransmission(FrameSequenceContext *context } } +bool Hcf::prepareAggregateData(FrameSequenceContext *context, const PreparedDataTransmission& selected) +{ + auto ht = dynamic_cast(selected.dataMode); + if (!ht || ht->getDataMode()->getBandwidth() != MHz(20) || ht->isHtShortGuardInterval() || + ht->getPreambleMode()->getPreambleFormat() != Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED) + return false; + auto header = selected.frame->peekAtFront(); + if (header->getAMsduPresent() || header->getMoreFragments() || header->getFragmentNumber() != 0) + return false; + auto peer = mib->findPeerHtState(header->getReceiverAddress()); + if (!peer || !peer->negotiatedCapabilities.localTxPeerRx.valid) + return false; + auto prepared = std::make_unique(); + prepared->modeSet = modeSet; + prepared->agreement = selected.agreement; + prepared->dataMode = selected.dataMode; + auto& limits = prepared->limits; + const auto& caps = peer->negotiatedCapabilities.localTxPeerRx; + limits.receiver = header->getReceiverAddress(); + limits.transmitter = mac->getAddress(); + limits.tid = header->getTid(); + limits.windowStart = selected.agreement->getStartingSequenceNumber(); + limits.windowSize = selected.agreement->getWindowSize(); + limits.maxLength = B((1 << (13 + caps.receiverMaxAmpduLengthExponent)) - 1); + limits.minimumMpduStartSpacing = caps.receiverMinimumMpduStartSpacing; + limits.mode = selected.dataMode; + auto owner = edca->getChannelOwner(); + std::vector retained; + for (auto packet : context->getInProgressFrames()->getEligibleFrames()) { + auto data = dynamicPtrCast(packet->peekAtFront()); + if (data && data->getReceiverAddress() == limits.receiver && data->getTid() == limits.tid && + !data->getAMsduPresent() && !data->getMoreFragments() && data->getFragmentNumber() == 0 && + selected.agreement->containsSequence(data->getSequenceNumber())) + retained.push_back(packet); + } + auto isRetry = [owner](Packet *packet) { + return owner->getAckHandler()->getQoSDataAckStatus(packet->peekAtFront()) != + QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED; + }; + std::stable_sort(retained.begin(), retained.end(), [&](Packet *left, Packet *right) { + if (isRetry(left) != isRetry(right)) + return isRetry(left); + return OriginatorBlockAckAgreement::computeSequenceDistance(limits.windowStart, + left->peekAtFront()->getSequenceNumber()) < + OriginatorBlockAckAgreement::computeSequenceDistance(limits.windowStart, + right->peekAtFront()->getSequenceNumber()); + }); + auto service = check_and_cast(originatorDataService); + prepared->batch = service->prepareBatch(owner->getPendingQueue(), retained, limits); + if (!prepared->batch) + return false; + auto& batch = *prepared->batch; + const IIeee80211Mode *rtsMode = nullptr; + while (!batch.members.empty()) { + if (batch.members.size() == 1 && (!batch.queued.empty() || !isRetry(batch.members.front()))) + break; + prepared->frame = batch.members.front(); + prepared->setEnvelope(service->buildAggregate(batch, limits)); + if (!prepared->envelope) + break; + prepared->ackMode = rateSelection->computeCompressedBlockAckResponseMode(prepared->dataMode); + auto data = prepared->frame->peekAtFront(); + bool protect = rtsPolicy->isRtsNeeded(prepared->envelope.get(), data); + prepared->setRtsPrediction(nullptr); + prepared->rtsMode = prepared->ctsMode = nullptr; + if (protect) { + prepared->setRtsPrediction(new Packet("prepared-RTS", rtsProcedure->buildRtsFrame(data))); + prepared->rtsPrediction->insertAtBack(makeShared()); + auto rts = prepared->rtsPrediction->peekAtFront(); + if (!rtsMode) + rtsMode = rateSelection->computeMode(prepared->rtsPrediction.get(), rts, context->getQoSContext()->txopProcedure); + prepared->rtsMode = rtsMode; + setFrameMode(prepared->rtsPrediction.get(), rts, rtsMode); + prepared->ctsMode = rateSelection->computeResponseCtsFrameMode(prepared->rtsPrediction.get(), rts, ResponseQueryRole::PREDICT); + auto legalControl = [](const IIeee80211Mode *mode) { + return mode->getModulationClass() == IIeee80211Mode::ModulationClass::OFDM || + mode->getModulationClass() == IIeee80211Mode::ModulationClass::ERP_OFDM; + }; + if (!legalControl(rtsMode) || !legalControl(prepared->ctsMode)) + throw cRuntimeError("HT A-MPDU protection requires non-HT OFDM or ERP-OFDM RTS and CTS modes"); + } + if (validatePreparedData(*prepared, simTime() + context->getIfs()) && + canAdmitPreparedData(context, *prepared, context->getIfs(), protect)) { + context->getQoSContext()->preparedData = std::move(prepared); + return true; + } + auto removed = batch.members.back(); + batch.members.pop_back(); + batch.queued.erase(std::remove_if(batch.queued.begin(), batch.queued.end(), + [removed](const std::unique_ptr& preview) { return preview->packet.get() == removed; }), batch.queued.end()); + } + return false; +} + +Packet *Hcf::prepareAggregateTransmission() +{ + Enter_Method("prepareAggregateTransmission"); + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + if (!prepared || !prepared->batch || !validatePreparedData(*prepared, simTime()) || + !canAdmitPreparedData(context, *prepared, SIMTIME_ZERO, false, + check_and_cast(context->getLastStep())->getIfs())) + return nullptr; + auto owner = edca->getChannelOwner(); + auto service = check_and_cast(originatorDataService); + std::vector committed; + if (!service->commitPreparedBatch(*prepared->batch, committed)) + return nullptr; + for (auto packet : committed) + owner->getInProgressFrames()->adoptPreparedFrame(packet); + auto txop = owner->getTxopProcedure(); + auto response = modeSet->getSifsTime() + prepared->ackMode->getDuration(LENGTH_COMPRESSED_BLOCKACK); + if (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION) + prepared->duration = encodeDuration(response, true); + else { + auto ppduDuration = prepared->dataMode->getDuration(prepared->envelope->getDataLength()); + auto reservation = std::max(SIMTIME_ZERO, txop->getReservationEnd() - simTime()); + auto remaining = std::max(SIMTIME_ZERO, txop->getStart() + txop->getLimit() - simTime()); + bool overrun = txop->getLimit() > 0 && ppduDuration + response > txop->getRemaining() && + isInitialDataException(context, *prepared); + prepared->duration = overrun ? computeMultipleProtectionDuration(0, 0, 0, ppduDuration, ppduDuration + response, false) : + computeMultipleProtectionDuration(txop->getLimit(), remaining, reservation, ppduDuration, + txop->getLimit() == 0 ? ppduDuration + response : SIMTIME_ZERO, txop->getReservationEnd() != 0); + } + for (auto packet : prepared->batch->members) { + auto data = packet->removeAtFront(); + auto status = owner->getAckHandler()->getQoSDataAckStatus(data); + data->setRetry(status != QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + data->setAckPolicy(NORMAL_ACK); + data->setDurationField(prepared->duration); + packet->insertAtFront(data); + tx->finalizeMpdu(packet); + } + prepared->setEnvelope(service->buildAggregate(*prepared->batch, prepared->limits)); + ASSERT(prepared->envelope); + prepared->envelope->addTagIfAbsent()->setMode(prepared->dataMode); + prepared->submitted = true; + for (auto packet : prepared->batch->members) { + prepared->agreement->recordTransmittedData(packet->getTag()->getAllocationIndex()); + RateSelection::emitDatarateSelected(this, packet->peekAtFront(), prepared->dataMode); + } + return prepared->envelope.get(); +} + +void Hcf::aggregateTransmissionCancelled() +{ + Enter_Method("aggregateTransmissionCancelled"); + frameSequenceHandler->transmissionCancelled(); + mac->sendDownPendingRadioConfigMsg(); +} + +void Hcf::processAggregateOutcome(const Ptr& response) +{ + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + auto owner = edca->getChannelOwner(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(prepared->limits.receiver, prepared->limits.tid); + if (prepared->cancelled || !prepared->submitted || !agreement || agreement != prepared->agreement) + return; + auto frames = owner->getInProgressFrames(); + std::set>> acked; + if (response) { + owner->getRecoveryProcedure()->blockAckFrameReceived(); + acked = owner->getAckHandler()->processReceivedCompressedBlockAck(response, agreement); + originatorBlockAckAgreementHandler->processReceivedBlockAck(response, this); + auto step = check_and_cast(context->getStepBeforeLast()); + rx->successfulFrameTransmitted(step->getPpduEnd(), prepared->duration); + } + else { + owner->getRecoveryProcedure()->controlFrameTransmissionFailed(); + agreement->setPendingProbe(true); + } + for (auto packet : prepared->batch->members) { + if (!frames->containsFrame(packet)) + continue; + auto data = packet->peekAtFront(); + bool acknowledged = response && owner->getAckHandler()->getQoSDataAckStatus(data) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED; + FrameTransmissionDetails details; + details.setStatus(acknowledged ? FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED : FRAME_TRANSMISSION_STATUS_UNACKNOWLEDGED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &details); + if (!acknowledged) { + owner->getRecoveryProcedure()->implicitBlockAckMemberFailed(packet, data); + if (!response) + owner->getAckHandler()->processFailedImplicitBlockAckMember(data); + auto updated = packet->removeAtFront(); + updated->setRetry(true); + packet->insertAtFront(updated); + } + } + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + if (data && owner->getAckHandler()->getQoSDataAckStatus(data) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED) + owner->getRecoveryProcedure()->dropFrame(data); + } + frames->dropFrames(acked); + owner->getAckHandler()->dropFrames(acked); + advanceOriginatorWindows(); + completeTransfers(); +} + +void Hcf::frameTransmissionStarted(Packet *packet) +{ + Enter_Method("frameTransmissionStarted"); + auto bar = dynamicPtrCast(packet->peekAtFront()); + if (bar && originatorBlockAckAgreementHandler) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), bar->getTidInfo()); + if (agreement) + agreement->setPendingProbe(false); + } +} + simtime_t Hcf::forecastSingleProtectionSuccessor(Packet *packet, const Ptr& header, TxopProcedure *txop, InProgressFrames *frames, simtime_t completionTime) { @@ -2086,9 +2389,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) Enter_Method("transmitFrame"); auto channelOwner = edca->getChannelOwner(); if (channelOwner) { - auto header = packet->peekAtFront(); auto context = frameSequenceHandler->getContext(); auto prepared = context->getQoSContext()->preparedData.get(); + if (dynamic_cast(context->getLastStep())) { + if (!prepared || !validatePreparedData(*prepared, simTime() + ifs) || + !canAdmitPreparedData(context, *prepared, ifs, false)) + return false; + tx->transmitAggregate(ifs, this); + return true; + } + auto header = packet->peekAtFront(); auto rtsStep = dynamic_cast(context->getLastStep()); bool preparedStep = prepared && (prepared->frame == packet || (rtsStep && rtsStep->getProtectedFrame() == prepared->frame)); @@ -2096,7 +2406,7 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) !canAdmitPreparedData(context, *prepared, ifs, rtsStep != nullptr))) return false; if (dynamicPtrCast(header) && - !(preparedStep && prepared->preparation) && + !(preparedStep && (prepared->preparation || prepared->batch)) && !channelOwner->getInProgressFrames()->containsFrame(packet)) return false; if (!isFrameEligible(packet, true)) @@ -2412,6 +2722,8 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + if (tx) + tx->cancelPendingAggregate(this); cancelAndDelete(addbaResponseTimer); cancelAndDelete(dataLifetimeTimer); cancelAndDelete(startRxTimer); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 068c93574ac..f6a773ff7d3 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -50,13 +50,16 @@ class Ieee80211Mib; /** * Implements IEEE 802.11 Hybrid Coordination Function. */ -class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public IBlockAckAgreementHandlerCallback, public ModeSetListener +class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::IAggregateCallback, public IProcedureCallback, public IBlockAckAgreementHandlerCallback, public ModeSetListener { private: BlockAckVariant blockAckVariant = BlockAckVariant::BASIC; bool validateBlockAckRequest(Packet *packet, const Ptr& bar) const; public: + Packet *prepareAggregateTransmission() override; + void aggregateTransmissionCancelled() override; + void frameTransmissionStarted(Packet *packet) override; static simsignal_t edcaCollisionDetectedSignal; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; @@ -145,10 +148,12 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: static simtime_t encodeDuration(simtime_t duration, bool holder); simtime_t computeImmediateResponseDuration(Packet *packet, const Ptr& header); DataPreparationResult prepareDataTransmission(FrameSequenceContext *context); + bool prepareAggregateData(FrameSequenceContext *context, const PreparedDataTransmission& selected); + void processAggregateOutcome(const Ptr& response); bool validatePreparedData(const PreparedDataTransmission& prepared, simtime_t start) const; simtime_t computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const; bool canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, - simtime_t ifs, bool protect) const; + simtime_t ifs, bool protect, simtime_t elapsedIfs = 0) const; bool isInitialDataException(const FrameSequenceContext *context, const PreparedDataTransmission& prepared) const; simtime_t forecastSingleProtectionSuccessor(Packet *packet, const Ptr& header, TxopProcedure *txop, InProgressFrames *frames, simtime_t completionTime); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc index f392d6fc3aa..7ab65c01b5f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc @@ -6,16 +6,38 @@ #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" namespace inet { namespace ieee80211 { using namespace inet::physicallayer; +AmpduTransmitStep::AmpduTransmitStep(std::shared_ptr prepared, simtime_t ifs) : + TransmitStep(nullptr, ifs), prepared(std::move(prepared)) +{ +} + +Packet *AmpduTransmitStep::getFrameToTransmit() +{ + return prepared->envelope.get(); +} + PreparedDataTransmission::~PreparedDataTransmission() { if (rtsPrediction) drop(rtsPrediction.get()); + if (envelope) + drop(envelope.get()); +} + +void PreparedDataTransmission::setEnvelope(Packet *packet) +{ + if (envelope) + drop(envelope.get()); + envelope.reset(packet); + if (envelope) + take(envelope.get()); } void PreparedDataTransmission::setRtsPrediction(Packet *packet) diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h index 85fa551c5f4..b5026782257 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h @@ -37,6 +37,11 @@ enum class DataPreparationResult { LEGACY, READY, DEFERRED }; struct INET_API PreparedDataTransmission : public cObject { std::unique_ptr preparation; + std::unique_ptr batch; + MpduAggregationLimits limits; + std::unique_ptr envelope; + simtime_t duration = 0; + bool submitted = false; Packet *frame = nullptr; // Borrowed from preparation, or from InProgressFrames after commitment. const physicallayer::Ieee80211ModeSet *modeSet = nullptr; const physicallayer::IIeee80211Mode *dataMode = nullptr; @@ -49,6 +54,7 @@ struct INET_API PreparedDataTransmission : public cObject ~PreparedDataTransmission() override; void setRtsPrediction(Packet *packet); + void setEnvelope(Packet *packet); }; class INET_API QoSContext @@ -68,7 +74,7 @@ class INET_API QoSContext TxopProcedure *txopProcedure = nullptr; const BlockAckVariant blockAckVariant; std::function prepareData; - std::unique_ptr preparedData; + std::shared_ptr preparedData; }; class INET_API NonQoSContext diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index c01596ee2ab..84d3d9ac4b2 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -22,8 +22,11 @@ void FrameSequenceHandler::interruptFrameSequence() auto outgoing = check_and_cast(context->getStepBeforeLast())->getFrameToTransmit(); // Retirement removes DATA and management frames from the queue before this transition. // Control exchanges retain their ordinary failure path through the recovery callback. - bool retired = dynamicPtrCast(outgoing->peekAtFront()) && - !context->getInProgressFrames()->containsFrame(outgoing); + auto prepared = context->getQoSContext() ? context->getQoSContext()->preparedData.get() : nullptr; + bool aggregate = prepared && prepared->batch && prepared->envelope.get() == outgoing; + bool retired = aggregate ? prepared->cancelled : + dynamicPtrCast(outgoing->peekAtFront()) && + !context->getInProgressFrames()->containsFrame(outgoing); if (retired) finishFrameSequence(); else @@ -74,6 +77,13 @@ void FrameSequenceHandler::transmissionComplete() } } +void FrameSequenceHandler::transmissionCancelled() +{ + ASSERT(isSequenceRunning() && dynamic_cast(context->getLastStep())); + context->removeLastStep(); + finishFrameSequence(); +} + void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, IFrameSequenceHandler::ICallback *callback) { EV_INFO << "Starting frame sequence.\n"; @@ -173,8 +183,10 @@ void FrameSequenceHandler::abortFrameSequence() auto step = context->getLastStep(); auto failedTxStep = check_and_cast(dynamic_cast(step) ? context->getStepBeforeLast() : step); auto frameToTransmit = failedTxStep->getFrameToTransmit(); - auto header = frameToTransmit->peekAtFront(); - if (auto dataOrMgmtHeader = dynamicPtrCast(header)) + auto header = dynamic_cast(failedTxStep) ? nullptr : frameToTransmit->peekAtFront(); + if (dynamic_cast(failedTxStep)) + callback->originatorProcessFailedFrame(frameToTransmit); + else if (auto dataOrMgmtHeader = dynamicPtrCast(header)) callback->originatorProcessFailedFrame(frameToTransmit); else if (auto rtsTxStep = dynamic_cast(failedTxStep)) callback->originatorProcessRtsProtectionFailed(const_cast(rtsTxStep->getProtectedFrame())); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index a3d0d6f647b..d18fceb8528 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -29,6 +29,7 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual void abortFrameSequence(); public: + void transmissionCancelled() override; virtual const FrameSequenceContext *getContext() const override { return context; } virtual const IFrameSequence *getFrameSequence() const override { return frameSequence; } virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, IFrameSequenceHandler::ICallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h index ce0b17abac6..79daa569a2b 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h @@ -8,11 +8,15 @@ #ifndef __INET_FRAMESEQUENCESTEP_H #define __INET_FRAMESEQUENCESTEP_H +#include + #include "inet/linklayer/ieee80211/mac/contract/IFrameSequence.h" namespace inet { namespace ieee80211 { +struct PreparedDataTransmission; + class INET_API TransmitStep : public ITransmitStep { protected: @@ -53,6 +57,15 @@ class INET_API RtsTransmitStep : public TransmitStep virtual const Packet *getProtectedFrame() { return protectedFrame; } }; +class INET_API AmpduTransmitStep : public TransmitStep +{ + std::shared_ptr prepared; + public: + AmpduTransmitStep(std::shared_ptr prepared, simtime_t ifs); + Packet *getFrameToTransmit() override; + const PreparedDataTransmission *getPreparedData() const { return prepared.get(); } +}; + class INET_API ReceiveStep : public IReceiveStep { protected: diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 66da226a753..4c5bc8b7414 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -10,6 +10,36 @@ namespace inet { namespace ieee80211 { +bool isValidCompressedBlockAck(Packet *packet, MacAddress local, MacAddress peer, Tid tid) +{ + auto response = dynamicPtrCast(packet->peekAtFront()); + return response && response->getReceiverAddress() == local && response->getTransmitterAddress() == peer && + response->getTidInfo() == tid && !response->getMultiTid() && response->getCompressedBitmap() && + response->getFragmentNumber() == 0 && response->getStartingSequenceNumber().isSpecified() && + response->getBlockAckBitmap().getSize() == 64 && + response->getChunkLength() == LENGTH_COMPRESSED_BLOCKACK - B(4) && packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK; +} + +IFrameSequenceStep *AmpduBlockAckFs::prepareStep(FrameSequenceContext *context) +{ + auto prepared = context->getQoSContext()->preparedData.get(); + if (step == 0) + return new AmpduTransmitStep(context->getQoSContext()->preparedData, context->getIfs()); + if (step == 1) + return new ReceiveStep(context->getQoSContext()->ackPolicy->getImplicitBlockAckTimeout(prepared->ackMode)); + return nullptr; +} + +bool AmpduBlockAckFs::completeStep(FrameSequenceContext *context) +{ + if (step++ == 0) + return true; + auto prepared = context->getQoSContext()->preparedData.get(); + auto receive = check_and_cast(context->getLastStep()); + return !prepared->cancelled && isValidCompressedBlockAck(receive->getReceivedFrame(), + prepared->limits.transmitter, prepared->limits.receiver, prepared->limits.tid); +} + // TODO remove isForUs checks it's already done in framesequencehandler void SelfCtsFs::startSequence(FrameSequenceContext *context, int firstStep) @@ -467,14 +497,8 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) auto compressed = dynamicPtrCast(request); auto response = dynamicPtrCast(receivedHeader); // IEEE Std 802.11-2024, 9.3.1.8.2: this profile supports the 64-bit bitmap without dynamic fragmentation. - return context->isForUs(receivedHeader) && compressed && response && - response->getReceiverAddress() == compressed->getTransmitterAddress() && - response->getTransmitterAddress() == compressed->getReceiverAddress() && - response->getTidInfo() == compressed->getTidInfo() && - !response->getMultiTid() && response->getCompressedBitmap() && response->getFragmentNumber() == 0 && - response->getStartingSequenceNumber().isSpecified() && response->getBlockAckBitmap().getSize() == 64 && - response->getChunkLength() == LENGTH_COMPRESSED_BLOCKACK - B(4) && - receivedPacket->getDataLength() == LENGTH_COMPRESSED_BLOCKACK; + return compressed && isValidCompressedBlockAck(receivedPacket, compressed->getTransmitterAddress(), + compressed->getReceiverAddress(), compressed->getTidInfo()); } default: throw cRuntimeError("Unknown step"); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h index 2a88d853108..70d9cc4ea0b 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h @@ -156,6 +156,18 @@ class INET_API LastFrameAckFs : public IFrameSequence virtual std::string getHistory() const override { return std::string("LAST-FRAME") + (step == 2 ? " ACK" : ""); } // TODO completeStep = true? }; +class INET_API AmpduBlockAckFs : public IFrameSequence +{ + int step = 0; + public: + void startSequence(FrameSequenceContext *context, int firstStep) override { step = 0; } + IFrameSequenceStep *prepareStep(FrameSequenceContext *context) override; + bool completeStep(FrameSequenceContext *context) override; + std::string getHistory() const override { return "A-MPDU BLOCKACK"; } +}; + +INET_API bool isValidCompressedBlockAck(Packet *packet, MacAddress local, MacAddress peer, Tid tid); + class INET_API BlockAckReqBlockAckFs : public IFrameSequence { protected: diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc index d2986e6dffe..78d11e1dd0f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -32,7 +32,9 @@ TxOpFs::TxOpFs() : new SequentialFs({new OptionalFs(new RtsCtsFs(), OPTIONALFS_PREDICATE(isRtsCtsNeeded)), new AlternativesFs({new ManagementAckFs(), /* TODO DATA + QAP*/}, - ALTERNATIVESFS_SELECTOR(selectMgmtOrDataQap))})}, + ALTERNATIVESFS_SELECTOR(selectMgmtOrDataQap))}), + new SequentialFs({new OptionalFs(new RtsCtsFs(), OPTIONALFS_PREDICATE(isRtsCtsNeeded)), + new AmpduBlockAckFs()})}, ALTERNATIVESFS_SELECTOR(selectTxOpSequence)) { } @@ -47,6 +49,8 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte auto qos = context->getQoSContext(); if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) return 2; + if (qos->preparedData && qos->preparedData->batch) + return 4; auto frameToTransmit = context->getDataFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); if (dynamicPtrCast(macHeader)) @@ -72,6 +76,9 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte bool TxOpFs::isRtsCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *context) { auto protectedFrame = context->getDataFrameToTransmit(); + auto prepared = context->getQoSContext()->preparedData.get(); + if (prepared && prepared->batch) + return prepared->rtsMode != nullptr; return context->getRtsPolicy()->isRtsNeeded(protectedFrame, protectedFrame->peekAtFront()); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index dbdfdf50d0b..b1aa01db57d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -39,6 +39,13 @@ bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *frames, TxopP std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { using Id = std::pair; + if (agreements && probeEligibility) { + for (const auto& entry : agreements->getAgreements()) { + auto agreement = entry.second; + if (agreement->getIsAddbaResponseReceived() && agreement->hasPendingProbe() && probeEligibility(frames, entry.first.second)) + return std::make_tuple(entry.first.first, agreement->getStartingSequenceNumber(), entry.first.second); + } + } std::map> groups; for (auto frame : frames->getOutstandingFrames()) { auto header = frame->peekAtFront(); @@ -127,5 +134,10 @@ simtime_t OriginatorQosAckPolicy::getBlockAckTimeout(Packet *packet, const PtrgetSifsTime() + modeSet->getSlotTime() + rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getPhyRxStartDelay() : blockAckTimeout; } +simtime_t OriginatorQosAckPolicy::getImplicitBlockAckTimeout(const physicallayer::IIeee80211Mode *responseMode) const +{ + return blockAckTimeout == -1 ? modeSet->getSifsTime() + modeSet->getSlotTime() + responseMode->getPhyRxStartDelay() : blockAckTimeout; +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index cefd7008567..405d0269169 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -25,6 +25,7 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat simtime_t blockAckTimeout = -1; simtime_t ackTimeout = -1; + std::function probeEligibility; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } @@ -33,6 +34,8 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; public: + void setProbeEligibility(std::function predicate) override { probeEligibility = predicate; } + simtime_t getImplicitBlockAckTimeout(const physicallayer::IIeee80211Mode *responseMode) const override; virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index e213b5665bb..f01e08414ca 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -229,6 +229,24 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const PtrgetReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()))] = Status::WAITING_FOR_NORMAL_ACK; } +void QosAckHandler::processTransmittedImplicitBlockAckMember(const Ptr& header) +{ + // IEEE Std 802.11-2024, 10.25.6.5: aggregate context makes policy 00 an implicit request. + QoSKey id = {header->getReceiverAddress(), {header->getTid(), + SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())}}; + ackStatuses.at(id) = Status::WAITING_FOR_BLOCK_ACK; +} + +void QosAckHandler::processFailedImplicitBlockAckMember(const Ptr& header) +{ + // IEEE Std 802.11-2024, 10.25.6.7.1 permits DATA retry after the Block Ack response fails. + QoSKey id = {header->getReceiverAddress(), {header->getTid(), + SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())}}; + auto found = ackStatuses.find(id); + if (found != ackStatuses.end() && found->second == Status::WAITING_FOR_BLOCK_ACK) + found->second = Status::BLOCK_ACK_NOT_ARRIVED; +} + void QosAckHandler::processTransmittedBlockAckReq(const Ptr& blockAckReq, const OriginatorBlockAckAgreement *agreement) { for (auto& ackStatus : ackStatuses) { @@ -250,7 +268,9 @@ void QosAckHandler::processTransmittedBlockAckReq(const Ptr(blockAckReq)) { if (compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && OriginatorBlockAckAgreement::isInBitmap(sequence, startingSeqNum) && seqCtrlField.getFragmentNumber() == 0) + bool transmitted = status == Status::BLOCK_ACK_NOT_YET_REQUESTED || status == Status::BLOCK_ACK_NOT_ARRIVED || + status == Status::BLOCK_ACK_ARRIVED_UNACKED || status == Status::NORMAL_ACK_NOT_ARRIVED; + if (transmitted && OriginatorBlockAckAgreement::isInBitmap(sequence, startingSeqNum) && seqCtrlField.getFragmentNumber() == 0) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 69371236796..fb5c7d29e7e 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -63,6 +63,8 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); + void processTransmittedImplicitBlockAckMember(const Ptr& header); + void processFailedImplicitBlockAckMember(const Ptr& header); virtual void processTransmittedBlockAckReq(const Ptr& blockAckReq, const OriginatorBlockAckAgreement *agreement); virtual void processFailedFrame(const Ptr& dataOrMgmtHeader); virtual void dropFrame(const Ptr& dataOrMgmtHeader); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc index e52f6b72652..75bc71ca2a1 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc @@ -126,6 +126,11 @@ void QosRecoveryProcedure::controlFrameTransmissionFailed() } } +void QosRecoveryProcedure::implicitBlockAckMemberFailed(Packet *packet, const Ptr& header) +{ + incrementCounter(header, packet->getByteLength() >= rtsThreshold ? longRetryCounter : shortRetryCounter); +} + // // This SRC and the SSRC shall be reset when a MAC frame of length less than or equal // to dot11RTSThreshold succeeds for that MPDU of type Data or MMPDU. diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h index e1804fcc323..62c9b663066 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h @@ -61,6 +61,7 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce bool isMulticastFrame(Packet *packet, const Ptr& header); public: + void implicitBlockAckMemberFailed(Packet *packet, const Ptr& header); virtual void multicastFrameTransmitted(); virtual void ctsFrameReceived(); diff --git a/tests/module/Ieee80211AmpduAdmission_1.test b/tests/module/Ieee80211AmpduAdmission_1.test new file mode 100644 index 00000000000..e4369a55619 --- /dev/null +++ b/tests/module/Ieee80211AmpduAdmission_1.test @@ -0,0 +1,701 @@ +%description: +Real HCF queues drive aggregate admission, protection, and delayed submission through two radios. +Cases 0 through 2 place the RTS threshold above, at, and below the complete PSDU length. +Case 3 adds peer spacing that crosses the RTS threshold. +Case 4 reduces three members to two across that threshold at one retained DATA mode. +Cases 5 and 6 send a retained singleton retry beyond a positive TXOP under multiple protection, without and with RTS. +Cases 7 and 8 cancel post-CTS DATA through real inactivity expiry and peer reset before SIFS ends. +Case 9 checks an ordinary positive TXOP. Threshold cases use a zero TXOP. +Cases 10 through 12 use DATA durations below, at, and above the HT mixed-format bound. +Cases 13 through 15 lose actual CTS responses for new, retained, and mixed batches. +Two RTS failures cross the contention retry boundary without member failure or disposal. +Cases 16 and 17 use nonzero propagation after RTS to test the next unprotected batch. +An encoded reservation beyond the TXOP deadline must defer that batch without source changes. +A reservation equal to the deadline must permit that batch when its exchange fits. +The selected mode supplies the boundary lengths and the reduction budget. +The fixture seeds accepted agreements and the singleton's prior failed attempt. +It checks current HCF and Tx behavior, not ADDBA negotiation or the seeded prior transmission. +The campaign uses HT20 MCS0 and MCS7, long GI, both bands, scalar reception, and seed zero. +At MCS0, case 3 proves that the selected spacing needs no extra delimiter. +At MCS7, case 3 proves that extra delimiters cross the RTS threshold. + +%extraargs: -r 0..56 + +%file: AmpduAdmission.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.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/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRtsPolicy.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +static const Ieee80211HtMode *admissionMode(int mcs, bool band5) +{ + return Ieee80211HtCompliantModes::getCompliantMode( + mcs == 0 ? &Ieee80211HtmcsTable::htMcs0BW20MHz : &Ieee80211HtmcsTable::htMcs7BW20MHz, + band5 ? Ieee80211HtMode::BAND_5GHZ : Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +} + +static cValue admissionTxopLimit(cComponent *, cValue arguments[], int) +{ + int scenario = arguments[0].intValue(); + auto mode = admissionMode(arguments[1].intValue(), arguments[2].boolValue()); + simtime_t limit; + if (scenario == 16 || scenario == 17) limit = SimTime(1, SIMTIME_MS); + else if (scenario < 3 || scenario >= 10) limit = SIMTIME_ZERO; + else if (scenario == 5 || scenario == 6) limit = SimTime(1, SIMTIME_US); + else if (scenario == 4) { + // This setup budget lies between two and three DATA PPDUs at the selected mode. + // A 6 Mbps response supplies a conservative response duration for both supported MCS values. + auto response = Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000)); + limit = (mode->getDuration(B(2008)) + mode->getDuration(B(3012))) / 2 + + mode->getSifsTime() + response->getDuration(LENGTH_COMPRESSED_BLOCKACK); + } + else limit = SimTime(1, SIMTIME_MS); + return cValue(limit.dbl(), "s"); +} +Define_NED_Function(admissionTxopLimit, "quantity admissionTxopLimit(int scenario, int mcs, bool band5)"); + +class AdmissionHcf; +class AdmissionDriver : public cSimpleModule +{ + bool verificationScheduled = false; + public: + void verifyAfter(simtime_t interval) { + Enter_Method("verifyAfter"); + if (!verificationScheduled) { + verificationScheduled = true; + scheduleAfter(interval, new cMessage("verify")); + } + } + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override; +}; +Define_Module(AdmissionDriver); + +class AdmissionErrorModel : public Ieee80211NistErrorModel +{ + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart) const override { + if (!par("dropCts").boolValue()) return 0; + auto packet = snir->getReception()->getTransmission()->getPacket(); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto ht = dynamicPtrCast(phy); + if (ht && ht->getAggregation()) return 0; + return packet->peekDataAt(phy->getChunkLength())->getType() == ST_CTS ? 1 : 0; + } + double computeChunkSuccessRate(const ISnir *, b) const override { return 1; } +}; +Define_Module(AdmissionErrorModel); + +class AdmissionRtsPolicy : public QosRtsPolicy +{ + public: + mutable std::vector> aggregateQueries; + void setThreshold(B length) { rtsThreshold = length.get(); } + bool isRtsNeeded(Packet *packet, const Ptr& header) const override { + bool result = QosRtsPolicy::isRtsNeeded(packet, header); + if (dynamicPtrCast(packet->peekAtFront())) + aggregateQueries.push_back({B(packet->getDataLength()), result}); + return result; + } +}; +Define_Module(AdmissionRtsPolicy); + +class AdmissionRateSelection : public IQosRateSelection +{ + public: + IQosRateSelection *delegate = nullptr; + const IIeee80211Mode *fixedDataMode = nullptr; + int dataQueries = 0; + const IIeee80211Mode *selectedDataMode = nullptr; + const IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txop) override { + // The fixture supplies a fixed HT tuple, including HT operation through the 5 GHz ac mode set. + auto result = dynamicPtrCast(header) ? fixedDataMode : delegate->computeMode(packet, header, txop); + if (dynamicPtrCast(header)) { + dataQueries++; + selectedDataMode = result; + } + return result; + } + const IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { + return delegate->computeResponseCtsFrameMode(packet, header, role); + } + const IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { + return delegate->computeResponseAckFrameMode(packet, header, role); + } + const IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { + return delegate->computeResponseBlockAckFrameMode(packet, header, role); + } + const IIeee80211Mode *computeCompressedBlockAckResponseMode(const IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } + const IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool starts) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, starts); + } +}; + +class AdmissionOriginatorAgreements : public OriginatorBlockAckAgreementHandler +{ + public: + void accept(MacAddress peer, int window = 64) { + auto agreement = new OriginatorBlockAckAgreement(peer, 0, SequenceNumberCyclic(0), window, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 0}] = agreement; + } +}; + +class AdmissionRecipientAgreements : public RecipientBlockAckAgreementHandler +{ + public: + void accept(MacAddress peer, int window = 64) { + auto agreement = new RecipientBlockAckAgreement(peer, 0, SequenceNumberCyclic(0), window, SIMTIME_ZERO); + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 0}] = agreement; + } +}; + +class AdmissionHcf : public Hcf +{ + struct Source { int64_t id; Packet *packet; cObject *owner; simtime_t deadline; }; + struct Canonical { Packet *packet; cObject *owner; simtime_t deadline; int sequence; int64_t allocation; QosAckHandler::Status status; }; + std::vector sources; + std::vector canonical; + AdmissionRateSelection selection; + const IIeee80211Mode *expectedMode = nullptr; + B unspacedLength = B(0); + B fullLength = B(0); + B reducedLength = B(0); + MacAddress peer; + bool postCtsEvent = false; + int scenario() const { return par("scenario"); } + int mcs() const { return par("mcs"); } + bool source() const { return getFullPath().find("sender") != std::string::npos; } + bool cancellationCase() const { return scenario() == 7 || scenario() == 8; } + bool singletonCase() const { return scenario() == 5 || scenario() == 6; } + bool rtsFailureCase() const { return scenario() >= 13 && scenario() <= 15; } + bool reservationCase() const { return scenario() == 16 || scenario() == 17; } + AdmissionDriver *driver() const { + return check_and_cast(getSimulation()->getSystemModule()->getSubmodule("driver")); + } + OriginatorQosMacDataService *service() const { return check_and_cast(originatorDataService); } + void assertNoMemberRetryCount(Packet *packet) { + bool absent = false; + try { + edca->getEdcaf(AC_BE)->getRecoveryProcedure()->getRetryCount(packet, packet->peekAtFront()); + } + catch (const cRuntimeError& error) { + ASSERT(std::string(error.what()).find("The retry counter entry doesn't exist") != std::string::npos); + absent = true; + } + ASSERT(absent); + } + Packet *makeMember(int length) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(peer); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(peer); + header->setTid(0); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet("admission-msdu", header); + packet->insertAtBack(makeShared(B(length) - header->getChunkLength() - B(4))); + packet->insertAtBack(makeShared()); + return packet; + } + public: + int rtsSent = 0; + int ctsReceived = 0; + int startCallbacks = 0; + int submissions = 0; + int aggregateCompletions = 0; + int successfulResponses = 0; + int failures = 0; + int cancellations = 0; + int implicitProbes = 0; + int delivered = 0; + int inactivityEvents = 0; + int rtsFailures = 0; + int dataOutcomes = 0; + int memberDisposals = 0; + B submittedLength = B(0); + int submittedMembers = 0; + int proposedMembers = 0; + simtime_t ctsTime; + bool checkedReservation = false; + MacAddress address() const { return mac->getAddress(); } + void seed(AdmissionHcf *other) { + Enter_Method("seed"); + peer = other->address(); + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + auto peerCapabilities = mib->localHtCapabilities; + if (source() && scenario() == 3) + peerCapabilities.minimumMpduStartSpacing = 7; + mib->setPeerHtCapabilities(peer, peerCapabilities, mib->getHtOperation()); + if (source()) { + selection.delegate = rateSelection; + rateSelection = &selection; + delete originatorBlockAckAgreementHandler; + auto accepted = new AdmissionOriginatorAgreements(); + accepted->accept(peer, reservationCase() ? 2 : 64); + originatorBlockAckAgreementHandler = accepted; + } + else { + delete recipientBlockAckAgreementHandler; + auto accepted = new AdmissionRecipientAgreements(); + accepted->accept(peer, reservationCase() ? 2 : 64); + recipientBlockAckAgreementHandler = accepted; + } + expectedMode = admissionMode(mcs(), par("band5").boolValue()); + ASSERT(modeSet->containsMode(expectedMode)); + selection.fixedDataMode = expectedMode; + } + void begin() { + Enter_Method("begin"); + ASSERT(source()); + int count = reservationCase() ? 4 : scenario() == 4 ? 3 : singletonCase() ? 1 : 2; + int length = scenario() == 4 || singletonCase() ? 1000 : 64; + std::vector frames; + if (scenario() >= 10 && scenario() <= 12) { + auto ht = check_and_cast(expectedMode); + auto target = SimTime(5480 + (scenario() - 10) * 4, SIMTIME_US); + int targetLength = 0; + for (int bytes = 30; bytes <= 65535; bytes++) { + if (ht->getDuration(B(bytes)) - ht->getSignalExtensionTime() == target) { + targetLength = bytes; + break; + } + } + ASSERT(targetLength > 0); + count = (targetLength + 2003) / 2004; + int firstLength = (targetLength - 4 * count) / count / 4 * 4; + for (int i = 0; i < count; i++) { + int memberLength = i + 1 == count ? targetLength - (count - 1) * (firstLength + 4) - 4 : firstLength; + ASSERT(memberLength >= 30 && memberLength <= 2346); + frames.push_back(makeMember(memberLength)); + } + } + else for (int i = 0; i < count; i++) frames.push_back(makeMember(reservationCase() && i < 2 ? 1000 : length)); + proposedMembers = frames.size(); + MpduAggregation builder; + unspacedLength = builder.computeAggregateLength(frames, expectedMode, 0); + fullLength = builder.computeAggregateLength(frames, expectedMode, scenario() == 3 ? 7 : 0); + if (scenario() == 4) { + auto reduced = frames; + reduced.pop_back(); + reducedLength = builder.computeAggregateLength(reduced, expectedMode, 0); + } + if (reservationCase()) { + fullLength = builder.computeAggregateLength({frames[0], frames[1]}, expectedMode, 0); + reducedLength = builder.computeAggregateLength({frames[2], frames[3]}, expectedMode, 0); + } + auto policy = check_and_cast(rtsPolicy); + B threshold = fullLength + B(1); + if (scenario() == 1) threshold = fullLength; + if (scenario() == 2) threshold = fullLength - B(1); + if (scenario() == 3) { + ASSERT((fullLength > unspacedLength) == (mcs() == 7)); + threshold = unspacedLength + B(1); + } + if (scenario() == 4) threshold = reducedLength + B(1); + if (scenario() == 6 || cancellationCase()) threshold = B(1); + if (rtsFailureCase()) threshold = fullLength - B(1); + if (reservationCase()) threshold = reducedLength + B(1); + policy->setThreshold(threshold); + if (rtsFailureCase()) { + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + subscribe(packetDroppedSignal, this); + } + for (auto packet : frames) { + processUpperFrame(packet, packet->peekAtFront()); + sources.push_back({packet->getId(), packet, packet->getOwner(), edca->getLifetimeHandler()->getDeadline(packet)}); + } + int retained = singletonCase() ? 1 : scenario() == 14 ? 2 : scenario() == 15 ? 1 : 0; + for (int i = 0; i < retained; i++) { + auto owner = edca->getEdcaf(AC_BE); + auto preview = service()->prepareFrame(owner->getPendingQueue(), false); + ASSERT(preview); + auto committed = service()->commitPreparedFrame(*preview); + ASSERT(committed); + owner->getInProgressFrames()->adoptPreparedFrame(committed); + auto header = committed->removeAtFront(); + header->setRetry(true); + header->setAckPolicy(NORMAL_ACK); + committed->insertAtFront(header); + owner->getAckHandler()->processTransmittedImplicitBlockAckMember(header); + owner->getAckHandler()->processFailedImplicitBlockAckMember(header); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 0); + agreement->recordTransmittedData(committed->getTag()->getAllocationIndex()); + canonical.push_back({committed, committed->getOwner(), edca->getLifetimeHandler()->getDeadline(committed), + header->getSequenceNumber().get(), committed->getTag()->getAllocationIndex(), + owner->getAckHandler()->getQoSDataAckStatus(header)}); + sources.erase(sources.begin()); + } + } + Packet *prepareAggregateTransmission() override { + startCallbacks++; + ASSERT(!cancellationCase()); + ASSERT(selection.dataQueries == (reservationCase() ? submissions + 1 : 1)); + ASSERT(selection.selectedDataMode == expectedMode); + auto owner = edca->getChannelOwner(); + auto txop = owner->getTxopProcedure(); + if (singletonCase()) { + ASSERT(txop->getLimit() > 0 && txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION); + ASSERT(expectedMode->getDuration(fullLength) > txop->getLimit()); + } + auto packet = Hcf::prepareAggregateTransmission(); + ASSERT(packet); + submissions++; + submittedLength = B(packet->getDataLength()); + ASSERT(packet->getTag()->getMode() == expectedMode); + simtime_t duration = -1; + int currentMembers = 0; + bool complete = MpduDeaggregation::visitSubframes(packet, B(0), submittedLength, + [](const auto&, B, bool valid) { ASSERT(valid); }, + [&](const auto& mpdu, B) { + Packet member("observed-member", mpdu); + auto header = member.peekAtFront(); + ASSERT(header->getRetry() == singletonCase()); + ASSERT(header->getAckPolicy() == NORMAL_ACK); + if (currentMembers == 0) duration = header->getDurationField(); + ASSERT(header->getDurationField() == duration); + submittedMembers++; + currentMembers++; + }); + ASSERT(complete); + ASSERT(duration > SIMTIME_ZERO); + if (reservationCase()) ASSERT(submittedLength == (submissions == 1 ? fullLength : reducedLength)); + else if (scenario() == 12) { + ASSERT(submittedLength < fullLength && submittedMembers == proposedMembers - 1); + ASSERT(check_and_cast(expectedMode)->isPpduDurationValid(submittedLength)); + } + else if (scenario() == 4) ASSERT(submittedLength == reducedLength && submittedMembers == 2); + else ASSERT(submittedLength == fullLength); + if (scenario() == 10 || scenario() == 11) { + auto ht = check_and_cast(expectedMode); + ASSERT(ht->getDuration(submittedLength) - ht->getSignalExtensionTime() == + SimTime(scenario() == 10 ? 5480 : 5484, SIMTIME_US)); + } + return packet; + } + void aggregateTransmissionCancelled() override { + cancellations++; + Hcf::aggregateTransmissionCancelled(); + } + void verify(AdmissionHcf *receiver) { + Enter_Method("verify"); + ASSERT(source()); + ASSERT(failures == 0 && implicitProbes == 0); + if (reservationCase()) { + ASSERT(checkedReservation && rtsSent == 1 && ctsReceived == 1); + ASSERT(startCallbacks == (scenario() == 16 ? 1 : 2)); + ASSERT(submissions == startCallbacks && aggregateCompletions == startCallbacks && successfulResponses == startCallbacks); + ASSERT(cancellations == 0 && receiver->delivered == (scenario() == 16 ? 2 : 4)); + auto queue = edca->getEdcaf(AC_BE)->getPendingQueue(); + ASSERT(queue->getNumPackets() == (scenario() == 16 ? 2 : 0)); + ASSERT(service()->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(scenario() == 16 ? 2 : 4)); + ASSERT(!tx->hasTransmission()); + if (scenario() == 16) { + for (size_t i = 2; i < sources.size(); i++) { + const auto& source = sources[i]; + auto retained = edca->getLifetimeHandler()->findFrame(source.id); + ASSERT(retained == source.packet && retained->getOwner() == source.owner); + ASSERT(edca->getLifetimeHandler()->getDeadline(retained) == source.deadline); + ASSERT(retained->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(!retained->findTag()); + } + } + } + else if (rtsFailureCase()) { + ASSERT(rtsFailures == 2 && rtsSent == 2 && ctsReceived == 0); + ASSERT(startCallbacks == 0 && submissions == 0 && aggregateCompletions == 0 && successfulResponses == 0); + ASSERT(cancellations == 0 && dataOutcomes == 0 && memberDisposals == 0 && receiver->delivered == 0); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 0); + ASSERT(agreement && !agreement->hasPendingProbe()); + auto owner = edca->getEdcaf(AC_BE); + ASSERT(owner->getPendingQueue()->getNumPackets() == int(sources.size())); + ASSERT(owner->getInProgressFrames()->getLength() == int(canonical.size())); + ASSERT(service()->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(canonical.size())); + for (const auto& source : sources) { + auto packet = edca->getLifetimeHandler()->findFrame(source.id); + ASSERT(packet == source.packet && packet->getOwner() == source.owner); + ASSERT(edca->getLifetimeHandler()->getDeadline(packet) == source.deadline); + ASSERT(!packet->findTag()); + ASSERT(packet->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + } + for (const auto& retained : canonical) { + auto packet = retained.packet; + ASSERT(owner->getInProgressFrames()->containsFrame(packet) && packet->getOwner() == retained.owner); + ASSERT(edca->getLifetimeHandler()->getDeadline(packet) == retained.deadline); + auto header = packet->peekAtFront(); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic(retained.sequence) && header->getRetry()); + ASSERT(packet->getTag()->getAllocationIndex() == retained.allocation); + ASSERT(owner->getAckHandler()->getQoSDataAckStatus(header) == retained.status); + assertNoMemberRetryCount(packet); + } + } + else if (cancellationCase()) { + ASSERT(postCtsEvent && ctsReceived == 1 && rtsSent == 1); + ASSERT(startCallbacks == 0 && submissions == 0 && aggregateCompletions == 0); + ASSERT(cancellations == 1 && successfulResponses == 0 && receiver->delivered == 0); + ASSERT(service()->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(0)); + ASSERT(!frameSequenceHandler->isSequenceRunning() && !tx->hasTransmission()); + ASSERT(!originatorBlockAckAgreementHandler->getAgreement(peer, 0)); + ASSERT(inactivityEvents == (scenario() == 7 ? 1 : 0)); + for (const auto& source : sources) { + auto retained = edca->getLifetimeHandler()->findFrame(source.id); + if (scenario() == 8) ASSERT(!retained); + else { + ASSERT(retained == source.packet && retained->getOwner() == source.owner); + ASSERT(edca->getLifetimeHandler()->getDeadline(retained) == source.deadline); + ASSERT(!retained->findTag()); + } + } + } + else { + ASSERT(startCallbacks == 1 && submissions == 1 && aggregateCompletions == 1 && successfulResponses == 1); + ASSERT(cancellations == 0 && receiver->delivered == submittedMembers); + ASSERT(rtsSent == ((scenario() == 1 || scenario() == 2 || (scenario() == 3 && mcs() == 7) || scenario() == 6) ? 1 : 0)); + auto policy = check_and_cast(rtsPolicy); + if (scenario() == 4) { + bool protectedFull = false, unprotectedReduced = false; + for (auto query : policy->aggregateQueries) { + protectedFull |= query.first == fullLength && query.second; + unprotectedReduced |= query.first == reducedLength && !query.second; + } + ASSERT(protectedFull && unprotectedReduced); + ASSERT(edca->getEdcaf(AC_BE)->getPendingQueue()->getNumPackets() == 1); + ASSERT(service()->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(2)); + } + } + std::cout << "Aggregate admission assertions passed: scenario=" << scenario() << " mcs=" << mcs() + << " band5=" << par("band5").boolValue() << "\n"; + } + protected: + void originatorProcessRtsProtectionFailed(Packet *packet) override { + Hcf::originatorProcessRtsProtectionFailed(packet); + ASSERT(rtsFailureCase()); + rtsFailures++; + ASSERT(rtsFailures <= 2); + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + ASSERT(prepared && prepared->batch && !prepared->submitted); + auto owner = edca->getChannelOwner(); + ASSERT(owner->getRecoveryProcedure()->getShortRetryLimit() == 1); + for (auto member : prepared->batch->members) + assertNoMemberRetryCount(member); + ASSERT(!prepared->agreement->hasPendingProbe()); + if (rtsFailures == 2) driver()->verifyAfter(SimTime(1, SIMTIME_NS)); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == Ieee80211Mac::frameTransmissionOutcomeSignal) { + auto packet = check_and_cast(object); + if (dynamicPtrCast(packet->peekAtFront())) dataOutcomes++; + } + else { + if (signal == packetDroppedSignal) { + auto packet = check_and_cast(object); + if (dynamicPtrCast(packet->peekAtFront())) memberDisposals++; + } + Hcf::receiveSignal(source, signal, object, details); + } + } + void originatorProcessTransmittedFrame(Packet *packet) override { + if (auto header = dynamicPtrCast(packet->peekAtFront())) { + if (header->getType() == ST_RTS) rtsSent++; + if (dynamicPtrCast(header)) implicitProbes++; + } + Hcf::originatorProcessTransmittedFrame(packet); + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + auto header = packet->peekAtFront(); + bool cts = header->getType() == ST_CTS; + bool blockAck = bool(dynamicPtrCast(header)); + Hcf::originatorProcessReceivedFrame(packet, transmitted); + if (cts) { + ctsReceived++; + ctsTime = simTime(); + if (cancellationCase()) { + ASSERT(service()->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(0)); + auto interval = modeSet->getSifsTime() / 2; + if (scenario() == 7) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 0); + agreement->setBlockAckTimeoutValue(interval); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(); + } + else scheduleAfter(interval, new cMessage("reset-during-sifs")); + driver()->verifyAfter(modeSet->getSifsTime() + SimTime(1, SIMTIME_NS)); + } + } + if (blockAck) { + successfulResponses++; + if (reservationCase() && successfulResponses == 1) { + auto txop = edca->getChannelOwner()->getTxopProcedure(); + auto deadline = txop->getStart() + txop->getLimit(); + auto responseMode = rateSelection->computeCompressedBlockAckResponseMode(expectedMode); + ASSERT(simTime() + modeSet->getSifsTime() + expectedMode->getDuration(reducedLength) + + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_COMPRESSED_BLOCKACK) < deadline); + if (scenario() == 16) ASSERT(txop->getReservationEnd() > deadline); + else ASSERT(txop->getReservationEnd() == deadline); + checkedReservation = true; + if (scenario() == 16) driver()->verifyAfter(modeSet->getSifsTime() + SimTime(1, SIMTIME_NS)); + } + else driver()->verifyAfter(SimTime(1, SIMTIME_NS)); + } + } + void handleMessage(cMessage *message) override { + if (message->isName("reset-during-sifs")) { + delete message; + ASSERT(tx->hasTransmission()); + postCtsEvent = true; + resetPeer(peer); + } + else { + bool inactivity = message == inactivityTimer; + if (inactivity) { + ASSERT(cancellationCase() && scenario() == 7 && tx->hasTransmission()); + ASSERT(ctsTime < simTime() && simTime() < ctsTime + modeSet->getSifsTime()); + postCtsEvent = true; + inactivityEvents++; + } + Hcf::handleMessage(message); + } + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (!header) aggregateCompletions++; + Hcf::transmissionComplete(packet, header); + } + void originatorProcessFailedFrame(Packet *packet) override { + failures++; + Hcf::originatorProcessFailedFrame(packet); + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + delivered++; + take(packet); + delete packet; + } + } +}; +Define_Module(AdmissionHcf); + +void AdmissionDriver::handleMessage(cMessage *message) +{ + auto sender = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf")); + auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); + bool begin = message->isName("begin"); + delete message; + if (begin) { + sender->seed(receiver); + receiver->seed(sender); + sender->begin(); + } + else { + sender->verify(receiver); + endSimulation(); + } +} + +%file: test.ned +import inet.node.inet.AdhocHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.originator.OriginatorQosMacDataService; +import inet.linklayer.ieee80211.mac.originator.QosRtsPolicy; +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module AdmissionHcf extends Hcf { parameters: int scenario; int mcs; bool band5; @class(::AdmissionHcf); } +module AdmissionRtsPolicy extends QosRtsPolicy { parameters: @class(::AdmissionRtsPolicy); } +module AdmissionErrorModel extends Ieee80211NistErrorModel { parameters: bool dropCts = default(false); @class(::AdmissionErrorModel); } +simple AdmissionDriver { parameters: @class(::AdmissionDriver); } +network Test { + submodules: + radioMedium: Ieee80211RadioMedium; + sender: AdhocHost; + receiver: AdhocHost; + driver: AdmissionDriver; +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.wlan[0].opMode = ${band5} ? "ac" : "n(mixed-2.4Ghz)" +**.wlan[0].mac.qosStation = true +**.wlan[0].mac.hcf.typename = "AdmissionHcf" +**.wlan[0].mac.hcf.scenario = ${scenario=0..17} +**.wlan[0].mac.hcf.mcs = ${mcs=0,7} +**.wlan[0].mac.hcf.band5 = ${band5=false,true} +**.wlan[0].mac.hcf.isBlockAckSupported = true +**.wlan[0].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[0].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +**.wlan[0].mac.hcf.rtsPolicy.typename = "AdmissionRtsPolicy" +**.wlan[0].mac.hcf.rateSelection.dataFrameBitrate = ${mcs} == 0 ? 6.5Mbps : 65Mbps +**.wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +**.wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +**.wlan[0].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +**.wlan[0].mac.hcf.rateSelection.responseCtsFrameBitrate = 6Mbps +**.wlan[0].mac.hcf.edca.*.txopProcedure.txopLimit = admissionTxopLimit(${scenario}, ${mcs}, ${band5}) +**.wlan[0].mac.hcf.edca.*.txopProcedure.protectionMechanism = (${scenario} == 5 || ${scenario} == 6 || ${scenario} == 9 || ${scenario} >= 16) ? "multiple" : "single" +**.wlan[0].radio.receiver.errorModel.typename = "AdmissionErrorModel" +*.sender.wlan[0].radio.receiver.errorModel.dropCts = ${scenario} >= 13 && ${scenario} <= 15 +**.wlan[0].mac.hcf.edca.*.recoveryProcedure.shortRetryLimit = (${scenario} >= 13 && ${scenario} <= 15) ? 1 : 7 +**.wlan[0].radio.bandName = ${band5} ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +**.wlan[0].mib.htMaxAmpduLengthExponent = 3 +**.wlan[0].radio.receiver.bandwidth = 20MHz +**.wlan[0].radio.separateReceptionParts = true +*.radioMedium.backgroundNoise.power = -110dBm +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = ${scenario} == 16 ? 250ns : (${scenario} == 17 ? 500ns : 0s) +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +constraint = $scenario < 13 || ($mcs == 7 && !$band5) + +%contains: stdout +Aggregate admission assertions passed: + +%contains: stdout +Run statistics: total 57, successful 57 diff --git a/tests/module/Ieee80211AmpduExchange_1.test b/tests/module/Ieee80211AmpduExchange_1.test new file mode 100644 index 00000000000..f444f92af81 --- /dev/null +++ b/tests/module/Ieee80211AmpduExchange_1.test @@ -0,0 +1,372 @@ +%description: +Production association establishes HT limits before two aggregate DATA hops. +The source sends sixty UDP payloads through an AP to a second station. +The fixture checks actual member FCS, shared addresses and Duration, policy bits, and implicit responses. +The matrix varies scalar or dimensional PHY, NIST or Yans, protection, and TXOP limit. +All cases use seed zero and fixed HT20 long-GI DATA rates. + +%extraargs: -r 0..15 + +%postrun-command: python3 check-exchange.py + +%file: Exchange.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class AmpduExchangeLossModel : public Ieee80211NistErrorModel +{ + mutable std::map losses; + mutable int implicitResponses = 0; + mutable int probeResponses = 0; + mutable int partialTransmission = -1; + mutable int partialMembers = 0; + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override { + auto transmission = snir->getReception()->getTransmission(); + auto packet = transmission->getPacket(); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto ht = dynamicPtrCast(phy); + if (ht && ht->getAggregation()) + return 0; + auto header = packet->peekDataAt(phy->getChunkLength()); + if (header->getType() != ST_BLOCKACK || par("lossMode").intValue() == 0) + return 0; + auto id = transmission->getId(); + auto found = losses.find(id); + if (found == losses.end()) { + bool implicit = strstr(packet->getName(), "Implicit") != nullptr; + bool lose = implicit ? implicitResponses++ == 0 : + implicitResponses > 0 && par("lossMode").intValue() == 2 && probeResponses++ == 0; + found = losses.emplace(id, lose).first; + } + return found->second ? 1 : 0; + } + double computeChunkSuccessRate(const ISnir *snir, b bitLength) const override { + B length(bitLength); + if (par("lossMode").intValue() != 3 || length == B(4)) + return 1; + auto id = snir->getReception()->getTransmission()->getId(); + if (partialTransmission == -1) + partialTransmission = id; + return id == partialTransmission && partialMembers++ == 0 ? 0 : 1; + } +}; +Define_Module(AmpduExchangeLossModel); + +class AmpduExchangeHcf : public Hcf +{ + public: + int aggregates = 0; + int members = 0; + int responses = 0; + int positive = 0; + int responsesSent = 0; + int acceptedDialogs = 0; + int failedImplicit = 0; + int probes = 0; + int dataAfterProbe = 0; + int barsSinceData = 0; + bool awaitProbe = false; + int attemptOutcomes = 0; + int retryBatches = 0; + int mixedBatches = 0; + std::map deadlines; + static bool isAggregate(Packet *packet) { + return dynamicPtrCast(packet->peekAtFront()) != nullptr; + } + protected: + void initialize(int stage) override { + Hcf::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == Ieee80211Mac::frameTransmissionOutcomeSignal) { + auto packet = check_and_cast(object); + if (dynamicPtrCast(packet->peekAtFront())) + attemptOutcomes++; + } + else + Hcf::receiveSignal(source, signal, object, details); + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (isAggregate(packet)) { + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + bool newMember = false; + bool retryMember = false; + int previousRetry = -1; + int previousNew = -1; + for (auto member : prepared->batch->members) { + auto data = member->peekAtFront(); + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(prepared->limits.windowStart, data->getSequenceNumber()); + if (data->getRetry()) { + ASSERT(!newMember && offset > previousRetry); + previousRetry = offset; + retryMember = true; + } + else { + ASSERT(offset > previousNew); + previousNew = offset; + newMember = true; + } + auto index = member->getTag()->getAllocationIndex(); + auto deadline = edca->getLifetimeHandler()->getDeadline(member); + auto previous = deadlines.find(index); + if (previous != deadlines.end()) + ASSERT(previous->second == deadline); + else + deadlines.emplace(index, deadline); + } + retryBatches += retryMember; + mixedBatches += retryMember && newMember; + if (probes > 0) + dataAfterProbe++; + barsSinceData = 0; + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() >= 0 && !mode->isHtShortGuardInterval()); + ASSERT(mode->getDataMode()->getBandwidth() == MHz(20)); + MacAddress receiver, transmitter; + Tid tid = -1; + simtime_t duration = -1; + int count = 0; + bool complete = MpduDeaggregation::visitSubframes(packet, B(0), B(packet->getDataLength()), + [](const auto&, B, bool valid) { ASSERT(valid); }, + [&](const auto& mpdu, B) { + Packet member("observed-member", mpdu); + auto data = member.peekAtFront(); + ASSERT(data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == NORMAL_ACK); + ASSERT(!data->getAMsduPresent() && !data->getMoreFragments() && data->getFragmentNumber() == 0); + if (count == 0) { + receiver = data->getReceiverAddress(); + transmitter = data->getTransmitterAddress(); + tid = data->getTid(); + duration = data->getDurationField(); + } + ASSERT(data->getReceiverAddress() == receiver && data->getTransmitterAddress() == transmitter); + ASSERT(data->getTid() == tid && data->getDurationField() == duration); + auto trailer = member.removeAtBack(B(4)); + ASSERT(trailer->getFcsMode() == FCS_COMPUTED); + auto bytes = member.peekAllAsBytes()->getBytes(); + ASSERT(ethernetFcs(bytes.data(), bytes.size()) == trailer->getFcs()); + count++; + }); + ASSERT(complete); + ASSERT(count > 0 && count <= 64); + ASSERT(mib->findPeerHtState(receiver) && mib->findPeerHtState(receiver)->valid); + aggregates++; + members += count; + } + else if (header && dynamicPtrCast(header) && awaitProbe) { + ASSERT(++barsSinceData == 1); + if (par("requireFreshProbe").boolValue()) + ASSERT(!edca->getChannelOwner()->getPendingQueue()->isEmpty()); + probes++; + awaitProbe = false; + } + Hcf::transmissionComplete(packet, header); + } + void originatorProcessFailedFrame(Packet *packet) override { + if (isAggregate(packet)) { + failedImplicit++; + awaitProbe = true; + } + Hcf::originatorProcessFailedFrame(packet); + } + void originatorProcessReceivedFrame(Packet *received, Packet *transmitted) override { + if (isAggregate(transmitted)) { + auto blockAck = received->peekAtFront(); + ASSERT(received->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + ASSERT(blockAck->getReceiverAddress() == mib->address); + responses++; + positive += blockAck->getBlockAckBitmap() != BitVector(0, 64); + } + Hcf::originatorProcessReceivedFrame(received, transmitted); + } + void recipientProcessTransmittedControlResponseFrame(Packet *packet, + const Ptr& header) override { + if (dynamicPtrCast(header)) { + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() < 0); + responsesSent++; + } + Hcf::recipientProcessTransmittedControlResponseFrame(packet, header); + } +}; +Define_Module(AmpduExchangeHcf); + +class AmpduExchangeTest : public cSimpleModule, public cListener +{ + int delivered = 0; + int bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(10, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *) override { + auto packet = check_and_cast(object); + delivered++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.sta2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(1400, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(delivered == 60 && bytes == 72000); + for (const char *path : {"^.sta1.wlan[0].mac.hcf", "^.ap.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + std::cout << "Aggregate exchange: node=" << path << " aggregates=" << hcf->aggregates + << " members=" << hcf->members << " responses=" << hcf->responses + << " positive=" << hcf->positive << " probes=" << hcf->probes + << " failures=" << hcf->failedImplicit << " dataAfterProbe=" << hcf->dataAfterProbe + << " outcomes=" << hcf->attemptOutcomes << " retries=" << hcf->retryBatches + << " mixed=" << hcf->mixedBatches << "\n"; + ASSERT(hcf->aggregates > 0 && hcf->members > hcf->aggregates); + ASSERT(hcf->responses > 0 && hcf->positive > 0); + ASSERT(hcf->attemptOutcomes == hcf->members); + if (par("recovery").boolValue()) { + ASSERT(hcf->failedImplicit > 0 && hcf->probes > 0 && hcf->dataAfterProbe > 0); + } + } + if (par("recovery").boolValue()) { + auto mode = getModuleByPath("^.sta1.wlan[0].radio.receiver.errorModel")->par("lossMode").intValue(); + auto source = check_and_cast(getModuleByPath("^.sta1.wlan[0].mac.hcf")); + if (mode == 2 || mode == 3) + ASSERT(source->retryBatches > 0); + if (mode == 3) + ASSERT(source->mixedBatches > 0); + } + std::cout << "Implicit aggregate exchange assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(AmpduExchangeTest); + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.common.contract.packetlevel.IRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module AmpduExchangeHcf extends Hcf { parameters: bool requireFreshProbe = default(false); @class(::AmpduExchangeHcf); } +module AmpduExchangeLossModel extends Ieee80211NistErrorModel { parameters: int lossMode = default(0); @class(::AmpduExchangeLossModel); } +simple AmpduExchangeTest { parameters: bool recovery = default(false); @class(::AmpduExchangeTest); } +network AmpduExchangeNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: like IRadioMedium; + ap: AccessPoint; + sta1: WirelessHost; + sta2: WirelessHost; + test: AmpduExchangeTest; +} + +%inifile: omnetpp.ini +[General] +network = AmpduExchangeNetwork +ned-path = .;../../../../src +sim-time-limit = 1.5s +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.radioMedium.typename = ${medium="Ieee80211ScalarRadioMedium","Ieee80211DimensionalRadioMedium"} +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +*.sta1.numApps = 1 +*.sta1.app[0].typename = "UdpBasicApp" +*.sta1.app[0].destAddresses = "sta2" +*.sta1.app[0].destPort = 1000 +*.sta1.app[0].messageLength = 1200B +*.sta1.app[0].startTime = 1s +*.sta1.app[0].stopTime = 1.00119s +*.sta1.app[0].sendInterval = 20us +*.sta2.numApps = 1 +*.sta2.app[0].typename = "UdpSink" +*.sta2.app[0].localPort = 1000 +*.sta*.ipv4.arp.typename = "GlobalArp" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].radio.typename = ${radio="Ieee80211ScalarRadio","Ieee80211DimensionalRadio" ! medium} +**.wlan[*].radio.receiver.errorModel.typename = ${model="Ieee80211NistErrorModel","Ieee80211YansErrorModel"} +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.fcsMode = "computed" +**.checksumMode = "computed" +**.wlan[*].classifier.typename = "QosClassifier" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "AmpduExchangeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +**.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorAckPolicy.blockAckReqThreshold = 5 +**.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.rtsPolicy.rtsThreshold = 2346B +**.txopProcedure.protectionMechanism = ${protection="single","multiple"} +**.txopProcedure.txopLimit = ${limit=0s,4ms} +**.bandName = "2.4 GHz" +**.wlan[*].radio.receiver.energyDetection = -105dBm +**.wlan[*].radio.receiver.sensitivity = -105dBm +**.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sta1.mobility.initialX = 0m +*.ap.mobility.initialX = 5m +*.sta2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +[Config Recovery] +constraint = $medium == "Ieee80211ScalarRadioMedium" && $model == "Ieee80211NistErrorModel" && $protection == "multiple" && $limit == 0s +*.test.recovery = true +*.sta1.wlan[0].mac.hcf.requireFreshProbe = true +*.radioMedium.typename = "Ieee80211ScalarRadioMedium" +**.wlan[*].radio.typename = "Ieee80211ScalarRadio" +**.wlan[*].radio.receiver.errorModel.typename = "AmpduExchangeLossModel" +**.wlan[*].radio.receiver.errorModel.lossMode = ${loss=1,2,3} +**.txopProcedure.txopLimit = 0s +**.txopProcedure.protectionMechanism = "multiple" + +%file: check-exchange.py +from pathlib import Path +import os +import subprocess + +text = Path("test.out").read_text() +assert "Run statistics: total 16, successful 16" in text +assert "Warning:" not in text +executables = [path for path in Path.cwd().glob("Ieee80211AmpduExchange_1*") if path.is_file() and os.access(path, os.X_OK)] +assert len(executables) == 1, executables +command = [str(executables[0]), "-u", "Cmdenv", "--check-signals=false", "-n", "../../../../src:.:../../lib", "omnetpp.ini", "_defaults.ini", "-c", "Recovery", "-r", "0..2"] +result = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) +Path("recovery.out").write_text(result.stdout) +assert result.returncode == 0, result.stdout +assert "Run statistics: total 3, successful 3" in result.stdout +assert "Warning:" not in result.stdout +print("Sixteen infrastructure cases and three response-loss recovery cases passed.") + +%contains: stdout +Implicit aggregate exchange assertions passed. diff --git a/tests/module/Ieee80211AmpduRecovery_1.test b/tests/module/Ieee80211AmpduRecovery_1.test new file mode 100644 index 00000000000..c0321f3073e --- /dev/null +++ b/tests/module/Ieee80211AmpduRecovery_1.test @@ -0,0 +1,359 @@ +%description: +Real infrastructure association establishes HT state before the production HCF accepts test DATA. +The AP silently loses its recipient agreement at the first aggregate. The originator retains its agreement. +Continuous arrivals must permit an actual Compressed BAR, recipient DELBA, originator retirement, later ADDBA, and later aggregate DATA. +The pending DATA queue must remain nonempty from recipient state loss through the first BAR transmission. +Finite traffic loses every actual BAR at the AP PHY. MSDU expiry must close the originator agreement before inactivity expiry. +Four runs combine finite or continuous arrivals with zero or 200 ms inactivity timeout. Seed zero fixes contention. +The fixture injects complete QoS DATA at the HCF upper boundary. It does not test an application or the IP path. + +%extraargs: -r 0..3 + +%file: AmpduRecovery.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class RecoveryPhysicalModel : public Ieee80211NistErrorModel +{ + public: + mutable int barLosses = 0; + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart) const override { + auto packet = snir->getReception()->getTransmission()->getPacket(); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto ht = dynamicPtrCast(phy); + if ((!ht || !ht->getAggregation()) && par("dropBars").boolValue()) { + auto header = packet->peekDataAt(phy->getChunkLength()); + if (dynamicPtrCast(header)) { + barLosses++; + return 1; + } + } + return 0; + } + double computeChunkSuccessRate(const ISnir *, b) const override { return 1; } +}; +Define_Module(RecoveryPhysicalModel); + +class RecoveryHcf : public Hcf +{ + public: + bool recipientStateLost = false; + simtime_t lossTime; + int aggregateReceptions = 0; + int receivedDelbas = 0; + int deliveredData = 0; + MacAddress address() const { return mac->getAddress(); } + queueing::IPacketQueue *dataQueue() const { return edca->getEdcaf(AC_BE)->getPendingQueue(); } + int queuedData() const { return dataQueue()->getNumPackets(); } + int retainedData() const { return edca->getEdcaf(AC_BE)->getInProgressFrames()->getLength(); } + size_t trackedData() const { return edca->getLifetimeHandler()->getNumFrames(); } + OriginatorBlockAckAgreement *originatorAgreement(MacAddress peer) const { + return originatorBlockAckAgreementHandler->getAgreement(peer, 0); + } + void injectData(MacAddress receiver, int count) { + Enter_Method("injectData"); + for (int i = 0; i < count; i++) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(receiver); + header->setToDS(true); + header->setTid(0); + auto frame = new Packet("recovery-data", header); + frame->insertAtBack(makeShared(B(200))); + frame->insertAtBack(makeShared()); + processUpperFrame(frame, header); + } + } + void processLowerAggregate(Packet *packet, AmpduReception reception) override { + aggregateReceptions++; + if (par("loseRecipientState").boolValue() && !recipientStateLost && !reception.members.empty()) { + const auto& header = reception.members.front().packet->peekAtFront(); + auto peer = header->getTransmitterAddress(); + auto tid = header->getTid(); + ASSERT(recipientBlockAckAgreementHandler->getAgreement(tid, peer)); + recipientStateLost = true; + lossTime = simTime(); + // Model lost recipient state without a local DELBA or a peer reset. + retireAgreement(peer, tid, false, true); + ASSERT(!recipientBlockAckAgreementHandler->getAgreement(tid, peer)); + } + Hcf::processLowerAggregate(packet, std::move(reception)); + } + protected: + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + if (auto delba = dynamicPtrCast(header)) { + if (!delba->getInitiator()) receivedDelbas++; + } + Hcf::recipientProcessReceivedManagementFrame(header); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + if (dynamicPtrCast(frame->peekAtFront())) { + deliveredData++; + take(frame); + delete frame; + } + else + Hcf::sendUp({frame}); + } + } +}; +Define_Module(RecoveryHcf); + +class RecoveryDriver : public cSimpleModule, public cListener +{ + RecoveryHcf *originator = nullptr; + RecoveryHcf *recipient = nullptr; + cModule *originatorRadio = nullptr; + cModule *recipientRadio = nullptr; + int originatorAdditions = 0; + int originatorDeletions = 0; + int addbaTransmissions = 0; + int barTransmissions = 0; + int recipientDelbaTransmissions = 0; + int aggregateTransmissions = 0; + int laterAggregateTransmissions = 0; + int lifetimeDrops = 0; + int queuedAtProbe = -1; + bool drainedBeforeProbe = false; + bool retirementAtInactivity = false; + simtime_t firstProbeTime; + simtime_t firstRetirementTime; + simtime_t laterSetupTime; + simtime_t firstRecipientDelbaTime; + bool continuous() const { return par("continuous"); } + void checkBacklog() { + if (continuous() && recipient->recipientStateLost && barTransmissions == 0 && originator->queuedData() == 0) + drainedBeforeProbe = true; + } + void observeTransmission(cComponent *source, const Ieee80211Transmission *transmission) { + auto packet = transmission->getPacket(); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto ht = dynamicPtrCast(phy); + if (source == originatorRadio && ht && ht->getAggregation()) { + ASSERT(transmission->getMode()->getHtMcsIndex() == 7); + ASSERT(transmission->getMode()->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(!transmission->getMode()->isHtShortGuardInterval()); + aggregateTransmissions++; + if (originatorDeletions > 0 && originatorAdditions >= 2) + laterAggregateTransmissions++; + return; + } + if (ht && ht->getAggregation()) return; + auto header = packet->peekDataAt(phy->getChunkLength()); + if (source == originatorRadio) { + if (dynamicPtrCast(header)) { + addbaTransmissions++; + if (originatorDeletions > 0) laterSetupTime = simTime(); + } + if (auto bar = dynamicPtrCast(header)) { + if (recipient->recipientStateLost) { + ASSERT(bar->getTidInfo() == 0); + checkBacklog(); + if (barTransmissions == 0) { + firstProbeTime = simTime(); + queuedAtProbe = originator->queuedData(); + } + barTransmissions++; + } + } + } + if (source == recipientRadio) { + if (auto delba = dynamicPtrCast(header)) { + if (!delba->getInitiator() && recipient->recipientStateLost) { + if (recipientDelbaTransmissions == 0) firstRecipientDelbaTime = simTime(); + recipientDelbaTransmissions++; + } + } + } + } + public: + RecoveryDriver() : cSimpleModule(65536) {} + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == transmissionStartedSignal) { + observeTransmission(source, check_and_cast(object)); + return; + } + if (source == originator && signal == Hcf::blockAckAgreementAddedSignal && dynamic_cast(object)) + originatorAdditions++; + if (source == originator && signal == Hcf::blockAckAgreementDeletedSignal) { + if (auto agreement = dynamic_cast(object)) { + if (recipient->recipientStateLost) { + if (originatorDeletions == 0) firstRetirementTime = simTime(); + originatorDeletions++; + retirementAtInactivity |= agreement->getExpirationTime() <= simTime(); + } + } + } + if (source == originator && signal == packetDroppedSignal) { + auto drop = dynamic_cast(details); + if (drop && drop->getReason() == LIFETIME_EXPIRED) lifetimeDrops++; + } + checkBacklog(); + } + void activity() override { + originator = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.hcf")); + recipient = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + originatorRadio = getModuleByPath("^.sta.wlan[0].radio"); + recipientRadio = getModuleByPath("^.ap.wlan[0].radio"); + originatorRadio->subscribe(transmissionStartedSignal, this); + recipientRadio->subscribe(transmissionStartedSignal, this); + originator->subscribe(Hcf::blockAckAgreementAddedSignal, this); + originator->subscribe(Hcf::blockAckAgreementDeletedSignal, this); + originator->subscribe(packetDroppedSignal, this); + auto queue = check_and_cast(originator->dataQueue()); + for (auto signal : {packetPushedSignal, packetPulledSignal, packetRemovedSignal}) queue->subscribe(signal, this); + wait(SimTime(30, SIMTIME_MS)); + auto staMib = check_and_cast(getModuleByPath("^.sta.wlan[0].mib")); + auto apMib = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + ASSERT(staMib->bssStationData.isAssociated); + ASSERT(staMib->bssData.bssid == apMib->address); + auto peer = staMib->findPeerHtState(apMib->address); + ASSERT(peer && peer->valid && peer->negotiatedCapabilities.localTxPeerRx.valid); + ASSERT(apMib->findPeerHtState(staMib->address)); + ASSERT(peer->negotiatedCapabilities.localTxPeerRx.receiverMaxAmpduLengthExponent == 1); + ASSERT(peer->negotiatedCapabilities.localTxPeerRx.receiverMinimumMpduStartSpacing == 2); + originator->injectData(recipient->address(), continuous() ? 256 : 32); + if (continuous()) { + while (simTime() < SimTime(110, SIMTIME_MS)) { + wait(SimTime(100, SIMTIME_US)); + checkBacklog(); + originator->injectData(recipient->address(), 4); + } + } + wait(SimTime(120, SIMTIME_MS) - simTime()); + ASSERT(recipient->recipientStateLost && recipient->aggregateReceptions > 0); + ASSERT(aggregateTransmissions > 0 && barTransmissions > 0); + ASSERT(firstProbeTime > recipient->lossTime); + ASSERT(originatorDeletions > 0 && !retirementAtInactivity); + if (continuous()) { + ASSERT(!drainedBeforeProbe && queuedAtProbe > 0); + ASSERT(recipientDelbaTransmissions > 0 && originator->receivedDelbas > 0); + ASSERT(firstRecipientDelbaTime > firstProbeTime); + ASSERT(firstRetirementTime >= firstRecipientDelbaTime); + ASSERT(originatorAdditions >= 2 && addbaTransmissions >= 2); + ASSERT(laterSetupTime > firstRetirementTime && laterAggregateTransmissions > 0); + } + else { + auto model = check_and_cast(recipientRadio->getSubmodule("receiver")->getSubmodule("errorModel")); + ASSERT(model->barLosses > 0); + ASSERT(recipientDelbaTransmissions == 0 && originator->receivedDelbas == 0); + ASSERT(lifetimeDrops > 0 && firstRetirementTime >= SimTime(60, SIMTIME_MS)); + ASSERT(originatorAdditions == 1); + ASSERT(!originator->originatorAgreement(recipient->address())); + ASSERT(originator->queuedData() == 0 && originator->retainedData() == 0 && originator->trackedData() == 0); + } + std::cout << "Absent-agreement recovery passed: continuous=" << continuous() + << " probes=" << barTransmissions << " delbas=" << recipientDelbaTransmissions + << " retirements=" << originatorDeletions << " setups=" << addbaTransmissions + << " laterAggregates=" << laterAggregateTransmissions << " lifetimeDrops=" << lifetimeDrops << "\n"; + endSimulation(); + } +}; +Define_Module(RecoveryDriver); + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module RecoveryPhysicalModel extends Ieee80211NistErrorModel { parameters: bool dropBars = default(false); @class(::RecoveryPhysicalModel); } +module RecoveryHcf extends Hcf { parameters: bool loseRecipientState = default(false); @class(::RecoveryHcf); } +simple RecoveryDriver { parameters: bool continuous; @class(::RecoveryDriver); } +network RecoveryNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: WirelessHost; + ap: AccessPoint; + test: RecoveryDriver; +} + +%inifile: omnetpp.ini +[General] +network = RecoveryNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 125ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.test.continuous = ${continuous=false,true} +*.ap.wlan[0].mgmt.typename = "Ieee80211MgmtAp" +*.sta.wlan[0].mgmt.typename = "Ieee80211MgmtSta" +*.sta.wlan[0].agent.typename = "Ieee80211AgentSta" +*.sta.wlan[0].agent.startingTime = 0s +*.sta.wlan[0].agent.channelsToScan = "6" +*.sta.wlan[0].agent.probeDelay = 1ms +*.sta.wlan[0].agent.minChannelTime = 5ms +*.sta.wlan[0].agent.maxChannelTime = 5ms +*.sta.wlan[0].agent.authenticationTimeout = 20ms +*.sta.wlan[0].agent.associationTimeout = 20ms +**.mgmt.numChannels = 1 +*.ap.wlan[0].address = "02:00:00:00:00:01" +*.sta.wlan[0].address = "02:00:00:00:00:02" +**.wlan[0].opMode = "n(mixed-2.4Ghz)" +**.wlan[0].bitrate = 65Mbps +**.wlan[0].radio.bandName = "2.4 GHz" +**.wlan[0].radio.channelNumber = 6 +**.wlan[0].radio.receiver.bandwidth = 20MHz +**.wlan[0].radio.receiver.errorModel.typename = "RecoveryPhysicalModel" +*.ap.wlan[0].radio.receiver.errorModel.dropBars = !${continuous} +*.ap.wlan[0].mib.htMaxAmpduLengthExponent = 1 +*.ap.wlan[0].mib.htMinimumMpduStartSpacing = 2 +*.sta.wlan[0].mib.htMaxAmpduLengthExponent = 0 +*.sta.wlan[0].mib.htMinimumMpduStartSpacing = 1 +**.wlan[0].mac.qosStation = true +**.wlan[0].mac.hcf.typename = "RecoveryHcf" +*.ap.wlan[0].mac.hcf.loseRecipientState = true +**.wlan[0].mac.hcf.isBlockAckSupported = true +**.wlan[0].mac.hcf.blockAckVariant = "compressed" +**.wlan[0].mac.hcf.rateControl.typename = "" +**.wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +**.wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +**.wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +**.wlan[0].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +**.wlan[0].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +**.wlan[0].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[0].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 2 +**.wlan[0].mac.hcf.originatorBlockAckAgreementPolicy.blockAckTimeoutValue = ${inactivity=0s,200ms} +**.wlan[0].mac.hcf.recipientBlockAckAgreementPolicy.blockAckTimeoutValue = ${inactivity} +**.wlan[0].mac.hcf.edca.edcaf[*].msduLifetime = 30ms +**.wlan[0].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = 1024 +**.wlan[0].mac.hcf.edca.edcaf[*].recoveryProcedure.longRetryLimit = 1024 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sta.mobility.initialX = 10m +*.ap.mobility.initialX = 20m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +*.radioMedium.backgroundNoise.power = -110dBm + +%contains: stdout +Absent-agreement recovery passed: + +%contains: stdout +Run statistics: total 4, successful 4 diff --git a/tests/module/Ieee80211AmpduReordering_1.test b/tests/module/Ieee80211AmpduReordering_1.test index 13169e08066..c487b1201d9 100644 --- a/tests/module/Ieee80211AmpduReordering_1.test +++ b/tests/module/Ieee80211AmpduReordering_1.test @@ -249,6 +249,7 @@ class ReorderPhysicalDriver : public cSimpleModule, public cListener, public ITx delete packet; } public: + void frameTransmissionStarted(Packet *) override {} void transmissionComplete(Packet *, const Ptr& header) override { ASSERT(header && header->getType() == ST_CTS); unrelatedCompletions++; diff --git a/tests/module/Ieee80211AmpduResponseWindow_1.test b/tests/module/Ieee80211AmpduResponseWindow_1.test new file mode 100644 index 00000000000..c7529b1facd --- /dev/null +++ b/tests/module/Ieee80211AmpduResponseWindow_1.test @@ -0,0 +1,349 @@ +%description: +A real HCF waits for an ACK after ordinary QoS DATA transmission. +A second radio starts an HT aggregate before the response timer expires. +The aggregate HEADER ends before expiry. Its DATA remains active at expiry. +The timer consumes its event while the production ReceiveStep remains active. +Overheard aggregate reception must end that wait with one originator failure. +Addressed aggregate reception must end that wait before one real Block Ack transmission. +The fixture supplies recipient agreement and peer HT state. It does not test ADDBA negotiation. +Both cases use long and short GI, separate reception parts, deterministic reception, and seed zero. +The addressed cases complete one non-HT Compressed Block Ack at 6 Mbps after SIFS. + +%extraargs: -r 0..3 + +%file: AmpduResponseWindow.cc +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class ResponseWindowHcf; + +class ResponseWindowDriver : public cSimpleModule, public cListener +{ + ResponseWindowHcf *hcf = nullptr; + const IIeee80211Mode *mode = nullptr; + simtime_t responseDeadline; + bool armed = false; + bool transmitted = false; + public: + void arm(simtime_t deadline); + protected: + void initialize() override; + void handleMessage(cMessage *message) override; + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *) override; + Packet *createAggregate(); +}; +Define_Module(ResponseWindowDriver); + +class ResponseWindowErrorModel : public Ieee80211NistErrorModel +{ + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeChunkSuccessRate(const ISnir *, b) const override { return 1; } +}; +Define_Module(ResponseWindowErrorModel); + +class ResponseWindowAgreements : public RecipientBlockAckAgreementHandler +{ + public: + void accept(MacAddress peer) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; + +class ResponseWindowHcf : public Hcf +{ + IRadio *physicalRadio = nullptr; + simtime_t aggregateEnd; + public: + int waitTimers = 0; + int consumedTimers = 0; + int failedFrames = 0; + int finishedSequences = 0; + int receivedAggregates = 0; + int transmittedBlockAcks = 0; + int delivered = 0; + bool observedDataAtExpiry = false; + MacAddress address() const { return mac->getAddress(); } + void begin() { + Enter_Method("begin"); + physicalRadio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(physicalRadio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + auto incomingPeer = MacAddress("02:00:00:00:00:02"); + mib->setPeerHtCapabilities(incomingPeer, mib->localHtCapabilities, mib->getHtOperation()); + delete recipientBlockAckAgreementHandler; + auto accepted = new ResponseWindowAgreements(); + accepted->accept(incomingPeer); + recipientBlockAckAgreementHandler = accepted; + + // The absent outgoing peer supplies no ACK. Its absent HT state retains Normal Ack DATA. + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(address()); + header->setAddress3(header->getReceiverAddress()); + header->setTid(0); + auto packet = new Packet("outgoing-normal-data", header); + packet->insertAtBack(makeShared(B(100))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void verify(bool addressed) { + Enter_Method("verify"); + ASSERT(waitTimers == 1 && consumedTimers == 1 && observedDataAtExpiry); + ASSERT(receivedAggregates == 1); + ASSERT(failedFrames == 1 && finishedSequences == 1); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + ASSERT(edca->getChannelOwner() == nullptr); + ASSERT(!startRxTimer->isScheduled()); + ASSERT(transmittedBlockAcks == (addressed ? 1 : 0)); + ASSERT(delivered == (addressed ? 2 : 0)); + ASSERT(!tx->hasTransmission()); + std::cout << "Aggregate response-window assertions passed: addressed=" << addressed << "\n"; + } + void processLowerAggregate(Packet *packet, AmpduReception reception) override { + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() == 0 && mode->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(mode->isHtShortGuardInterval() == par("shortGi").boolValue()); + ASSERT(!mode->isHtShortGuardInterval() || mib->localHtCapabilities.shortGi20); + ASSERT(!mib->getBssRateSet().basic.known); + aggregateEnd = simTime(); + ASSERT(consumedTimers == 1); + ASSERT(frameSequenceHandler->isSequenceRunning()); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::RECEIVE); + ASSERT(!startRxTimer->isScheduled()); + receivedAggregates++; + Hcf::processLowerAggregate(packet, std::move(reception)); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + ASSERT(edca->getChannelOwner() == nullptr); + } + protected: + void scheduleStartRxTimer(simtime_t timeout) override { + Hcf::scheduleStartRxTimer(timeout); + waitTimers++; + ASSERT(waitTimers == 1); + check_and_cast(getSimulation()->getSystemModule()->getSubmodule("driver"))->arm(startRxTimer->getArrivalTime()); + } + void handleMessage(cMessage *message) override { + if (message == startRxTimer) { + ASSERT(physicalRadio->getReceivedSignalPart() == IRadioSignal::SIGNAL_PART_DATA); + ASSERT(isReceptionInProgress()); + ASSERT(frameSequenceHandler->isSequenceRunning()); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::RECEIVE); + observedDataAtExpiry = true; + Hcf::handleMessage(message); + consumedTimers++; + ASSERT(!startRxTimer->isScheduled()); + ASSERT(frameSequenceHandler->isSequenceRunning()); + } + else + Hcf::handleMessage(message); + } + void originatorProcessFailedFrame(Packet *packet) override { + ASSERT(packet->peekAtFront()->getReceiverAddress() == MacAddress("02:00:00:00:00:09")); + failedFrames++; + Hcf::originatorProcessFailedFrame(packet); + } + void frameSequenceFinished() override { + finishedSequences++; + Hcf::frameSequenceFinished(); + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) { + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() < 0 && mode->getDataMode()->getNetBitrate() == bps(6000000)); + ASSERT(simTime() - mode->getDuration(packet->getDataLength()) == aggregateEnd + modeSet->getSifsTime()); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + ASSERT(edca->getChannelOwner() == nullptr); + transmittedBlockAcks++; + } + Hcf::transmissionComplete(packet, header); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered++; + take(frame); + delete frame; + } + } +}; +Define_Module(ResponseWindowHcf); + +void ResponseWindowDriver::initialize() +{ + hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + getModuleByPath("^.sender.radio")->subscribe(transmissionStartedSignal, this); + auto modes = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < modes->getNumModes(); i++) { + auto candidate = modes->getMode(i); + if (candidate->getHtMcsIndex() == 0 && candidate->getDataMode()->getBandwidth() == MHz(20) && + candidate->isHtShortGuardInterval() == par("shortGi").boolValue()) + mode = candidate; + } + ASSERT(mode); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); +} + +void ResponseWindowDriver::arm(simtime_t deadline) +{ + Enter_Method("arm"); + ASSERT(!armed); + armed = true; + responseDeadline = deadline; + ASSERT(simTime() + SimTime(20, SIMTIME_US) < deadline); + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("aggregate")); +} + +Packet *ResponseWindowDriver::createAggregate() +{ + auto packet = new Packet("incoming-aggregate"); + for (int sequence : {100, 101}) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(par("addressed").boolValue() ? hcf->address() : MacAddress("02:00:00:00:00:08")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setAddress3(header->getTransmitterAddress()); + header->setTid(5); + header->setAckPolicy(NORMAL_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setDurationField(SIMTIME_ZERO); + auto delimiter = makeShared(); + delimiter->setLength(1000); + packet->insertAtBack(delimiter); + packet->insertAtBack(header); + packet->insertAtBack(makeShared(B(1000) - header->getChunkLength() - B(4))); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(trailer); + } + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + packet->addTag()->setMode(mode); + return packet; +} + +void ResponseWindowDriver::receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *) +{ + Enter_Method_Silent(); + auto transmission = check_and_cast(object); + ASSERT(armed && !transmitted); + transmitted = true; + // The real radio must reach DATA before expiry and remain there until after expiry. + ASSERT(transmission->getStartTime(IRadioSignal::SIGNAL_PART_DATA) < responseDeadline); + ASSERT(responseDeadline < transmission->getEndTime(IRadioSignal::SIGNAL_PART_DATA)); + scheduleAt(transmission->getEndTime() + SimTime(1, SIMTIME_MS), new cMessage("verify")); +} + +void ResponseWindowDriver::handleMessage(cMessage *message) +{ + if (message->isName("begin")) { + delete message; + hcf->begin(); + } + else if (message->isName("aggregate")) { + delete message; + send(createAggregate(), "out"); + } + else { + delete message; + ASSERT(transmitted); + hcf->verify(par("addressed").boolValue()); + endSimulation(); + } +} + +%file: test.ned +import inet.mobility.static.StationaryMobility; +import inet.node.inet.AdhocHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module ResponseWindowHcf extends Hcf { parameters: bool shortGi; @class(::ResponseWindowHcf); } +module ResponseWindowErrorModel extends Ieee80211NistErrorModel { parameters: @class(::ResponseWindowErrorModel); } +simple ResponseWindowDriver { parameters: bool addressed; bool shortGi; @class(::ResponseWindowDriver); gates: output out; } +module ResponseWindowSender { + parameters: @networkNode; + gates: input in; input radioIn @directIn; + submodules: + mobility: StationaryMobility; + radio: Ieee80211Radio; + connections allowunconnected: + in --> radio.upperLayerIn; + radioIn --> radio.radioIn; +} +network Test { + submodules: + radioMedium: Ieee80211RadioMedium; + driver: ResponseWindowDriver; + sender: ResponseWindowSender; + host: AdhocHost; + connections: + driver.out --> sender.in; +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +*.driver.addressed = ${addressed=false,true} +*.driver.shortGi = ${shortGi=false,true} +*.host.wlan[0].mac.hcf.shortGi = ${shortGi} +*.host.wlan[0].opMode = "n(mixed-2.4Ghz)" +*.host.wlan[0].mac.qosStation = true +*.host.wlan[0].mac.hcf.typename = "ResponseWindowHcf" +*.host.wlan[0].mac.hcf.isBlockAckSupported = true +*.host.wlan[0].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +*.host.wlan[0].mac.hcf.originatorAckPolicy.ackTimeout = 200us +*.host.wlan[0].radio.receiver.errorModel.typename = "ResponseWindowErrorModel" +*.host.wlan[0].radio.separateReceptionParts = true +*.sender.radio.separateReceptionParts = true +*.sender.radio.opMode = "n(mixed-2.4Ghz)" +*.sender.radio.initialRadioMode = "transmitter" +*.sender.radio.antenna.mobilityModule = "^.^.mobility" +**.recoveryProcedure.shortRetryLimit = 1 +**.recoveryProcedure.longRetryLimit = 1 +**.radio.receiver.bandwidth = 20MHz +*.radioMedium.backgroundNoise.power = -110dBm +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +Aggregate response-window assertions passed: + +%contains: stdout +Run statistics: total 4, successful 4 diff --git a/tests/module/Ieee80211AmpduTxStart_1.test b/tests/module/Ieee80211AmpduTxStart_1.test new file mode 100644 index 00000000000..6033f51af65 --- /dev/null +++ b/tests/module/Ieee80211AmpduTxStart_1.test @@ -0,0 +1,226 @@ +%description: +Production Tx calls aggregate preparation at the exact submission time after IFS, or immediately for zero IFS. +Cancellation before IFS expiry prevents preparation and submission. Refusal clears the request before its cancellation callback. +Completion accepts a one-byte envelope without a MAC header or trailer and supplies a null header to the callback. +The completion callback can submit a replacement before the earlier completion returns. +A minimal MAC subclass captures submissions. The fixture calls the production radio-completion entry directly. +This module evidence does not establish production HCF cancellation or real radio completion. + +%file: AggregateTxStart.cc +#include +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Tx.h" +#include "inet/linklayer/ieee80211/mac/contract/IRx.h" + +using namespace inet; +using namespace inet::ieee80211; + +class AggregateTxNavProbe : public IRx +{ + public: + int updates = 0; + bool isReceptionInProgress() const override { return false; } + bool isMediumFree() const override { return true; } + void invalidateInterframeHistory() override {} + simtime_t getTxnavRemaining() const override { return SIMTIME_ZERO; } + void successfulFrameTransmitted(simtime_t, simtime_t) override {} + void setContentionBlocked(bool) override {} + void frameTransmitted(simtime_t) override { updates++; } + void registerContention(IContention *) override {} + void receptionStateChanged(physicallayer::IRadio::ReceptionState) override {} + void transmissionStateChanged(physicallayer::IRadio::TransmissionState) override {} + void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart) override {} + bool lowerFrameReceived(Packet *) override { throw cRuntimeError("Unexpected receive operation"); } + AmpduReception lowerAggregateReceived(Packet *) override { throw cRuntimeError("Unexpected aggregate receive operation"); } +}; + +class AggregateCaptureMac : public Ieee80211Mac +{ + protected: + int numInitStages() const override { return 1; } + void initialize(int) override {} + public: + std::vector submissions; + std::vector submittedIds; + void sendDownFrame(Packet *packet) override { + Enter_Method("sendDownFrame"); + take(packet); + ASSERT(packet->getDataLength() == B(1)); + ASSERT(packet->peekAllAsBytes()->getBytes() == std::vector{0x5a}); + submissions.push_back(simTime()); + submittedIds.push_back(packet->getId()); + delete packet; + } +}; +Define_Module(AggregateCaptureMac); + +class AggregateBoundaryTx : public Tx +{ + protected: + int numInitStages() const override { return 1; } + void initialize(int) override { + mac = check_and_cast(getModuleByPath("^.mac")); + rx = &navProbe; + endIfsTimer = new cMessage("endIFS"); + } + public: + AggregateTxNavProbe navProbe; +}; +Define_Module(AggregateBoundaryTx); + +class AggregateUnusedCallback : public ITx::IAggregateCallback +{ + public: + void frameTransmissionStarted(Packet *) override { throw cRuntimeError("Unexpected callback"); } + Packet *prepareAggregateTransmission() override { throw cRuntimeError("Unexpected callback"); } + void aggregateTransmissionCancelled() override { throw cRuntimeError("Unexpected callback"); } + void transmissionComplete(Packet *, const Ptr&) override { throw cRuntimeError("Unexpected callback"); } +}; + +class AggregateTxStartTest : public cSimpleModule, public ITx::IAggregateCallback +{ + AggregateBoundaryTx *tx = nullptr; + AggregateCaptureMac *capture = nullptr; + AggregateUnusedCallback unrelated; + std::unique_ptr source; + int prepared = 0; + int cancelled = 0; + int completed = 0; + int ordinaryStarted = 0; + bool refuse = false; + bool replaceAtCompletion = false; + simtime_t expectedSubmission; + std::vector preparationTimes; + + public: + AggregateTxStartTest() : cSimpleModule(65536) {} + Packet *prepareAggregateTransmission() override { + ASSERT(tx->hasTransmission()); + ASSERT(simTime() == expectedSubmission); + prepared++; + preparationTimes.push_back(simTime()); + ASSERT(source->getOwner() == this); + return refuse ? nullptr : source.get(); + } + void aggregateTransmissionCancelled() override { + ASSERT(!tx->hasTransmission()); + ASSERT(simTime() == expectedSubmission); + cancelled++; + } + void frameTransmissionStarted(Packet *) override { ordinaryStarted++; } + void transmissionComplete(Packet *packet, const Ptr& header) override { + ASSERT(!tx->hasTransmission()); + ASSERT(header == nullptr); + ASSERT(packet != source.get() && packet->getId() != source->getId()); + ASSERT(packet->getDataLength() == B(1)); + ASSERT(packet->peekAllAsBytes()->getBytes() == std::vector{0x5a}); + ASSERT(source->getOwner() == this); + completed++; + if (replaceAtCompletion) { + replaceAtCompletion = false; + expectedSubmission = simTime(); + tx->transmitAggregate(SIMTIME_ZERO, this); + ASSERT(tx->hasTransmission()); + } + } + protected: + void activity() override { + tx = check_and_cast(getModuleByPath("^.tx")); + capture = check_and_cast(getModuleByPath("^.mac")); + wait(SimTime(1, SIMTIME_MS)); + int passed = 0; + for (const std::string operation : {"delayed", "zero", "cancel", "refuse", "zeroRefuse", "replace"}) { + ASSERT(!tx->hasTransmission()); + prepared = cancelled = completed = ordinaryStarted = 0; + refuse = operation == "refuse" || operation == "zeroRefuse"; + replaceAtCompletion = operation == "replace"; + source = std::make_unique("one-byte-envelope", makeShared(std::vector{0x5a})); + auto originalId = source->getId(); + auto before = capture->submissions.size(); + auto delay = operation == "zero" || operation == "zeroRefuse" ? SIMTIME_ZERO : SimTime(16, SIMTIME_US); + expectedSubmission = simTime() + delay; + tx->transmitAggregate(delay, this); + ASSERT(!tx->cancelPendingAggregate(&unrelated)); + if (operation == "cancel") { + ASSERT(tx->hasTransmission() && prepared == 0); + wait(delay / 2); + ASSERT(tx->cancelPendingAggregate(this)); + ASSERT(!tx->hasTransmission()); + ASSERT(!tx->cancelPendingAggregate(this)); + wait(delay); + ASSERT(prepared == 0 && cancelled == 0 && completed == 0); + ASSERT(capture->submissions.size() == before); + } + else { + if (delay > SIMTIME_ZERO) { + ASSERT(tx->hasTransmission() && prepared == 0); + ASSERT(capture->submissions.size() == before); + wait(delay / 2); + ASSERT(prepared == 0 && tx->hasTransmission()); + wait(delay / 2 + SimTime(1, SIMTIME_NS)); + } + ASSERT(prepared == 1); + if (refuse) { + ASSERT(cancelled == 1 && completed == 0 && !tx->hasTransmission()); + ASSERT(capture->submissions.size() == before); + ASSERT(!tx->cancelPendingAggregate(this)); + } + else { + ASSERT(cancelled == 0 && completed == 0 && tx->hasTransmission()); + ASSERT(capture->submissions.size() == before + 1); + ASSERT(capture->submissions.back() == preparationTimes.back()); + ASSERT(capture->submittedIds.back() != originalId); + ASSERT(!tx->cancelPendingAggregate(this)); + tx->radioTransmissionFinished(); + if (operation == "replace") { + ASSERT(completed == 1 && prepared == 2 && tx->hasTransmission()); + ASSERT(capture->submissions.size() == before + 2); + tx->radioTransmissionFinished(); + ASSERT(completed == 2); + } + else + ASSERT(completed == 1); + ASSERT(!tx->hasTransmission()); + tx->radioTransmissionFinished(); + ASSERT(completed == (operation == "replace" ? 2 : 1)); + } + } + ASSERT(ordinaryStarted == 0 && tx->navProbe.updates == 0); + ASSERT(source->getId() == originalId && source->getOwner() == this); + ASSERT(source->getDataLength() == B(1)); + source.reset(); + passed++; + std::cout << "Aggregate Tx boundary passed: " << operation << "\n"; + } + ASSERT(passed == 6); + std::cout << "All six aggregate Tx boundary cases passed.\n"; + endSimulation(); + } +}; +Define_Module(AggregateTxStartTest); + +%file: test.ned +import inet.common.SimpleModule; +simple AggregateCaptureMac extends SimpleModule { parameters: @class(::AggregateCaptureMac); } +simple AggregateBoundaryTx extends SimpleModule { parameters: @class(::AggregateBoundaryTx); } +simple AggregateTxStartTest extends SimpleModule { parameters: @class(::AggregateTxStartTest); } +network Test { + submodules: + mac: AggregateCaptureMac; + tx: AggregateBoundaryTx; + test: AggregateTxStartTest; +} + +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false + +%contains: stdout +All six aggregate Tx boundary cases passed. diff --git a/tests/module/Ieee80211RxTransmissionCompletion_1.test b/tests/module/Ieee80211RxTransmissionCompletion_1.test index 39fe7f1aa4e..808d676bf2f 100644 --- a/tests/module/Ieee80211RxTransmissionCompletion_1.test +++ b/tests/module/Ieee80211RxTransmissionCompletion_1.test @@ -50,6 +50,7 @@ class CompletionDriver : public cSimpleModule, public cListener, public ITx::ICa int completions = 0; bool completed = false; void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("start")); } + void frameTransmissionStarted(Packet *) override {} void transmissionComplete(Packet *, const Ptr&) override { callbacks++; ASSERT(!tx->hasTransmission()); diff --git a/tests/protocol/wifi/11n/N_Ampdu.test b/tests/protocol/wifi/11n/N_Ampdu.test index dc5492f03e0..412399fd28b 100644 --- a/tests/protocol/wifi/11n/N_Ampdu.test +++ b/tests/protocol/wifi/11n/N_Ampdu.test @@ -1,16 +1,11 @@ -%# expected-result: FAIL %description: -Expected failure by explicit maintainer decision: defer this nontrivial repair to future work. -Repair requires production QoS aggregation selection, transmission, receive integration, and Block Ack progress. -Evidence and deferred-repair decision: doc/project/evidence/model/wifi/results.md#deferred-repair-expectations. -This declaration records unresolved work, not supported behavior. - -Observe an HT A-MPDU subframe. Aggregation construction exists, but the production -originator invocation is commented out and the default policy never aggregates. +Observe an HT A-MPDU through production association and fixed-rate Compressed Block Ack. +Require an implicit response and exactly sixty upper deliveries through the AP. %file: N_Ampdu.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { @@ -18,8 +13,18 @@ namespace protocoltest { Define_ProtocolTest(wifi_11n_ampdu) { return ProtocolTest("wifi_11n_ampdu") + .meanwhile(exactlyTimes(60, on("sta2.udp").signal("packetSentToUpper").within(1.5))) + .meanwhile(atLeastTimes(1, on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + return c.event.packet && dynamicPtrCast( + c.event.packet->peekAtFront()) != nullptr; + }) + .describe("an implicit Compressed Block Ack response").within(1.5))) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") - .filterPacket("Ieee80211MpduSubframeHeader.length >= 0") + .filterEvent([](const MatchContext& c) { + return c.event.packet && dynamicPtrCast( + c.event.packet->peekAtFront()) != nullptr; + }) .describe("a frame containing an A-MPDU subframe header").within(2.0)); } @@ -30,16 +35,32 @@ Define_ProtocolTest(wifi_11n_ampdu) [General] *.tester.testName = "wifi_11n_ampdu" -*.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" -*.ap.wlan[*].mac.hcf.isBlockAckSupported = true -*.sta1.app[0].sendInterval = 0.1ms -include ../../ini/_n.ini +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +**.wlan[*].mac.hcf.rateControl.typename = "" +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +**.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +**.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.wlan[*].mac.fcsMode = "computed" +**.checksumMode = "computed" +**.txopProcedure.txopLimit = 0s +*.sta1.app[0].messageLength = 1200B +*.sta1.app[0].sendInterval = 20us +*.sta1.app[0].stopTime = 1.00119s +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" **.wlan[*].radio.receiver.energyDetection = -105dBm +**.wlan[*].radio.receiver.sensitivity = -105dBm +**.wlan[*].radio.receiver.snirThreshold = 0dB **.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B -**.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 -**.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 100 +include ../../ini/_n.ini include ../../ini/_base.ini %contains: stdout PROTOCOLTEST wifi_11n_ampdu: PASS + +%contains: stdout +WifiInfraNetwork.sta2.app[0]: received 60 packets diff --git a/tests/protocol/wifi/11n/N_AmpduImplicitBa.test b/tests/protocol/wifi/11n/N_AmpduImplicitBa.test new file mode 100644 index 00000000000..e3d91ea22d2 --- /dev/null +++ b/tests/protocol/wifi/11n/N_AmpduImplicitBa.test @@ -0,0 +1,154 @@ +%description: +Real association and application traffic use HT aggregates on both AP hops. +Computed member FCS and a matched implicit response permit exactly sixty upper deliveries. +A deterministic partial-loss case requires an aggregate member with Retry set. +Both cases use seed zero. This fixture does not claim performance validation. + +%extraargs: -r 0..1 + +%postrun-command: python3 check-implicit.py + +%file: check-implicit.py +from pathlib import Path + +output = Path("test.out").read_text() +for name in ("wifi_11n_implicit_ba", "wifi_11n_implicit_ba_partial"): + assert output.count(f"PROTOCOLTEST {name}: PASS") == 1 +assert output.count("WifiInfraNetwork.sta2.app[0]: received 60 packets") == 2 +assert " Warning:" not in output +assert " Error:" not in output +assert ": FAIL" not in output + +%file: N_AmpduImplicitBa.cc +#include +#include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" + +namespace inet::protocoltest { +using namespace ieee80211; +using namespace physicallayer; + +class ImplicitProtocolErrorModel : public Ieee80211NistErrorModel +{ + mutable bool firstMember = true; + public: + double computePacketErrorRate(const ISnir *, IRadioSignal::SignalPart) const override { return 0; } + double computeChunkSuccessRate(const ISnir *, b bitLength) const override { + B length(bitLength); + if (length == B(4)) + return 1; + bool fail = par("partialLoss").boolValue() && firstMember; + firstMember = false; + return fail ? 0 : 1; + } +}; +Define_Module(ImplicitProtocolErrorModel); + +static bool hasAggregate(const MatchContext& context, bool requireRetry) +{ + auto packet = context.event.packet; + if (!packet || !dynamicPtrCast(packet->peekAtFront())) + return false; + auto mode = packet->getTag()->getMode(); + if (mode->getHtMcsIndex() < 0 || mode->isHtShortGuardInterval()) + return false; + bool retry = false; + int members = 0; + bool complete = MpduDeaggregation::visitSubframes(packet, B(0), B(packet->getDataLength()), + [](const auto&, B, bool valid) { ASSERT(valid); }, + [&](const auto& mpdu, B) { + Packet member("observed-member", mpdu); + auto data = member.peekAtFront(); + ASSERT(data->getAckPolicy() == NORMAL_ACK && data->getFragmentNumber() == 0 && !data->getMoreFragments()); + retry |= data->getRetry(); + members++; + }); + return complete && members > 0 && (!requireRetry || retry); +} + +static ProtocolTest implicitProgram(const char *name, bool partialLoss) +{ + auto program = ProtocolTest(name) + .meanwhile(exactlyTimes(60, on("sta2.udp").signal("packetSentToUpper").within(1.5))); + if (partialLoss) + program.meanwhile(atLeastTimes(1, on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { return hasAggregate(c, true); }) + .describe("selective DATA retry after partial receipt").within(1.5))); + program.once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto header = c.event.packet->peekAtFront(); + auto response = dynamicPtrCast(header); + return response && response->getStatusCode() == SC_SUCCESSFUL && response->getBlockAckPolicy(); + }).describe("accepted immediate agreement after infrastructure association").within(1.2)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { return hasAggregate(c, false); }) + .describe("production HT aggregate from the source").within(0.1)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = dynamicPtrCast(c.event.packet->peekAtFront()); + return response && response->getBlockAckBitmap().getSize() == 64; + }).describe("implicit Compressed Block Ack").within(0.02)) + .once(on("ap.wlan[0].mac").signal("packetSentToLower") + .filterEvent([](const MatchContext& c) { return hasAggregate(c, false); }) + .describe("production HT aggregate from the AP").within(0.1)); + return program; +} + +Define_ProtocolTest(wifi_11n_implicit_ba) +{ + return implicitProgram("wifi_11n_implicit_ba", false); +} + +Define_ProtocolTest(wifi_11n_implicit_ba_partial) +{ + return implicitProgram("wifi_11n_implicit_ba_partial", true); +} +} + +%file: ImplicitProtocolErrorModel.ned +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +module ImplicitProtocolErrorModel extends Ieee80211NistErrorModel { + parameters: + bool partialLoss = default(false); + @class(::inet::protocoltest::ImplicitProtocolErrorModel); +} + +%inifile: test.ini +[General] +*.tester.testName = ${test="wifi_11n_implicit_ba","wifi_11n_implicit_ba_partial"} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.rateControl.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.wlan[*].mac.fcsMode = "computed" +**.checksumMode = "computed" +**.wlan[*].radio.receiver.errorModel.typename = "ImplicitProtocolErrorModel" +**.wlan[*].radio.receiver.errorModel.partialLoss = ${test} == "wifi_11n_implicit_ba_partial" +**.txopProcedure.txopLimit = 0s +*.sta1.app[0].messageLength = 1200B +*.sta1.app[0].sendInterval = 20us +*.sta1.app[0].stopTime = 1.00119s +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +**.wlan[*].radio.receiver.energyDetection = -105dBm +**.wlan[*].radio.receiver.sensitivity = -105dBm +**.wlan[*].radio.receiver.snirThreshold = 0dB +include ../../ini/_n.ini +include ../../ini/_base.ini + +%contains: stdout +PROTOCOLTEST wifi_11n_implicit_ba: PASS + +%contains: stdout +PROTOCOLTEST wifi_11n_implicit_ba_partial: PASS diff --git a/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test b/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test index 1b68c527b62..2f5e9b03780 100644 --- a/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test +++ b/tests/protocol/wifi/common/WifiBlockAckBarWithoutAgreement.test @@ -61,6 +61,7 @@ class LifecycleBarStimulus : public cSimpleModule, public ITx::ICallback check_and_cast(network->getSubmodule("sta1")->getSubmodule("wlan", 0)->getSubmodule("mac"))->testBarOutstanding = true; sender->sendTestBar(peer, tid, this); } + void frameTransmissionStarted(Packet *) override {} void transmissionComplete(Packet *, const Ptr&) override {} }; Define_Module(LifecycleBarStimulus); diff --git a/tests/protocol/wifi/common/WifiLocalInterframeInputs.test b/tests/protocol/wifi/common/WifiLocalInterframeInputs.test index 8f40bead894..b301769f205 100644 --- a/tests/protocol/wifi/common/WifiLocalInterframeInputs.test +++ b/tests/protocol/wifi/common/WifiLocalInterframeInputs.test @@ -139,6 +139,7 @@ class LocalInputDriver : public cSimpleModule, public cListener, public ITx::ICa bool completed = false; int ackCompletions = 0; simtime_t ackEnd; + void frameTransmissionStarted(Packet *) override {} void transmissionComplete(Packet *, const Ptr&) override { ackCompletions++; ackEnd = simTime(); diff --git a/tests/unit/Ieee80211AmpduResponseValidation_1.test b/tests/unit/Ieee80211AmpduResponseValidation_1.test new file mode 100644 index 00000000000..f7888b5d34a --- /dev/null +++ b/tests/unit/Ieee80211AmpduResponseValidation_1.test @@ -0,0 +1,45 @@ +%description: +The shared Compressed response validator accepts a zero bitmap at sequence wrap. +It rejects mismatched addresses, TID, profile, fragment fields, bitmap width, and wire length. +The aggregate frame sequence supplies its actual peer and TID to this validator. +These helper checks supplement the real receive-window fixture. + +%includes: +#include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +using namespace inet; +using namespace inet::ieee80211; + +%activity: +const MacAddress local("02:00:00:00:00:01"); +const MacAddress peer("02:00:00:00:00:02"); +const MacAddress other("02:00:00:00:00:03"); +for (int scenario = 0; scenario < 12; scenario++) { + auto header = makeShared(); + header->setReceiverAddress(scenario == 1 ? other : local); + header->setTransmitterAddress(scenario == 2 ? other : peer); + header->setTidInfo(scenario == 3 ? 2 : 5); + header->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + header->setBlockAckBitmap(BitVector(0, scenario == 7 ? 63 : scenario == 8 ? 65 : 64)); + if (scenario == 4) header->setMultiTid(true); + if (scenario == 5) header->setCompressedBitmap(false); + if (scenario == 6) header->setFragmentNumber(1); + if (scenario == 9) header->setChunkLength(B(27)); + Packet packet("response", header); + if (scenario != 10) + packet.insertAtBack(makeShared()); + if (scenario == 11) + packet.insertAtBack(makeShared(B(1))); + ASSERT(isValidCompressedBlockAck(&packet, local, peer, 5) == (scenario == 0)); +} +auto basic = makeShared(); +basic->setReceiverAddress(local); +basic->setTransmitterAddress(peer); +basic->setTidInfo(5); +Packet wrongProfile("basic", basic); +wrongProfile.insertAtBack(makeShared()); +ASSERT(!isValidCompressedBlockAck(&wrongProfile, local, peer, 5)); +EV << "Compressed response validation boundaries passed.\n"; + +%contains: stdout +Compressed response validation boundaries passed. diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test index afeb6c4f616..9f13e313dab 100644 --- a/tests/unit/Ieee80211BlockAckScheduling_1.test +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -189,6 +189,47 @@ ASSERT(policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); wait(duration); ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); ASSERT(ack.getQoSDataAckStatus(unrelated.front()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +// A pending probe survives complete member expiry. The caller's EDCA predicate limits its owner. +agreements.getAgreement(peer, 2)->setPendingProbe(true); +selectedAgreement->setPendingProbe(true); +otherAgreement->setPendingProbe(true); +policy.setProbeEligibility([](InProgressFrames *, Tid) { return false; }); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +policy.setProbeEligibility([&frames](InProgressFrames *candidate, Tid tid) { return candidate == &frames && tid == 5; }); +choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 5); +ASSERT(std::get<1>(choice) == selectedAgreement->getStartingSequenceNumber()); +selectedAgreement->setPendingProbe(false); +choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == other && std::get<2>(choice) == 5); +otherAgreement->setPendingProbe(false); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +// A probe wait includes failed transmitted members, but excludes a numbered unsent member. +QosAckHandler implicitAck; +SchedulingFrames implicitFrames; +implicitFrames.configure(&implicitAck); +OriginatorBlockAckAgreement implicitAgreement(peer, 5, SequenceNumberCyclic(4095), 2, false, false); +auto attempted = implicitFrames.add(peer, 5, 4095, false); +auto numberedUnsent = implicitFrames.add(peer, 5, 0, false); +implicitAck.processTransmittedImplicitBlockAckMember(attempted); +implicitAck.processFailedImplicitBlockAckMember(attempted); +ASSERT(implicitAck.getQoSDataAckStatus(attempted) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); +auto probe = makeShared(); +probe->setReceiverAddress(peer); +probe->setTidInfo(5); +probe->setStartingSequenceNumber(SequenceNumberCyclic(4095)); +implicitAck.processTransmittedBlockAckReq(probe, &implicitAgreement); +ASSERT(implicitAck.getQoSDataAckStatus(attempted) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +ASSERT(implicitAck.getQoSDataAckStatus(numberedUnsent) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); +auto zeroResponse = makeShared(); +zeroResponse->setTransmitterAddress(peer); +zeroResponse->setTidInfo(5); +zeroResponse->setStartingSequenceNumber(SequenceNumberCyclic(4095)); +zeroResponse->setBlockAckBitmap(BitVector(0, 64)); +ASSERT(implicitAck.processReceivedCompressedBlockAck(zeroResponse, &implicitAgreement).empty()); +ASSERT(implicitAck.getQoSDataAckStatus(attempted) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED); +ASSERT(implicitAck.isEligibleToTransmit(attempted)); +ASSERT(implicitAck.getQoSDataAckStatus(numberedUnsent) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); std::cout << "Block Ack scheduling isolation assertions passed.\n"; %contains: stdout diff --git a/tests/unit/Ieee80211TxNavChoice_1.test b/tests/unit/Ieee80211TxNavChoice_1.test index 50942c6885a..226acdcc8fa 100644 --- a/tests/unit/Ieee80211TxNavChoice_1.test +++ b/tests/unit/Ieee80211TxNavChoice_1.test @@ -64,6 +64,7 @@ class CompletionCallback : public ITx::ICallback { CompletionTx *tx; bool replacementChoice; CompletionCallback(CompletionTx *tx, bool choice) : tx(tx), replacementChoice(choice) {} + void frameTransmissionStarted(Packet *) override {} void transmissionComplete(Packet *, const Ptr&) override { ASSERT(!tx->hasTransmission()); tx->replaceChoice(replacementChoice); From 94fd3d21759884471808fad23cddb7cb7e0c728a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 18 Oct 2026 02:29:59 +0200 Subject: [PATCH 22/24] examples: add: demonstrate HT implicit Block Ack through an AP Before this commit, users lack a complete configuration for the new HT aggregate profile. Selecting an HT mode alone does not establish peer capabilities or an accepted agreement. After this commit, one example uses production management and fixed HT20 long-GI DATA rates. Scalar, dimensional, and zero-TXOP configurations send UDP traffic through an AP. The README explains opt-in, selective retry, probe recovery, and profile limits. These demonstrations establish no throughput result. Plan: plan/pending/ieee80211-ampdu-implicit-blockack.md Change: examples | behavior.add | - | ieee80211-implicit-blockack --- examples/wireless/htampdu/HtAmpdu.ned | 18 ++++++++ examples/wireless/htampdu/README | 13 ++++++ examples/wireless/htampdu/omnetpp.ini | 61 +++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 examples/wireless/htampdu/HtAmpdu.ned create mode 100644 examples/wireless/htampdu/README create mode 100644 examples/wireless/htampdu/omnetpp.ini diff --git a/examples/wireless/htampdu/HtAmpdu.ned b/examples/wireless/htampdu/HtAmpdu.ned new file mode 100644 index 00000000000..25678277f6f --- /dev/null +++ b/examples/wireless/htampdu/HtAmpdu.ned @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +package inet.examples.wireless.htampdu; + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.common.contract.packetlevel.IRadioMedium; + +network HtAmpdu +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: like IRadioMedium; + ap: AccessPoint; + source: WirelessHost; + destination: WirelessHost; +} diff --git a/examples/wireless/htampdu/README b/examples/wireless/htampdu/README new file mode 100644 index 00000000000..8fb89a50b1d --- /dev/null +++ b/examples/wireless/htampdu/README @@ -0,0 +1,13 @@ +This example selects HT A-MPDU transmission with implicit Compressed Block Ack. + +The source sends UDP traffic through an AP to the destination. Production management establishes peer HT capabilities and Block Ack agreements. HCF selects independent MPDUs from the queue after agreement acceptance. Each aggregate receives one Compressed Block Ack response. The originator retains unacknowledged members for selective retry with their original deadlines. + +Run the default configuration from this directory: + + inet -u Cmdenv -c General + +The ZeroTxop configuration permits one aggregate DATA PPDU per channel grant. The Dimensional configuration selects the dimensional PHY. The default configuration uses the scalar PHY. These runs demonstrate the configured mechanism; they do not supply throughput validation. + +HtMpduAggregationPolicy requires fixed DATA rates without a rate-control submodule. This example selects HT mixed format, 20 MHz, one spatial stream, and long GI. The receiver also accepts advertised HT20 short-GI aggregates. A-MSDU nesting, adaptive aggregate rates, HT40 aggregate transmission, VHT aggregates, Multi-TID Block Ack, and correlated FEC errors remain outside this profile. + +NIST and Yans use cached delimiter and MPDU outcomes. A failed member does not invalidate every other member. The MAC still validates delimiters, FCS, addresses, and agreement state before receipt. After an absent or invalid implicit response, the originator submits at most one explicit BAR probe before its next DATA attempt. This probe is an INET recovery policy. diff --git a/examples/wireless/htampdu/omnetpp.ini b/examples/wireless/htampdu/omnetpp.ini new file mode 100644 index 00000000000..28f5f459cb4 --- /dev/null +++ b/examples/wireless/htampdu/omnetpp.ini @@ -0,0 +1,61 @@ +[General] +network = HtAmpdu +sim-time-limit = 1.5s +seed-set = 0 + +*.source.numApps = 1 +*.source.app[0].typename = "UdpBasicApp" +*.source.app[0].destAddresses = "destination" +*.source.app[0].destPort = 1000 +*.source.app[0].messageLength = 1200B +*.source.app[0].startTime = 1s +*.source.app[0].stopTime = 1.03s +*.source.app[0].sendInterval = 30us +*.destination.numApps = 1 +*.destination.app[0].typename = "UdpSink" +*.destination.app[0].localPort = 1000 +*.source.ipv4.arp.typename = "GlobalArp" +*.destination.ipv4.arp.typename = "GlobalArp" + +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].classifier.typename = "QosClassifier" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.fcsMode = "computed" +**.checksumMode = "computed" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +**.wlan[*].mac.hcf.rateControl.typename = "" +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +**.wlan[*].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +**.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.txopProcedure.txopLimit = 4ms +**.txopProcedure.protectionMechanism = "multiple" +**.rtsPolicy.rtsThreshold = 2346B +**.bandName = "2.4 GHz" + +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.source.mobility.initialX = 0m +*.ap.mobility.initialX = 5m +*.destination.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +[Config ZeroTxop] +description = "One aggregate DATA PPDU per channel grant" +**.txopProcedure.txopLimit = 0s + +[Config Dimensional] +description = "HT aggregates through the dimensional PHY" +*.radioMedium.typename = "Ieee80211DimensionalRadioMedium" +**.wlan[*].radio.typename = "Ieee80211DimensionalRadio" From a81ffed601355b7023486ee66f87d4240d28781b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 14:06:12 +0100 Subject: [PATCH 23/24] doc: comment: explain HT aggregate transmission and migration Before this commit, the documentation covers preparation and reception but still describes local aggregate transmission as unavailable. Sender activation adds configuration and Tx contracts that these documents do not explain. After this commit, the user guide explains explicit policy selection, independent member state, complete exchange admission, selective retry, and probe recovery. The migration guide describes sender callbacks, cancellation, acknowledgment policy, and member outcome status. The release note states the supported profile and limits. The Wi-Fi evidence report retains the approved HT marker removal and its historical evidence. This commit restores the complete original documentation after the first two PRs introduce their applicable sections. It changes no runtime behavior. Change: doc | comment | - | ieee80211-implicit-blockack --- doc/project/evidence/model/wifi/results.md | 8 ++++- .../ieee80211-implicit-blockack.rst | 8 +++-- doc/src/users-guide/ch-80211.rst | 32 +++++++++++++------ .../ieee80211-implicit-blockack.txt | 10 +++--- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/doc/project/evidence/model/wifi/results.md b/doc/project/evidence/model/wifi/results.md index 59555af44de..91f58ed65d2 100644 --- a/doc/project/evidence/model/wifi/results.md +++ b/doc/project/evidence/model/wifi/results.md @@ -1,9 +1,15 @@ # Wi-Fi protocol tests — expectation audit -> **Kind:** report · **Status:** snapshot 2026-09-17 · **Seal:** none · **Owns:** — · **Stands on:** [failure classification](../../../guide/derive-tests-from-a-standard.md#the-class-of-a-failure-and-when-to-declare-it-expected), [testing rules](../../../rule/testing.md) +> **Kind:** report · **Status:** snapshot 2026-10-18 with September history · **Seal:** none · **Owns:** — · **Stands on:** [failure classification](../../../guide/derive-tests-from-a-standard.md#the-class-of-a-failure-and-when-to-declare-it-expected), [testing rules](../../../rule/testing.md) Raw artifacts linked under `audit/` are local, Git-ignored evidence and are not included in the branch. The observations, classifications, and follow-up work are recorded below. +## HT aggregate update, 2026-10-18 + +`N_Ampdu` now uses the supported HT20 aggregate profile with fixed rates and Compressed Block Ack. Its production exchange observes an aggregate and an implicit response. It requires exactly 60 UDP deliveries and independently checks 60 application receipts. The maintainer explicitly approves removal of this test's expected-failure marker after the focused pass. The [Implicit Block Ack execution record](../../../../../plan/pending/ieee80211-ampdu-implicit-blockack-execution.md) supplies the source identity, commands, and final evidence. + +`Ac_Ampdu` retains its expected failure because VHT aggregates remain outside this profile. HT transmission from an `ac` interface does not establish VHT aggregate support. All other deferred declarations retain their existing scope. The sections below retain the September snapshot and its historical counts. + ## Deferred-repair expectations After the audit and rebase, the maintainer explicitly requested that the 12 failures from the 18-test focused run be declared expected: their repairs are nontrivial and deferred to future contributions. This later decision supersedes the removal decision for those 12 tests only. The six repaired passing tests retain their PASS expectations. The maintainer subsequently extended the same decision to the separately added `Ac_Ldpc` test. The total deferred-repair scope is now 13 tests. diff --git a/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst b/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst index 9fee5959629..c49e02e7520 100644 --- a/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst +++ b/doc/src/migration-guide/development/ieee80211-implicit-blockack.rst @@ -1,15 +1,17 @@ IEEE 802.11 implicit Block Ack extension points ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The default MPDU policy leaves aggregate transmission disabled. ``HtMpduAggregationPolicy`` supplies selection for independent HT20 long-GI members with fixed DATA rates. Local aggregate transmission remains unavailable. Custom MPDU policies implement ``isEnabled()`` and ``selectFrames()``. The caller supplies receiver limits and one retained DATA mode. A policy returns borrowed member references without queue extraction or state changes. +The default MPDU policy remains disabled. ``HtMpduAggregationPolicy`` enables independent HT20 long-GI members with fixed DATA rates. Custom MPDU policies implement ``isEnabled()`` and ``selectFrames()``. The caller supplies negotiated receiver limits and one retained DATA mode. A policy returns borrowed member references without queue extraction or state changes. Custom QoS data services implement batch preparation, validation, commitment, and construction through ``IOriginatorQosMacDataService``. Each new member owns one reversible preview. The batch borrows canonical members from the in-progress queue. Commitment validates every queued source before the first extraction. Failed preparation preserves sequence state and independent deadlines. ``IRx::lowerAggregateReceived()`` returns separately owned correct members and immutable delimiter history. The caller retains the envelope. Custom recipient services pass ``BlockAckReceiveContext`` to the reorder owner. This context supplies physical interval and delimiter facts; it does not replace receipt state. Custom agreement handlers implement ``implicitBlockAckRequested()`` to refresh inactivity for valid implicit DATA. -Every custom rate selector implements ``computeCompressedBlockAckResponseMode()`` for implicit callers. A selector without this capability must throw a diagnostic from its concrete override. Explicit Compressed BAR and implicit DATA share this rate calculation without a fabricated BAR. +Custom Tx implementations support ``transmitAggregate()``, ``cancelPendingAggregate()``, and ``finalizeMpdu()``. The aggregate callback validates and commits members at actual submission after the existing IFS timer. Cancellation removes a delayed request before exchange-context disposal. Aggregate completion supplies a null MAC header because the envelope has no outer MAC header or FCS. Every Tx callback implements ``frameTransmissionStarted()`` for an actual-submission notification. A concrete callback can use an empty override when submission requires no state change. -Custom recipient procedures construct a Compressed response directly from peer, TID, and current agreement state. Local aggregate transmission remains unavailable. Custom Tx implementations need no aggregate submission operation at this boundary. +Custom QoS ACK policies accept probe eligibility through existing EDCA classification. They also supply the implicit Block Ack receive-start timeout from the actual response mode. Every custom rate selector implements ``computeCompressedBlockAckResponseMode()`` for implicit callers. A selector without this capability must throw a diagnostic from its concrete override. Explicit Compressed BAR and implicit DATA share this rate calculation without a fabricated BAR. + +Custom recipient procedures construct a Compressed response directly from peer, TID, and current agreement state. ``FrameTransmissionDetails`` adds ``FRAME_TRANSMISSION_STATUS_UNACKNOWLEDGED`` for unsuccessful aggregate member attempts. A probe supplies no second DATA attempt outcome. Existing packet-drop signals retain terminal disposal observations. IEEE 802.11 HT aggregate construction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index 04302eebf24..4f5d8d8aa28 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -609,7 +609,7 @@ minimum. Outside active Compressed agreements, multiple protection does not check complete exchange cost against the remaining budget. For example, that retained policy can start a 120 us exchange with only 100 us left. Active Compressed agreements use their separate complete-cost admission. The reference scope assumes whole-microsecond positive limits, integral initial PPDU airtime, and integral legacy RTS airtime. -Multiple protection adds no A-MPDU, new fragmentation, CTS-to-self, dual CTS, PIFS recovery, or TXOP sharing. Reverse direction, beamforming, S1G, multi-user operation, and general MAC lifecycle support remain outside this implementation. General rate selection and recipient response timing retain their separate limits. +Multiple protection supports the HT aggregate profile below. New fragmentation, CTS-to-self, dual CTS, PIFS recovery, and TXOP sharing remain outside this implementation. Reverse direction, beamforming, S1G, multi-user operation, and general MAC lifecycle support also remain outside this implementation. General rate selection and recipient response timing retain their separate limits. .. _ug:sec:80211:fragmentation: @@ -642,7 +642,7 @@ default policy is :ned:`BasicMsduAggregationPolicy` with these parameters: - :par:`aggregationLengthThreshold`: the smallest aggregated payload that makes an A-MSDU. The default, ``-1``, means no check. -Set ``msduAggregationPolicy.typename = ""`` to turn A-MSDU aggregation off. A-MSDU aggregation needs packets in the queue. The default MPDU policy leaves A-MPDU transmission disabled. The MAC can receive HT aggregates under an eligible Compressed Block Ack agreement. Local aggregate transmission remains unavailable. +Set ``msduAggregationPolicy.typename = ""`` to turn A-MSDU aggregation off. A-MSDU aggregation needs packets in the queue. The default MPDU policy also leaves A-MPDU transmission disabled. The HT policy below enables A-MPDU transmission with implicit Compressed Block Ack. .. code-block:: ini @@ -651,10 +651,10 @@ Set ``msduAggregationPolicy.typename = ""`` to turn A-MSDU aggregation off. A-MS .. _ug:sec:80211:htampdu: -HT aggregate reception and implicit Block Ack -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +HT A-MPDU and implicit Block Ack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Configure an HT QoS station with Compressed Block Ack support. An accepted immediate Compressed Block Ack agreement must exist for the peer and TID before the recipient accepts aggregate DATA. +Select ``HtMpduAggregationPolicy`` to combine independent MPDUs after an accepted Compressed Block Ack agreement. The first transmit profile uses HT mixed format, 20 MHz, and long GI. Configure a fixed DATA rate without a rate-control submodule. HCF rejects aggregate opt-in with rate control at initialization. The receiver also accepts advertised HT20 short-GI aggregates. .. code-block:: ini @@ -662,12 +662,26 @@ Configure an HT QoS station with Compressed Block Ack support. An accepted immed **.wlan[*].mac.qosStation = true **.wlan[*].mac.hcf.isBlockAckSupported = true **.wlan[*].mac.hcf.blockAckVariant = "compressed" + **.wlan[*].mac.hcf.rateControl.typename = "" + **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps + **.wlan[*].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz + **.wlan[*].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 + **.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps + **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" -The receiver checks each member against its cached PHY outcome and FCS. One incorrect member does not invalidate a correct neighbor. The recipient records accepted members and sends one Compressed Block Ack after SIFS. Delimiter history and physical interval facts control reorder release. +Production infrastructure management supplies peer capabilities and receiver limits. An HT mode alone does not supply this state. Each aggregate contains unfragmented QoS DATA for one receiver and TID. HCF applies the peer's byte limit, minimum MPDU start spacing, transmit window, and both HT duration limits. A-MSDU members remain outside this aggregate profile. -The receiver accepts HT20 long-GI aggregates and advertised HT20 short-GI aggregates. Both peers must select compatible Block Ack profiles because ADDBA does not negotiate the profile selector. An active Basic agreement causes a diagnostic for implicit Compressed DATA. DATA without an eligible agreement creates no receipt state or response. +HCF selects transmitted retries first, followed by numbered unsent members and new queued members. New members retain separate sequence numbers and deadlines. HCF commits queued members at actual DATA submission after the IFS interval. Expiry or agreement loss before submission cancels the batch without a DATA attempt. RTS protection uses the complete PSDU length, including delimiters and spacing. -These changes add recipient support. They do not enable local aggregate transmission. +Every member carries ACK policy bits ``00`` and the same Duration value. Aggregate context makes those bits an implicit Block Ack request. The recipient validates each member against its copied PHY outcome and FCS. A valid member can arrive despite another member's failure. One correct member prevents EIFS, including an overheard member or DATA without an agreement. + +The recipient sends one Compressed Block Ack after SIFS for accepted aggregate DATA. Positive bits release only transmitted members. Missing bits retain the affected members for selective retry. A valid zero-bit response completes the response exchange without a probe. An absent or invalid response arms one explicit BAR probe before further DATA for that agreement. + +The probe is an INET recovery policy. It can expose lost recipient agreement state while fresh DATA remains queued. Probe submission consumes the pending flag. Probe failure returns to DATA retry without another probe until a later DATA attempt. Each probe adds airtime and channel access delay. + +A zero TXOP limit permits one aggregate DATA PPDU with its protection and response. Multiple-member aggregates must fit a positive limit. A singleton retry can use the applicable single-MPDU exception. The example at ``examples/wireless/htampdu`` provides scalar, dimensional, and zero-limit configurations. + +NIST and Yans retain cached PHY outcomes in chunk flags under scalar SNIR reduction. This abstraction does not model correlated FEC errors or interference changes within a chunk. Adaptive aggregate rates, HT40 transmit aggregation, VHT aggregates, and Multi-TID Block Ack remain outside this profile. These capabilities do not establish complete amendment conformance or throughput validation. .. _ug:sec:80211:ack: @@ -1065,7 +1079,7 @@ The model does not contain: - PCF and HCCA, the polled channel access methods. The module :ned:`Hcca` exists as a placeholder. -- A-MPDU aggregation and self-CTS protection. +- HT40 transmit aggregation, VHT aggregates, and self-CTS protection. - Multi-TID block acknowledgement and delayed Compressed Block Ack. diff --git a/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt b/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt index c2f35a27674..464e227e1a5 100644 --- a/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt +++ b/doc/whatsnew/incompatible/ieee80211-implicit-blockack.txt @@ -1,9 +1,9 @@ -IEEE 802.11 HT aggregate preparation and reception +IEEE 802.11 HT A-MPDU and implicit Compressed Block Ack -HT capability exchange now retains the peer receiver's minimum MPDU start spacing. The MIB parameter htMinimumMpduStartSpacing selects the encoded local receiver limit from zero through seven. Zero preserves the default with no spacing restriction. +HtMpduAggregationPolicy enables HT20 mixed-format A-MPDU transmission after an accepted Compressed agreement. The default policy leaves aggregation disabled. The first transmit profile requires long GI and fixed DATA rates without rate control. The receiver also accepts advertised HT20 short-GI aggregates. The htampdu example supplies scalar, dimensional, and zero-TXOP configurations. -The HT aggregate builder checks receiver byte limits, spacing, PPDU duration, and L-SIG length. It retains each source Packet. The QoS service can prepare independent members before it extracts queued sources. Each member retains its sequence number and deadline. Failed preparation preserves queued sources. +Each member retains its sequence number, retry state, and independent deadline. HCF commits new members after the IFS interval at actual DATA submission. The complete PSDU length controls RTS protection and exchange admission. Partial PHY loss permits separate member acceptance and one implicit response after SIFS. One correct member prevents EIFS, including overheard DATA and DATA without an agreement. -Rx now checks each aggregate member against its PHY outcome and FCS. Correct members can enter receipt and reorder state under an eligible Compressed agreement. The recipient sends one implicit Compressed Block Ack after SIFS. One correct member prevents EIFS for that PPDU. Delimiter history and physical interval facts control reorder release without a second receipt record. +Missing or invalid implicit responses cause one contention failure and one pending explicit BAR probe. Probe submission consumes that flag. Probe failure permits DATA retry without another probe until a later DATA attempt. This INET policy exposes lost recipient agreement state under continuous traffic. It adds probe airtime and channel access delay. Valid zero-bit responses do not arm a probe. -Custom aggregation policies, aggregate builders, QoS data services, Rx, recipient services, rate selectors, and response procedures require the migration changes. The default MPDU policy leaves aggregation disabled. Local aggregate transmission remains unavailable at this boundary. Implicit DATA without an eligible agreement creates no receipt state or response. +Custom aggregation policies, QoS data services, Rx, Tx, recipient services, ACK policies, and response procedures require the migration changes. NIST and Yans retain cached PHY outcomes in chunk flags. Adaptive aggregate rates, HT40 transmit aggregation, VHT aggregates, nested A-MSDUs, and correlated FEC errors remain outside this profile. These changes can alter affected exchange order, delivery times, and simulation results. From 5c0505f3ef979873ab574c89373e1f66e60350d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 5 Nov 2026 14:06:12 +0100 Subject: [PATCH 24/24] plan: comment: record implicit Block Ack completion and evidence Before this commit, the plan lacks the completed MAC execution record. Readers cannot locate the earlier acceptance evidence or its limits from the plan. After this commit, the record maps implementation stages to their fixtures and historical results. It retains failed attempts, source identities, commands, baseline approvals, and coverage limits. The prerequisite check of the MAC contracts remains open, so the plan stays under pending. This commit adds historical evidence and plan status. It changes no runtime behavior. The new PR evidence supplies fresh verification for this reconstructed series. Change: plan | comment | - | ieee80211-implicit-blockack --- ...80211-ampdu-implicit-blockack-execution.md | 307 ++++++++++++++++++ ...ee80211-ampdu-implicit-blockack-summary.md | 12 +- .../ieee80211-ampdu-implicit-blockack.md | 14 +- 3 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 plan/pending/ieee80211-ampdu-implicit-blockack-execution.md diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack-execution.md b/plan/pending/ieee80211-ampdu-implicit-blockack-execution.md new file mode 100644 index 00000000000..740deb09c25 --- /dev/null +++ b/plan/pending/ieee80211-ampdu-implicit-blockack-execution.md @@ -0,0 +1,307 @@ +# Implicit Block Ack execution record + +Execution date: 2026-10-18. A1 through A7 are complete on the approved base, within the profile and verification limits below. The implementation evidence describes the source tree before commit. The user subsequently authorizes commit, push, and PR creation. A0 remains open because prerequisite merge confirmation is absent. + +## A2–A7 authorization and scope + +The user's request `Execute stages A2-A7` authorizes the remaining stages on this worktree after A1. This request extends implementation beyond the earlier A1-only exception. The base remains `56b75c959af104b5db931bef022bdfe5578eead9`, with the recorded A1 changes. The prerequisite merge status remains unverified. This execution preserves the separate `AGENTS.md` change. Commits, publication, and baseline changes require their own authority. + +The root agent owns production source changes. Separate read-only investigations trace transmit contracts and mandatory receive release. A test specialist owns the new A2 boundary fixture. The existing plan defines each stage's behavior and acceptance conditions. Stage completion requires the corresponding direct evidence. + +### A2 implementation contract + +Rx must reject a delivered PHY failure container before a standalone MAC header or trailer peek. The current guard checks only the decoded Aggregation property. A failed common header supplies no accepted Aggregation property. Rx will also check common header success and complete content from the typed PHY indication. This guard requires no range draw, shared chunk change, or new event. + +The existing real-radio fixture supplies copied long-GI and short-GI outcomes, all four decision parts, actual failure paths, and receive metadata. The new boundary fixture will prove refusal before the FCS predicate, including short malformed containers. Ordinary standalone reception retains its current path. A4 will add the separate aggregate entry and member validation. Source inspection and the seal registry validate this bounded contract before the edit. + +### A3 implementation contract + +The data service will prepare independent queued previews beside borrowed canonical members. The new HT policy will select one peer, transmitter, and TID within supplied window, byte, mode, and spacing limits. The existing Basic policy will retain its disabled behavior. HCF will reject aggregate opt-in with a rate-control submodule before DATA attempts. The new policy requires HT mixed format, 20 MHz, and long GI. + +Preparation will assign consecutive provisional numbers without a counter update. Batch validation will check every queued source before the first extraction. Commitment will reuse the existing extraction, region-tag, lifetime, and allocation owners for each new member. Canonical members retain their numbers and deadlines. The in-progress queue will expose eligible canonical members without extraction. HCF will supply retries before numbered unsent members, followed by queued candidates. + +Batch refusal will retain sources and sequence state. The preparation fixture will check this boundary through production queues and commitment. It will also check sequence wrap, independent deadlines, count and byte limits, and numbered unsent members. Existing collision recovery remains a single-frame path. A5 will connect this preparation to actual aggregate submission. The inspected owners, interface implementations, and unsealed paths validate this contract before source edits. + +### A5 implementation contract + +HCF will retain one prepared batch beside the canonical members in the in-progress queue. The frame sequence will use a distinct aggregate transmit step and an implicit Block Ack receive step. This distinction prevents an envelope from entry into ordinary MAC-header paths. Each batch will retain one actual DATA mode through size reduction, RTS prediction, admission, and submission. HCF will supply transmitted retries first, followed by numbered unsent members, each in cyclic sequence order. + +Tx will use its existing IFS timer for aggregate requests. At expiry, HCF will validate current agreement identity, member eligibility, deadlines, mode compatibility, and complete exchange cost. HCF will commit queued previews only after those checks. Tx will finalize each member before envelope construction. Actual submission will record every member's allocation index with the agreement owner. + +Cancellation will clear the Tx request before HCF deletes its exchange context. Refusal will produce no DATA attempt, DATA outcome, probe, or queue extraction. Zero IFS will use the same submission callback. Ordinary Tx callers will retain their current contract. No new timer or simulation event is necessary. + +The shared response selector will accept an actual eliciting mode instead of a fabricated BAR. Explicit and implicit Compressed responses will use the same legal non-HT rate rule. The recipient procedure will construct the bitmap directly from current receipt state. The implicit response will follow SIFS and use the aggregate's common Duration value. HCF will retain the existing receive-start timer and reception-completion mechanism. + +The acknowledgment owner will attach all submitted members to the implicit wait. A valid matched bitmap will complete one successful response exchange. Positive bits will acknowledge only transmitted members. Missing bits will retain retry eligibility and independent deadlines. A missing or invalid response will produce one contention failure and arm one pending probe on the current agreement. + +The BAR policy will select pending probes through existing EDCA classification. Submission will consume the probe flag. Refusal and member expiry will retain the flag until transfer completion or agreement retirement. Probe outcomes will produce no second DATA attempt or member outcome. Aggregate RTS failure will update contention once without member retry increments or disposal. + +The proposed exchange fixture uses production association, queues, radios, and both DATA hops through an AP. It will assert member FCS, shared wire fields, actual aggregate modes, matched responses, and upper-layer delivery. Its bounded matrix varies PHY representation, NIST/Yans, protection, and TXOP limit at seed zero. Separate focused cases must prove cancellation, partial receipt, failed responses, probe transitions, and recovery under continuous traffic. A build or healthy exchange cannot establish those failure paths. + +Current source inspection identifies each owner and its terminal paths. The WLAN sources remain outside the packet subsystem seal. The new exchange fixture exists but remains unrun. These facts validate the contract before the A5 production edit. A4 retains sole production write ownership until its stable handoff. + +### Required A5 scope correction + +The infrastructure fixture initially showed source aggregates but individual AP frames. DS forwarding copies the incoming transmitter address into its outgoing header. Tx replaces that address only at submission. The HT policy therefore refused the AP's queued candidates before that replacement. + +An initial correction assigned the local transmitter address in DS. Fingerprint comparison exposed an ordinary A-MSDU effect: its builder derives Address3 from that transmitter field. The correction therefore changed ordinary wire bytes. The final implementation removes the DS edit. + +Aggregate preparation now assigns the local transmitter address only to its independent queued preview. HCF supplies that address through the existing batch limits. Tx assigns the same address before FCS calculation. + +The queued source retains its identity, owner, header, sequence state, and deadline until commitment. Retained canonical members still face the existing transmitter check. This correction does not normalize a pre-existing numbered, unsent AP member with another transmitter address. Such a member can use standalone service. + +The preparation fixture adds a forwarded-source case. The infrastructure exchange checks aggregate transmission across both hops. Fresh fingerprint checks verify ordinary behavior after the DS edit disappears. + +### Required cyclic arithmetic correction + +The physical reorder fixture reaches sequence wrap with a two-slot window. The shared cyclic helper rejects an intermediate arithmetic value of minus one. Its operators already reject an unspecified source before that calculation. Thus, valid arithmetic such as `0 - 1` cannot return sequence 4095. Window advancement can fail through the same helper. + +The unsealed target is `mac/common/Ieee80211Defs.h`. The correction removes only the internal rejection of a valid arithmetic intermediate. Constructors and operators retain their checks for an unspecified sequence object. A new unit fixture will check every sequence against bounded positive and negative offsets. The real-radio wrap cases will verify receipt and reorder advancement. These facts validate the expanded contract before the edit. + +## A2–A7 result and verification scope + +HCF now transmits independent HT MPDUs in one PPDU after acceptance of a Compressed Block Ack agreement. Each member retains its sequence number, FCS, allocation index, lifetime, and retry state. The recipient validates correct members, updates receipt state, applies mandatory release, and returns one Compressed response after SIFS. The originator retries only unacknowledged members. + +For example, an aggregate can contain three members with sequence numbers 10, 11, and 12. If member 11 fails, the recipient records 10 and 12. The originator retains 11 with its original deadline. A later aggregate can combine that retry with new members. This example describes the implemented mechanism; the physical loss fixtures supply direct checks. + +The transmit policy supports HT mixed format, 20 MHz, one retained long-GI DATA mode, and fixed rates. The receiver also accepts advertised HT20 short-GI aggregates. Basic and disabled MPDU policies retain their historical behavior. Adaptive aggregate rates, HT40 transmit aggregation, VHT aggregates, Multi-TID Block Ack, and correlated FEC errors remain outside this profile. + +The originator marks one probe requirement after an unsuccessful implicit response. BAR selection checks that requirement before further DATA, subject to access-category ownership and legal admission. Actual BAR submission consumes the requirement. Probe failure returns to DATA retry without a second DATA outcome. A BAR received without an agreement uses the existing DELBA path. Continuous traffic can therefore retire a stale agreement without queue drain. + +### Required admission correction + +The example exposed a reservation endpoint beyond the positive TXOP deadline. At event 2845, the remaining reservation was 2.831966644 ms. The remaining TXOP was 2.831933288 ms. Fractional propagation delay and Duration encoding produced the 33.356 ns difference. The new preparation path bypassed the legacy admission guard against that condition. + +HCF now refuses the next prepared exchange when the reservation endpoint exceeds the deadline. The explicit initial singleton-overrun exception remains valid. Refusal occurs before queue commitment or a DATA attempt. The admission fixture adds a fractional-propagation case and an equal-endpoint control. The example retains its physical propagation delay. + +### Interface contracts + +The response-rate method and actual-submission callback are pure virtual interface declarations. Every in-tree implementation supplies a concrete override. DCF requires no submission transition, so its concrete callback does nothing. Custom rate selectors must implement implicit response selection or throw an explicit capability diagnostic. The migration guide lists these obligations. + +### Final campaign + +The final campaign uses the unchanged base and uncommitted source described above. Its artifacts reside in `out/verification/ampdu-a2-a7/`. This ignored directory retains logs across tool-session replacement. Earlier `/tmp/ampdu-*` logs became unavailable after such a replacement. Earlier results remain historical observations; the final campaign repeats the affected checks against the fresh library. + +Each runner first executes `source ./setenv -q`. Each runner uses `MAKEFLAGS=--jobserver-style=pipe MPLCONFIGDIR=/tmp/ampdu-matplotlib`. Debug compilation precedes all behavioral checks. The unit, module, and protocol commands below use serial fixture execution. Independent categories use separate generated directories. + +```sh +make MODE=debug -j8 +make MODE=release -j8 + +inet_run_unit_tests -m debug -f 'Ieee80211(Ampdu.*|SequenceNumberCyclic|BlockAck.*|CompressedBlockAck.*|ResponseTimeout|TxNavChoice|TxopProcedure|HtCapabilities|HtMgmtElements|HtModeSet|HtModeCache)_1\.test$' --no-concurrent + +inet_run_module_tests -m debug -f 'Ieee80211(Ampdu(Admission|Capabilities|Exchange|Preparation|Recovery|ResponseWindow|RxBoundary|TxStart|PhyReception)|CompressedBlockAck.*|BlockAck.*|Edca.*Lifetime.*|HtAssociation|MgmtStaSimplifiedInitialization|VhtHtSupport|HcfMultipleProtection|HcfSingleProtection|SingleProtectionGroup|Txnav)_1\.test$' --no-concurrent + +inet_run_module_tests -m debug -f 'Ieee80211AmpduReordering_1\.test$' --no-concurrent + +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '/(N_AmpduImplicitBa|N_Ampdu|N_BlockAck|Ac_BlockAck|Ac_BlockAckFallback)\.test$' --no-concurrent +``` + +The unit selection checks response validation, sequence wrap, batch construction, probe selection, and ordinary timing controls. The module selection checks production negotiation, physical reception, actual submission, admission, retry, stale-agreement recovery, and historical compatibility. The separate reorder campaign preserves its original 160 physical cases and adds 16 negative RIFS cases. Seed zero controls the feature fixtures except the PHY matrix, which defines seeds zero, one, and two. + +The real infrastructure exchange varies scalar and dimensional reception, NIST and Yans, single and multiple protection, and zero or positive TXOP limits. Its 16 main configurations require exactly 60 upper deliveries across both AP hops. Three additional cases force full, partial, or failed probe recovery with a healthy agreement. Fresh DATA remains queued at the probe. The fixture checks later DATA progress, original deadlines, retry order, mixed batches, and one outcome per submitted member. + +The admission matrix covers both bands, MCS 0/7, complete-PSDU RTS thresholds, spacing, fixed-mode reduction, and HT duration boundaries. It also checks positive-limit singleton overrun, zero-limit behavior, post-CTS cancellation, and new, retained, or mixed-batch RTS failure. RTS failures preserve member state without a DATA attempt. The final two cases check reservation overshoot and equality with actual propagation delay. + +The response-window fixture uses a real outstanding receive timer. A new aggregate overlaps the timeout. An addressed aggregate receives one response after SIFS; an overheard aggregate resolves the previous wait without a local response. Long and short GI cases use actual reception. The response uses non-HT 6 Mbps when BSS basic-rate state is unknown. + +The recovery fixture separately checks absent-agreement behavior. Continuous cases retain fresh queued DATA with inactivity values zero and 200 ms. They require BAR, recipient DELBA, agreement retirement, later ADDBA, and further aggregates. Finite cases lose the probes and require lifetime completion. These fixtures distinguish seeded agreements from the infrastructure fixture's production management exchange. + +The example provides scalar, dimensional, and zero-TXOP configurations. The user guide explains opt-in and recovery. The migration guide lists each changed extension role. WHATSNEW states the supported profile and its limits. These artifacts establish no throughput or complete-amendment conformance claim. + +The user separately approves removal of the historical `N_Ampdu.test` expected-failure marker on 2026-10-18. The final focused campaign proves aggregate observation, implicit response, and exactly 60 application deliveries before that removal. The VHT marker and stored fingerprints remain unchanged. No commit or publication forms part of this execution. + +### Corrected evidence boundaries + +The first protocol output check matched a test name without its verdict. Raw output showed a failed protocol assertion despite the runner's PASS. Shared INI defaults also preceded the intended traffic overrides. The corrected fixtures place their overrides first and require complete PASS records. + +UDP supplies the supported upper-delivery signal. The sink's actual finish counter independently proves application receipt. The deterministic zero-propagation fixture disables only the diagnostic for simultaneous transmission starts; the medium still models interference. + +An all-failed aggregate exposed an ownership warning. Rx retained ownership of the envelope after its method returned. MAC now takes the envelope and returned members before local disposal or HCF handoff. The strict reorder checker requires all 176 successful runs without warnings or errors. + +The first ownership build failed inside Clang with invalid-UTF-8 diagnostics for built-in definitions. The compiler failure's cause remains unknown. A retry with `CCACHE_DISABLE=1` compiled the same source successfully. Subsequent final source builds retain that setting. `debug-ownership-build.log` and `debug-ownership-build-retry.log` preserve both results. + +Positive RIFS remains unverified because a consistent supported configuration cannot reach it. HCF compares the physical gap plus the preceding signal extension with 2 microseconds. HT at 2.4 GHz adds a 6 microsecond extension. The `ac` mode admits HT at 5 GHz, but HCF excludes RIFS for that VHT mode set. The fixture preserves all reachable SIFS cases and adds 16 negative RIFS cases. It does not introduce a mixed, inconsistent mode configuration. + +### Mechanical checks and review + +Both scoped WLAN architecture checks pass. The changed-file NED/MSG naming check passes. The source-seal check passes without a sealed source edit. The document-seal index remains consistent. The separate `AGENTS.md` change remains outside this feature work. The final preservation check uses its recorded SHA-256. + +The interface checker exits 1 for two unchanged bodies in `IFrameSequence.h`. `AV-CONTRACT-02` records these as open deviations in `doc/project/audit/architecture-exceptions.md`. The new interface methods contain no default bodies. Independent review confirms that each in-tree implementation supplies its required override. These facts do not imply a clean project-wide interface gate. + +The complete document-link check reports 163 broken links to absent standards files in unchanged documents. The scoped check uses the canonical checker with an explicit inventory of four changed Markdown documents. It reports zero broken links. The C++ checker cannot run because this build supplies no `compile_commands.json`; it exits 2. Independent source review and debug/release compilation remain separate evidence. No C++ checker pass follows from them. + +The final narrow source review resolves the preview address, ownership, interface, and protocol-observation defects. The earlier integrated review covers response windows, singleton overrun, probe counters, and cyclic sequence arithmetic. Neither review supplies runtime evidence by itself. No new production-source finding remains open in those review scopes. + +The artifacts use these names: + +| Check | Result | Artifact under `out/verification/ampdu-a2-a7/` | +| --- | --- | --- | +| Final debug compilation. | Exit 0. | `debug-preview-build.log` | +| Final release compilation. | Exit 0. | `release-final-build.log` | +| Unit campaign. | 17 fixtures pass. | `units.log` | +| Module campaign. | 45 fixtures pass. | `modules.log` | +| Final preview and infrastructure exchange. | Both fixtures pass; preparation includes 12 cases and two AARF rejection controls. | `preview-integration.log` | +| Physical reorder and history. | 176 runs pass without warnings or errors. | `reordering-final176.log`, `.out`, and `.err` | +| Focused Wi-Fi protocol campaign. | All five fixtures pass before marker removal. | `protocol-final-focused.log` | +| Complete Wi-Fi inventory after setup recovery. | 77 distinct fixtures: 48 passes and 29 expected failures. No unresolved error or unexpected runtime failure remains. | `protocol-combined-results.json`, `protocol-full.log`, `protocol-space-retry.log`, `protocol-matcher-retry.log` | +| Ordinary fingerprints. | All 23 configurations pass; exit 0. | `fingerprints-final.log`, `fingerprints-final-raw.log` | +| Graphical fingerprints. | All 20 match the parent; 18 stored expectations differ on both revisions. The runner exits 1. | `fingerprints-final-tyf.log`, `final-tyf-parent-comparison.json` | +| Three example configurations. | All reach the simulation limit without error. | `example-final-General.log`, `example-final-ZeroTxop.log`, `example-final-Dimensional.log` | +| MAC and PHY architecture. | Both pass. | `architecture-mac.log`, `architecture-phy.log` | +| Changed-file names and source seals. | Both pass. | `naming-diff.log`, `source-seals.log` | +| Document seals and scoped links. | Both pass. | `document-seals.log`, `document-links-scoped.log` | +| Interface checker. | Two known, unchanged deviations; exit 1. | `interfaces.log` | +| Complete document links. | 163 pre-existing missing standards links; exit 1. | `document-links.log` | +| C++ checker. | No compilation database; no analysis; exit 2. | `cpp.log` | + +The module campaign spans the final corrections. The final preview and exchange rerun replaces its earlier evidence for the changed preparation path. The strict reorder run replaces the earlier run with an ownership warning. Other affected fixtures use the fresh library after their relevant source correction. The final protocol and fingerprint campaigns start after the last production edit. + +### Final regression campaigns + +The focused protocol campaign passes all five fixtures. The corrected implicit fixture runs both normal receipt and deterministic partial loss. Each run requires 60 UDP deliveries, 60 sink receipts, and its complete protocol PASS record. The historical HT fixture retains aggregate observation and adds receipt completion. The approved marker removal applies only to that HT fixture. The complete Wi-Fi campaign uses this command: + +```sh +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' --no-concurrent +``` + +The full campaign selects 77 test files. Its log is `protocol-full.log`. Existing expected failures remain explicit. The runner's preview mode previously raised a selection error before any simulation. That preview supplies no executed-test evidence. The actual campaign supplies the executed inventory. + +The full invocation reports 40 passes, 22 expected failures, and 15 setup errors after filesystem exhaustion. Its log also shows a matcher write error for `Ac_MultiTidBa`. Thus only 61 fixtures supply complete observations before recovery: 40 passes and 21 expected failures. The invocation exits 1. No unexpected runtime failure precedes that setup failure. The HT marker metadata still reflects its earlier declaration in that invocation; its observed PASS remains valid. + +The recovery removes duplicate generated matcher output and compresses raw simulation logs without source changes. Compressed logs remain beside their original paths as `test.out.gz`. This action restores 4.7 GiB of free space. A filtered retry selects the 15 setup failures and the approved HT marker control. It reports nine passes and seven expected failures, with exit 0. Its log is `protocol-space-retry.log`. + +A separate `Ac_MultiTidBa` retry resolves the matcher write error. Its log is `protocol-matcher-retry.log`. The original full-invocation errors remain visible in the record. These filtered retries replace incomplete evidence; they do not change source or expectations. + +The matcher retry reports its expected failure and exits 1 under the runner's all-failure aggregation rule. The final combined inventory contains all 77 distinct fixtures: 48 passes and 29 expected failures. Every final observation matches its current declared expectation. `protocol-combined-results.json` identifies the authoritative artifact for each fixture. No unresolved setup error or unexpected runtime failure remains. This result combines the full invocation with the bounded retries; it is not a single successful full-suite invocation. + +```sh +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '/(Ac_LdpcStbcCap|Ac_Ldpc|Ac_4ss|Ac_OpModeNotification|Ac_PhyMode|Ac_MuMimo|Ac_VhtIe|Ac_Ampdu|Ac_256qam|Legacy_RtsCts|Legacy_PhyMode|Legacy_DataAck|Legacy_Pcf|Legacy_Fragmentation|Legacy_Nav|N_Ampdu)\.test$' --no-concurrent +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '/Ac_MultiTidBa\.test$' --no-concurrent +``` + +The ordinary fingerprint campaign passes all 23 selected configurations without an expectation change. It uses the final debug library and these exact commands from the repository root: + +```sh +cd tests/fingerprint +PYTHONUNBUFFERED=1 ./fingerprinttest -d -t 4 -m '(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless/qos|adhoc/qos))' -f 'tplx' -f '~tNl' -f '~tND' +``` + +`fingerprints-final.log` records the 23 passes and exit 0. `fingerprints-final-raw.log` preserves the exact simulator commands and output. The CSV rows define configurations and limits. The scalar attributes record seed sets zero and one. The passing byte fingerprints replace the earlier four differences from the removed DS edit. + +Graphical fingerprints use a separate campaign with the same selector and `-f 'tyf'`. The legacy wrapper supplies fake-GUI probability 0.001. Parent controls use that same value. The newer Python API's probability 0.1 would define different inputs. These checks do not establish visual rendering correctness. + +The parent control uses revision `56b75c959af104b5db931bef022bdfe5578eead9` and its existing debug library. No parent production source is newer than that library. The control uses the same current NED inputs, commands, configurations, and seeds as the feature. It changes no parent file. `fingerprint-control/README.md` states the library hashes and input identities. The control covers all 20 graphical configurations separately. + +The final graphical campaign records two baseline passes and 18 baseline mismatches; its runner exits 1. Every final actual value equals its corresponding parent value. `compare-final-tyf.py` verifies all 20 rows against the isolated parent results. Its output is `final-tyf-parent-comparison.log`; its detailed rows are `final-tyf-parent-comparison.json`. This comparison establishes no new graphical fingerprint change under the recorded inputs. It does not repair the pre-existing baseline differences. + +### Stage closure + +| Stage | Closure evidence | +| --- | --- | +| A2. | PHY refusal stops before standalone MAC/FCS peeks. The physical matrix retains all 468 runs and copied outcome checks. | +| A3. | Twelve preparation cases and two AARF rejection controls prove refusal, commitment, retained members, limits, and forwarded previews. | +| A4. | All 176 physical reorder/history cases pass without warnings or errors. They cover reachable mandatory release and the recorded negative RIFS boundary. | +| A5. | Admission passes 57 runs. Actual-start and response-window fixtures pass. Infrastructure exchanges verify computed member FCS, implicit responses, selective retry, and probe policy. | +| A6. | Production management establishes both AP hops. Healthy and absent-agreement recovery pass. Historical module controls and the complete protocol inventory retain their declared outcomes. | +| A7. | The example, user guide, migration guide, release note, and execution records exist. Debug/release compilation, ordinary fingerprints, and parent graphical comparisons complete the declared verification. | + +The known interface deviations, absent C++ compilation database, missing standards links, and positive-RIFS coverage limit remain explicit above. No publication gate receives an inferred pass. The plan remains under `pending` because A0 is open. The approved execution exception closes A1–A7 on this base. The later publication request authorizes commit, push, and PR creation. Merge approval remains separate. + +## A1 scope and authorization + +The user authorizes A1 on base `56b75c959af104b5db931bef022bdfe5578eead9` before the prerequisite PRs merge. This exception applies to A1 only. The active branch is `work/ieee80211-ampdu-implicit-blockack-a1-a7`. The [feature plan](ieee80211-ampdu-implicit-blockack.md) defines A1. The [PHY execution record](ieee80211-ampdu-phy-execution.md) supplies the prerequisite evidence. + +GitHub queries failed, so this execution does not confirm merge status. No commit, push, or baseline change forms part of this task. + +## Validated implementation contract + +The MIB owns local HT capabilities and negotiated receiver limits. Before A1, management conversion omitted minimum MPDU start spacing. The serializer already preserved its encoded bits. A1 adds the encoded value to both capability directions and management conversion. Existing peer removal removes the complete capability state. The default remains zero. + +The aggregate builder owns delimiter insertion, alignment, and content references. Before A1, its region-tag destination preceded the member content. A1 corrects that offset and uses the source Packet's front offset. A new construction API retains the source Packets on success and refusal. The existing consuming API retains its ownership contract. No queue, sequence, lifetime, retry, or acknowledgment state changes through construction. + +The HT mode owns the conversion from receiver spacing to octet distance. It also owns the L-SIG length and PPDU duration checks. The builder uses these calculations with the supplied peer byte limit. It adds zero-length delimiters between members when alignment alone cannot satisfy spacing. The final member has no alignment padding or extra delimiter. + +Empty batches and size-limit violations produce refusal before allocation or source disposal. Invalid API parameters produce an error. + +For example, two 30-octet members at 65 Mbps with encoded spacing 7 require 130 octets between their starts. Alignment and zero-length delimiters increase that distance to 132 octets. The builder retains both source Packets. The resulting aggregate ends immediately after the second member. + +The focused checks cover all spacing encodings and exponents, asymmetric negotiation, management bytes, and cleanup. Existing full and simplified association fixtures verify their production paths. New construction checks cover alignment, zero-length delimiters, byte limits, source retention, region tags, and singleton identity. The existing PHY wire fixture owns HT-SIG golden bytes and independent decode. The PHY reception fixture owns error-model compatibility. These checks do not establish aggregate selection or implicit responses, which belong to later stages. + +Source inspection confirms one implementation of the aggregate interface. No target falls under the packet subsystem seal. The contract passes the pre-write checks. The implementation adds no event, timer, callback, retained protocol state, or lifecycle operation. + +## Result and limits + +The MIB and management preserve the receiver spacing value in both directions. The new aggregate API retains source Packets and refuses excessive member, PSDU, peer, or duration values. The builder copies region tags to member content positions. HT modes supply octet distance, L-SIG length, and separate duration predicates. No HT-SIG field, serializer, or bounded parser required a change; A1 consumes the implemented P1 representation. + +The new construction fixture checks MCS 0 and 7, four alignment residues, and all eight spacing encodings. It checks the 65535-octet PSDU boundary at MCS 15, where airtime permits that length. This calculation test does not expand the feature's transmit profile. It separately checks the 4095-octet member boundary and source retention after refusal. Deaggregation restores each member's region-tag offsets. Fresh bytes preserve singleton HT-SIG identity. + +The capability fixture checks all 32 combinations of length exponent and spacing through production simplified management. It passes the negotiated transmit limits to the builder. Existing fixtures check asymmetric values through full association and simplified restart, shutdown, and crash. Peer cleanup removes the complete old state. The PHY reception fixture retains its absent-role, incomplete-coverage, absent-model, and compatible-model checks. + +A1 adds no aggregate selection, DATA commitment, implicit response, or selective retry. The existing disabled invocation and default aggregate policy remain in place. A3 owns the production selection of members. Later stages retain their MAC reception and exchange requirements. No fingerprint, performance, or complete feature claim follows from these checks. Release compilation and publication gates remain outside this task. + +## Verification + +All commands ran from the active checkout. Builds use debug mode. Each test command first uses `source ./setenv -q`. This selects the active checkout instead of the inherited parent environment. Each runner also uses the environment assignments below: + +```sh +MAKEFLAGS=--jobserver-style=pipe MPLCONFIGDIR=/tmp/a1-matplotlib +``` + +The assignments precede each runner command on the same shell line. They select pipe-based make coordination and a writable Matplotlib directory. The initial inherited environment caused a read-only filesystem error before any test ran. The corrected environment resolved that setup error. + +| Check | Exact command after the environment setup | Result and artifact | +| --- | --- | --- | +| Base library before source edits. | `make MODE=debug -j8` | Exit 0; `/tmp/a1-build-base.log`. | +| Base unit controls. | `inet_run_unit_tests -m debug -f 'Ieee80211(HtCapabilities\|HtMgmtElements\|AmpduWire\|CompressedBlockAckRecord)_1\.test$' --no-concurrent` | Four fixtures pass, exit 0; `/tmp/a1-base-unit.log`. | +| Base ordinary PHY and Normal Ack controls. | `inet_run_module_tests -m debug -f 'Ieee80211(VhtHtSupport\|CompressedBlockAckMigration)_1\.test$' --no-concurrent` | Two fixtures pass across 13 runs, exit 0; `/tmp/a1-base-module.log`. | +| A1 library. | `make MODE=debug -j8` | Exit 0; `/tmp/a1-build.log`. | +| Unit selection. | `inet_run_unit_tests -m debug -f 'Ieee80211(AmpduCapabilityEncoding\|AmpduConstruction\|AmpduWire\|MpduDelimiterWire\|MpduSubframeHeaderSerializer\|HtCapabilities\|HtMgmtElements\|HtModeSet\|HtModeCache\|CompressedBlockAckRecord)_1\.test$' --no-concurrent` | Eight existing fixtures pass. Two new fixtures require test corrections; exit 1. `/tmp/a1-unit.log`. | +| Corrected new unit fixtures. | `inet_run_unit_tests -m debug -f 'Ieee80211Ampdu(CapabilityEncoding\|Construction)_1\.test$' --no-concurrent` | Both fixtures pass, exit 0; `/tmp/a1-new-unit.log`. | +| Module selection. | `inet_run_module_tests -m debug -f 'Ieee80211(AmpduCapabilities\|HtAssociation\|MgmtStaSimplifiedInitialization\|VhtHtSupport\|CompressedBlockAckMigration\|BlockAckAssociationReset\|CompressedBlockAckExchange)_1\.test$' --no-concurrent` | Six existing fixtures pass across 47 runs. The new fixture requires a test correction; exit 1. `/tmp/a1-module.log`. | +| Corrected capability fixture and PHY controls. | `inet_run_module_tests -m debug -f 'Ieee80211Ampdu(Capabilities\|PhyReception)_1\.test$' --no-concurrent` | Both fixtures pass, exit 0; `/tmp/a1-capabilities-phy.log`. Capability coverage uses 32 runs with seed 0. PHY coverage uses 468 defined configurations with its seeds 0, 1, and 2. | + +The new unit fixtures required an explicit Packet include and a valid HT factory call. The new module fixture required an explicit global class name. Its first corrected run completed all 32 simulations but lacked the required output marker. A direct completion message resolved that fixture failure. `/tmp/a1-capabilities-module.log` records the intermediate marker failure. Production source remained unchanged through these test corrections. + +The final evidence covers ten distinct unit fixtures and eight distinct module fixtures. No final fixture retains an unexpected failure. The table preserves earlier runner failures rather than treating their aggregate exit status as success. The source base has no production diff before the base build; `/tmp/a1-base-source-diff.log` records that check. + +Module fixtures use configuration `General`. All module runs use seed 0 except the PHY fixture, which explicitly varies seeds 0, 1, and 2. The exact run ranges are: + +| Fixture | Runs | +| --- | --- | +| `Ieee80211VhtHtSupport_1.test` | 0–6 | +| `Ieee80211CompressedBlockAckMigration_1.test` | 0–5 | +| `Ieee80211CompressedBlockAckExchange_1.test` | 0–15 | +| `Ieee80211BlockAckAssociationReset_1.test` | 0–15 | +| `Ieee80211HtAssociation_1.test` | 0 | +| `Ieee80211MgmtStaSimplifiedInitialization_1.test` | 0 | +| `Ieee80211AmpduCapabilities_1.test` | 0–31 | +| `Ieee80211AmpduPhyReception_1.test` | 0–467 | + +Both scoped architecture commands pass with exit 0: + +```sh +doc/project/enforcement/check-architecture.sh src/inet/linklayer/ieee80211 +doc/project/enforcement/check-architecture.sh src/inet/physicallayer/wireless/ieee80211 +``` + +Their logs are `/tmp/a1-architecture-mac.log` and `/tmp/a1-architecture-phy.log`. `doc/project/enforcement/check-source-seals.sh --diff` passes with exit 0. Its log is `/tmp/a1-source-seals.log`. `python3 doc/project/enforcement/check-ned-msg-naming.py --diff --scope src/inet/linklayer/ieee80211` passes with exit 0. Its log is `/tmp/a1-naming-diff.log`. + +The complete WLAN naming scan uses `doc/project/enforcement/check-naming.sh src/inet/linklayer/ieee80211`. It exits 1 with 32 candidates, all in unchanged source files. `/tmp/a1-naming.log` preserves that result. This task introduces no candidate in that scan. The execution does not claim that the complete subtree satisfies every naming rule. + +The self-audit confirms the interface implementation, ownership paths, source offsets, byte limits, and mode calculations against the validated contract. No implementation scope expansion occurred. `git diff --check` passes. A byte comparison confirms preservation of the separate `AGENTS.md` change. + +## Standard evidence + +The implementation uses normative IEEE Std 802.11-2024 requirements. The corpus supplies these canonical nodes: + +| Requirement | Canonical node | Physical PDF pages | +| --- | --- | --- | +| Receiver length and spacing encodings. | `ieee80211-2024:table:9-225` | 1126 | +| Octet distance and zero-length delimiters. | `ieee80211-2024:clause:10.12.3` | 1977–1979 | +| Mixed-format L-SIG length calculation. | `ieee80211-2024:clause:10.27.4` | 2091–2092 | +| HT PSDU limit, PPDU time limit, and signal extension. | `ieee80211-2024:table:19-25` | 3493–3494 | + +The corpus required a rebuild for its current format. Its linter reports 76 ambiguous headings and 425 unresolved references across the corpus. Exact node retrieval identifies the requirements above. The spacing clause resolves its reference to Table 9-225 through exact table retrieval. + +PDF inspection confirms the ceiling operators in Equation (10-17) on physical page 2091. The image is `/tmp/a1-duration-standard.png`. The raw retrieval and lint artifacts use the `/tmp/a1-` prefix. diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md index 6c56fc574dd..4f392cbb506 100644 --- a/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md +++ b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md @@ -1,16 +1,18 @@ # A-MPDU with implicit Compressed Block Ack: plan summary +The rebase on 2026-11-04 uses `work/ieee80211-ampdu-implicit-blockack` at `c136f222f30fab53144b438e28ff9bc3b7107c28`. It preserves chunk-based PHY outcomes and Rx ownership of interframe evidence. Historical results in the execution record describe the earlier base. Only the final rebased commit receives new build and test execution. Intermediate results remain unverified. + > **Kind:** reference · **Status:** draft · **Seal:** none · **Owns:** — · **Stands on:** [feature plan](ieee80211-ampdu-implicit-blockack.md), [PHY plan](ieee80211-ampdu-phy-reception.md) -This plan proposes several independently acknowledged MPDUs in one HT transmission. The recipient sends one Compressed Block Ack after the transmission ends. The aggregate requests that response without a separate BAR. The originator retries only the MPDUs that remain unacknowledged. +This feature supplies several independently acknowledged MPDUs in one HT transmission. The recipient sends one Compressed Block Ack after the transmission ends. The aggregate requests that response without a separate BAR. The originator retries only the MPDUs that remain unacknowledged. -This summary describes the [feature plan](ieee80211-ampdu-implicit-blockack.md) and [PHY plan](ieee80211-ampdu-phy-reception.md). Both plans use `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. The MAC feature remains proposed. The diagrams describe the design, not executed simulation traces. +This summary describes the [feature plan](ieee80211-ampdu-implicit-blockack.md) and [PHY plan](ieee80211-ampdu-phy-reception.md). Both plans use `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. This branch contains the MAC feature from the original nine-commit series. The diagrams describe the design, not executed simulation traces. Master contains the merged explicit Compressed Block Ack implementation from PR #1345. Rx supplies local interframe observations. The radio supplies metadata for Radiotap. The PHY topic must preserve these contracts. Only the final commit receives build and test execution. The PHY plan supplies aggregate reception before the MAC feature uses it. Its first gate proves deterministic member outcomes and includes the shared NIST/Yans implementation. It also preserves default HT operation and reception without an error model. Its second gate extends the NIST/Yans and compatibility evidence. The MAC feature can reach deterministic integration between those gates. -The ten-commit series supplies PHY P0 through P4 and gates D1 and D2. The [final execution record](ieee80211-ampdu-phy-execution.md) states verification and its limits. VHT identity, bounded inspection, deaggregation use, and HT-SIG representation have separate commit boundaries. P3 checks NIST/Yans probabilities and seeded outcomes. P4 supplies ordinary controls. MAC aggregate receipt and implicit Block Ack remain pending. +The ten-commit series supplies PHY P0 through P4 and gates D1 and D2. The [final execution record](ieee80211-ampdu-phy-execution.md) states verification and its limits. VHT identity, bounded inspection, deaggregation use, and HT-SIG representation have separate commit boundaries. P3 checks NIST/Yans probabilities and seeded outcomes. P4 supplies ordinary controls. The MAC feature adds aggregate receipt and implicit Block Ack on this PHY base. The merge prerequisite is satisfied on the integrated master base. MAC step A0 must compare the contracts and record a feature-off baseline before MAC implementation. @@ -337,7 +339,9 @@ flowchart TD D2 --> A7 ``` -The two plans now contain ten fixtures across unit, module, and protocol tests. The PHY plan owns three implemented fixtures; the feature plan owns seven proposed fixtures. P4 adds the parent-compatible ordinary reception fixture. The complete feature still needs source ownership, reorder release, exchanges, recovery, timing, and production negotiation checks. Current Basic, explicit Compressed, lifetime, reset, and disabled-feature checks provide regression coverage. Compatible configurations with disabled transmit aggregation must preserve current frame sequences and random draws. [Verification methods](ieee80211-ampdu-implicit-blockack.md#7-verification-methods-and-expected-results). +The implementation supplies the planned module and protocol fixtures. Additional fixtures isolate PHY refusal, actual submission, response-window recovery, response validation, capability encoding, construction, and cyclic sequence arithmetic. The [execution record](ieee80211-ampdu-implicit-blockack-execution.md) maps these checks to their boundaries. The PHY plan also retains its ordinary reception comparison with the parent. + +Basic, explicit Compressed, lifetime, reset, and disabled-feature checks provide regression controls. Physical fixtures check reorder release, exchanges, recovery, timing, and production negotiation. Compatible configurations with disabled transmit aggregation must preserve current frame sequences and random draws. [Verification methods](ieee80211-ampdu-implicit-blockack.md#7-verification-methods-and-expected-results). Direct exchange assertions must prove one DATA PPDU, one response after SIFS, correct receipt bits, selective retry, and exactly-once upper delivery. Physical reception tests must produce the partial results through the real receiver. An injected bitmap cannot prove that PHY integration. Fingerprints can detect a changed trajectory; they cannot replace these assertions. diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack.md b/plan/pending/ieee80211-ampdu-implicit-blockack.md index d6856937def..06287ca0c9f 100644 --- a/plan/pending/ieee80211-ampdu-implicit-blockack.md +++ b/plan/pending/ieee80211-ampdu-implicit-blockack.md @@ -1,5 +1,7 @@ # Implementation plan: A-MPDU with implicit Compressed Block Ack +The rebase on 2026-11-04 uses `work/ieee80211-ampdu-implicit-blockack` at `c136f222f30fab53144b438e28ff9bc3b7107c28`. It preserves chunk-based PHY outcomes and Rx ownership of interframe evidence. Historical results in the execution record describe the earlier base. Only the final rebased commit receives new build and test execution. Intermediate results remain unverified. + > **Kind:** procedure · **Status:** draft · **Seal:** none · **Owns:** — · **Stands on:** [PHY prerequisite plan](ieee80211-ampdu-phy-reception.md), [S5 execution record](../done/ieee80211-compressed-blockack-s5-execution.md), [implementation plan procedure](../../doc/project/guide/write-an-implementation-plan.md), [IEEE 802.11 rules](../../doc/project/domain/ieee80211.md) Checkout: `/home/user/omnetpp_ws/inet-ieee80211-ampdu-implicit-blockack`, branch `work/ieee80211-ampdu-implicit-blockack`. The implementation base is `master` at `f5612791b773bc83f654dbfdefa26e243f756c86`. @@ -10,6 +12,10 @@ This plan defines PSDU-based RTS selection, recovery before DATA submission, and The project plan procedure remains a draft; this document uses its structure without an additional approval requirement. +A1 is complete on the separately approved base `56b75c959af104b5db931bef022bdfe5578eead9`. The user authorizes A1 before the prerequisite merges on 2026-10-18. The later request `Execute stages A2-A7` extends that authority on the same base. A2 through A7 are complete within the recorded profile and verification limits. The user subsequently authorizes commit, push, and PR creation. + +The [execution record](ieee80211-ampdu-implicit-blockack-execution.md) states the changes, evidence, and limits. This exception does not close A0. Expected-result changes, commits, and publication retain their separate authority requirements. + This feature plan consumes the [PHY prerequisite plan](ieee80211-ampdu-phy-reception.md). That plan owns aggregate representation, bounded delimiter parsing, cached partial reception, and error-model integration. This plan owns member selection, MAC reception, mandatory release, implicit responses, and recovery. Master contains the prerequisite Block Ack changes, so the merge prerequisite is satisfied. A0 must compare the MAC contracts and record a feature-off baseline before MAC implementation. @@ -393,7 +399,7 @@ The proposed transmit configuration is: **.mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "HtMpduAggregationPolicy" ``` -`HtMpduAggregationPolicy` does not exist yet. The fixed rate above selects HT MCS 0 with one spatial stream under the required HT20 long-GI configuration. These settings also require compatible modes, usable peer HT state, and accepted agreements. The example must configure those conditions explicitly. The initial policy adds no local count or byte cap. It derives those limits from the agreement, peer, and PHY. +`HtMpduAggregationPolicy` implements this opt-in profile. The fixed rate above selects HT MCS 0 with one spatial stream under the required HT20 long-GI configuration. These settings also require compatible modes, usable peer HT state, and accepted agreements. The example configures those conditions explicitly. The initial policy adds no local count or byte cap. It derives those limits from the agreement, peer, and PHY. Custom data services, MPDU policies, Tx/Rx modules, response policies, and response-rate selectors can need interface updates. The migration guide must list each changed method and distinguish unsupported capability from an empty valid result. It must cover aggregate start, cancellation, member finalization, and the complete receive outcome. The DATA rate-control API remains unchanged; aggregate transmit opt-in requires its HCF submodule to be absent. Current `BasicMpduAggregationPolicy` configurations retain their rate-control behavior. The PHY plan owns error-model migration and preserves operation without an error model. @@ -422,7 +428,7 @@ The PHY plan defines two gates. D1 proves cached aggregate reception with determ A1 and A3 can supply reviewed preparation changes before the complete feature exists. A5 and A6 establish deterministic integration after A4. They do not establish NIST/Yans support. The final feature claim requires A7. Dependencies describe delivery order; they assign no concurrent agents. -The following subjects identify the future commits. Each entry uses the purpose, components, behavior, and dependencies in its step above. Section 5 identifies the affected source files. Section 7 defines the test scenarios. Each behavior commit must add the named reviewer document beside its protocol tests under `tests/protocol/wifi/`. +The following subjects identify the target base’s proposed delivery structure. The original MAC implementation uses nine commits instead. Each entry uses the purpose, components, behavior, and dependencies in its step above. Section 5 identifies the affected source files. Section 7 defines the test scenarios. Each behavior commit must add the named reviewer document beside its protocol tests under `tests/protocol/wifi/`. | Commit ID | Proposed subject | Tests and reviewer document | | --- | --- | --- | @@ -443,11 +449,11 @@ Each future A1 through A6 behavior commit must include its direct protocol tests ### Proposed direct checks -The seven fixtures below belong to this feature plan and remain proposed. The [PHY plan](ieee80211-ampdu-phy-reception.md#7-verification-methods-and-expected-results) separately lists its unit, module, and protocol fixtures. Those fixtures check wire fields, bounded inspection, deaggregation, cached reception, ordinary controls, and the current MAC rejection boundary. The [PHY execution record](ieee80211-ampdu-phy-execution.md) states observed PHY results and their limits. That evidence does not establish MAC feature completion. +The fixture table below states the original acceptance requirements. The execution record identifies the implemented fixtures and historical results. The [PHY plan](ieee80211-ampdu-phy-reception.md#7-verification-methods-and-expected-results) separately lists its unit, module, and protocol fixtures. Those fixtures check wire fields, bounded inspection, deaggregation, cached reception, ordinary controls, and the current MAC rejection boundary. The [PHY execution record](ieee80211-ampdu-phy-execution.md) states observed PHY results and their limits. That evidence does not establish MAC feature completion. Fixed causal cases use seed 0. Injected agreements or deterministic error models establish only the stated boundary. Infrastructure cases separately prove production negotiation. [Appendix B](#appendix-b-verification-commands-and-campaigns) contains commands, campaign bounds, and evidence requirements. -| Proposed fixture and category | Trigger and production boundary | Required observation | +| Fixture and category | Trigger and production boundary | Required observation | | --- | --- | --- | | `Ieee80211AmpduCapabilities_1.test`, module. | Full and simplified management with asymmetric receive limits. Include an absent aggregate role, a role without advertised short-GI support, and an absent error model. Use exact MAC inputs with a one-antenna limit and catalog modes outside that limit. | Exponents 0–3 and spacing values 0–7 reach the correct directional consumer; reset removes old peer limits. Absent roles fail before advertisement; incomplete coverage fails before the MIB changes local HT capability. Unsupported unadvertised modes do not cause rejection. An absent model retains valid HT initialization and negotiation. Compatible models retain advertised HT20 short-GI support. | | `Ieee80211AmpduPreparation_1.test`, module. | Production queues, HT policy, fixed DATA rates, batch preparation, and commitment. Include internal collision survivors and aggregate transmit opt-in with AARF. | Source identities, sequence wrap, independent deadlines, count/window/byte limits, and refusal before extraction. Numbered, unsent members keep their allocation and first-DATA Retry value. Internal collision terminal disposal still affects its selected frame only. Batch reduction retains one rate decision. AARF fails initialization with and without fixed-rate overrides. |