diff --git a/WHATSNEW b/WHATSNEW index bdad647e6ad..e9fabaa0469 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -428,6 +428,94 @@ Notable backward incompatible changes are the following: The new parameter urgentOutOfBand, which LinuxTcp sets, gives the receive side of urgent data of Linux without SO_OOBINLINE: the last byte of urgent data leaves the stream, and a read stops before it. INET does not deliver that byte, which Linux gives to recv(MSG_OOB), and it sends no urgent data. Tcp keeps treating URG-marked bytes as ordinary data. Simulations change only where a peer sends urgent data to LinuxTcp. +55. IEEE 802.11 Block Ack agreement lifecycle + + HCF now dispatches accepted Block Ack Actions once after duplicate checks. Setup correlates the peer, TID, and dialog token. The recipient response determines the accepted buffer size and inactivity timeout. Both negotiated flags control A-MSDU use. + + HCF defers new DATA extraction during setup and recovery. The first DATA under an accepted agreement retains the request sequence number. The response timeout starts after the request ACK, with a configurable default of 100 ms. Uncertain setup requires DELBA before surviving unsent DATA can resume. Failed DELBA discards the current deferred batch. Later DATA can start a fresh recovery exchange. + + If an internal collision discards the first DATA before transmission, HCF retires the agreement and queues DELBA. The unsent successor remains deferred until recovery resolves. Lifetime expiry and RTS failure use the same retirement mechanism. + + Directional teardown preserves the opposite agreement. The originator discards transmitted, unacknowledged DATA, even when its lifetime remains valid at inactivity expiration. It also discards unsent fragments through the highest transmitted allocation index. This local index records allocation order before fragmentation. This prevents transmission of an incomplete unit outside the agreement. Later intact DATA remains eligible, including across sequence wraparound. + + Originator retirement now uses local allocation indices to preserve this boundary across large sequence gaps. A distance of exactly 2048 between DATA transmissions no longer lets unsent fragments survive teardown. The wire sequence numbers remain unchanged. + + The recipient delivers complete buffered units in cyclic order across receive gaps before resource release. Association reset instead discards old buffered units in both directions. With Block Ack enabled, reset also discards queued and in-progress QoS DATA for the old peer, including unsent Normal Ack DATA. These changes can alter delivery, disposal, frame exchanges, and fingerprints. + + Custom agreement handlers, recipient services, and management implementations require the changes in the migration guide. External queues need the optional selected-extraction contract only when deferred DATA hides eligible work. Built-in queues preserve ordinary pull accounting for that extraction. + +51. IEEE 802.11 Basic Block Ack transfer completion + + Callers must supply agreement arguments explicitly for BAR selection and transmitted-BAR processing. An omitted argument now fails compilation instead of silently selecting behavior without agreement state. + + Basic BAR selection now separates each peer and TID. Short bursts and small negotiated windows can cause BAR below the configured threshold. BAR can obtain channel access without queued DATA. After final completion or final DATA expiration, the originator sends DELBA even with zero inactivity timeout. Later bursts can require another ADDBA exchange. These changes affect exchange order, airtime, simulation results, and fingerprints for Basic Block Ack configurations. + + Basic single protection now reserves only an already prepared successor. Further DATA extraction starts a new TXOP and can require more contention. Final DATA in a zero-limit TXOP reserves only its immediate response. These corrections can change Duration fields, airtime, and fingerprints. + + Custom QoS ACK policies must accept the agreement-handler argument in isBlockAckReqNeeded() and computeBlockAckReqParameters(). Custom in-progress queues must preserve the extract argument in getPendingFrameFor(). QoS data services must implement IOriginatorQosMacDataService for candidate preview without extraction. + +52. IEEE 802.11 immediate Compressed Block Ack + + HCF defaults to immediate Compressed Block Ack. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. + + Both endpoints must select the same blockAckVariant because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Compressed rejects delayedAckPolicySupported = true in either agreement policy at initialization. + + Compressed receipt state retains positive bits after delivery and reports zero for absent receipts. HCF recognizes the required SIFS or RIFS relation from actual physical endpoints. It can release individual MPDUs under that relation despite a receive gap. Compressed agreements suppress fragmentation for their peer and TID; Basic retains its fragment bitmap. + + Compressed BAR uses a legal non-HT rate. Initial and later BAR consume their applicable BSS and peer rate sets. A configured controlFrameBitrate must satisfy the applicable rule. Response duration, protection, and timeout calculations use the 32-byte response length. Basic retains its separate 152-byte response. + + Active Compressed agreements check complete DATA exchange cost before destructive aggregation. If an aggregate cannot qualify, HCF evaluates its first ordinary MSDU with the same selected mode. Refusal preserves original queue frames, deadlines, and sequence state. Single protection reserves only the current exchange; a later BAR can require fresh contention. These changes can alter airtime, contention, delivery times, and simulation results. Basic and traffic outside active Compressed agreements retain their existing admission limits. + + Before DATA commitment, failed RTS updates access-category retry state without a provisional sequence counter. Original DATA lifetimes bound repeated attempts. This changes the scoped path's former discard after a fixed number of RTS failures. + + Existing demonstrations retain explicit Basic settings, configurations, and result rows. IEEE Std 802.11e-2005 introduced Basic; IEEE Std 802.11-2024 reserves its Type 0 value. Retention does not establish complete historical conformance. VHT A-MPDU, Multi-TID Block Ack, and the existing delayed Basic requirement remain deferred. Compressed support does not establish complete VHT transfer support or MAC stop/start recovery. + + Custom QoS rate selectors, recipient ACK policies, recipient data services, and frame sequences require the interface changes in the migration guide. Custom radio media, communication caches, and IEEE 802.11 modes also require the new timing queries. Basic classes and methods remain available. + +53. Radio medium timing queries + + Radio media now expose copied transmission endpoints and cached arrival endpoints. Consumers can preserve physical timing evidence after cache expiry without retained transmission pointers or new propagation calculations. Receivers attach the transmission identifier to SignalTimeInd so later frame delivery can identify that evidence. Custom radio media and communication caches must implement the new query methods described in the migration guide. + +54. IEEE 802.11 signal extension + + HT duration at 2.4 GHz now includes its six-microsecond signal extension. Previously, that waveform ended early, which could change interframe recognition and airtime. Custom IEEE 802.11 modes must supply the new nominal-extension query. A transmission query supplies the actual extension from its waveform so consumers do not count it twice. + +55. IEEE 802.11 Block Ack wire types + + The serializer supports the complete four-bit Type field for Basic compatibility and Compressed frames. Compressed responses contain a 64-bit bitmap and occupy 32 bytes with FCS. Both BAR profiles occupy 24 bytes with FCS. Basic producers must keep reserved bits that overlap Type zero. Invalid Types and malformed fragment fields now fail validation instead of ambiguous decoding. + +56. IEEE 802.11 RIFS permission + + HT Operation now preserves RIFS Mode through management conversion and equality checks. The MIB permits RIFS under unprotected HT operation and clears the permission for VHT. A recipient can therefore check the actual advertised permission before it recognizes a RIFS relation. This change adds no RIFS transmission scheduler. + +57. IEEE 802.11 reversible DATA preparation + + QoS data services now supply an owned DATA preview before queue extraction or sequence assignment. A successful commit transfers that same preview to the caller. A refused commit preserves the preview and original queue frames. Custom services must implement the preparation contract in the migration guide. The existing extraction path remains available. + + Commitment of ordinary DATA now preserves normal queue service effects. Queue output statistics count the departure, and the retained preview includes elapsed queueing time and queue events. This also applies when a deferred head causes selection of a later eligible frame. + +58. 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. + +59. IEEE 802.11 partial HT aggregate reception + + NIST and Yans receivers now retain separate delimiter and MPDU outcomes for HT20 aggregates. Partial DATA loss preserves aggregate bytes for later MAC acceptance. This change does not enable MAC aggregate receipt or implicit Block Ack. Custom HT error models must implement the optional WLAN aggregate contract. Aggregate error-rate estimates can be unknown. The migration guide describes the contract and metadata changes. + + Rx now drops aggregate containers before standalone FCS or MAC header checks. This prevents parser errors for successful and partially failed aggregates. PHY range outcomes remain available in the drop signal. MAC aggregate acceptance remains pending. + + 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. + +60. IEEE 802.11 HT A-MPDU and implicit Compressed Block Ack + + 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. + + 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. + + 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, QoS data services, Rx, Tx, recipient services, ACK policies, and response procedures require the migration changes. NIST and Yans use cached range outcomes. 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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics @@ -504,12 +592,16 @@ Notable backward compatible changes are the following: 14. Explicit historical Basic Block Ack selection - HCF adds blockAckVariant = "basic" for historical compatibility. Existing Block Ack, TXOP, QoS, and fragmentation demonstrations select it explicitly. Basic remains the default. HCF rejects unknown values and the reserved "compressed" value during initialization until Compressed exchanges exist. These checks also apply when Block Ack support is disabled. Basic originated in IEEE Std 802.11e-2005; IEEE Std 802.11-2024 reserves its type value. + HCF retains blockAckVariant = "basic" for historical compatibility. Existing Block Ack, TXOP, QoS, and fragmentation demonstrations select it explicitly. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Basic originated in IEEE Std 802.11e-2005; IEEE Std 802.11-2024 reserves its type value. 15. IEEE 802.11 retry counters for each receiver QoS recovery now keeps separate retry counters for each receiver. Frames with equal TID, sequence number, and fragment number no longer share a counter across receivers. An acknowledgment or retry-limit disposal for one receiver preserves the other receiver's counter. This repair applies before DATA lifetime expiration. +16. Optional selected extraction from packet queues + + PacketQueue, CompoundPacketQueueBase, and PriorityScheduler now support the optional ISelectivePacketCollection contract. A consumer can find an eligible packet without a state change. It can extract that packet with ordinary pull events, residence metadata, and ownership transfer. Ordinary head selection remains unchanged. Providers without this capability report UnsupportedPacketSelection before extraction. + Notable bug fixes are the following: 1. IEEE 802.11 HT modes preserve the requested band and preamble 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/index.rst b/doc/src/migration-guide/index.rst index 6da76178c57..cfdb577ca14 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -14,6 +14,124 @@ retransmissions do (``synRetries``). When they run out, the connection raises th ``TCP_E_TIMEOUT_CONN_ESTAB``, as the timer did before, so a subclass that handles this event needs no change. +IEEE 802.11 implicit Block Ack extension points +--------------------------------------------- + +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 topology. 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. + +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 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 +------------------------------------ + +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. + +IEEE 802.11 HT A-MPDU PHY reception +--------------------------------- + +HT receivers require a present error model to implement ``IIeee80211AmpduErrorModel``. This optional WLAN contract provides pure mode-support queries and probabilities for bounded PSDU ranges. ``Ieee80211ErrorModelBase`` supplies the implementation for NIST and Yans. Custom HT models outside this base must implement the C++ contract and its NED module interface. Non-HT models require no aggregate role. + +An absent error model remains valid. The receiver retains its acquisition, sensitivity, attempt, and SNIR checks without error draws. This includes ordinary HT operation with ``Ieee80211UnitDiskRadio``. The aggregate profile covers scalar and dimensional HT20 reception; it adds no unit-disk aggregate support claim. + +The receiver checks role presence at physical-layer initialization. Before the MIB update, the MAC supplies its exact mode set, channel widths, and spatial-stream limit for receive coverage validation. The model must support the modes that these inputs advertise, including HT20 short GI. Unsupported modes outside the effective stream limit cause no rejection. Runtime mode-set changes also check role presence. Any later capability-update caller must validate its exact inputs before the MIB update. + +BerTable retains its current limit. Its NED type rejects the radio's HT operation string. The allowed ``n`` value reaches its initializer, which also rejects HT. Both failures precede the new role check. BerTable does not gain HT aggregate support. Supported HT configurations can use NIST or Yans. + +The new role diagnostic applies to otherwise valid custom HT models without the contract. The current packet-level BerTable reception path also requires a transmission bit model. Scalar packet-level transmissions do not supply that model. This existing limitation remains outside the HT A-MPDU change. + +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. + +``Ieee80211AmpduInd`` carries copied PHY facts without reception or transmission pointers. Each range identifies a delimiter or complete MPDU by PSDU-relative byte offset, byte length, validity, and physical success. Spacing delimiters have four-byte ranges. Alignment padding has no range outcome or separate draw. MAC delimiter and FCS checks still control acceptance; the PHY indication alone grants no delivery or response permission. + +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 an incomplete layout. Extraction alone grants no MAC acceptance. + +The medium cache retains each aggregate decision. PREAMBLE calculates no HEADER or DATA outcome. HEADER, DATA, WHOLE, and direct success queries reuse retained prerequisites and samples. WHOLE summarizes all ranges. Partial DATA failure preserves the aggregate bytes for MAC member validation. The PHY indication alone does not authorize MAC receipt or an implicit Block Ack response. + +With a present model, aggregate ``ErrorRateInd`` fields contain ``NaN`` for unknown probabilities. The receiver does not calculate the former whole-PSDU estimate for these fields. Member outcomes are separate from probability estimates. Without a model, the receiver preserves the common base's tag presence and values. Ordinary MPDUs retain their current metadata. Custom readers of aggregate error-rate fields must accept unknown values. + +NIST and Yans calculate each range probability from its byte length and the configured scalar SNIR reduction. The receive profile includes HT20 long GI and short GI. Each range uses an independent draw when its success probability lies strictly between zero and one. This model does not represent correlated FEC errors or interference changes within one member. Alignment padding contributes duration but receives no separate outcome. The model checks establish no new error-accuracy or goodput claim. + +Radio medium timing queries +-------------------------- + +Custom radio media must implement ``getTransmissionTimingSnapshot()`` and ``computePrecedingSignalGap()``. These queries supply copied physical endpoints from actual transmissions and cached arrivals. Unavailable endpoints produce an absent result. The snapshot must not retain transmission, arrival, or radio pointers. Communication caches must implement the const ``findCachedArrival()`` query without insertion or other mutation. + +``SignalTimeInd`` now carries the medium's transmission identifier. The receiver supplies this local indication when it accepts a transmission. Consumers can associate later frame delivery with the corresponding physical timing evidence. The identifier is local metadata; it adds no transmitted field. + +IEEE 802.11 signal extension +--------------------------- + +Custom IEEE 802.11 modes must implement ``getSignalExtensionTime()``. ``Ieee80211ModeBase`` supplies zero. ERP and 2.4 GHz HT modes supply their six-microsecond extension. HT duration now includes that extension, which can change airtime and simulation results. The transmission query derives the actual extension from its waveform, so consumers must not add the nominal value again. + +IEEE 802.11 RIFS permission +-------------------------- + +HT Operation now preserves RIFS Mode through management conversion and equality checks. VHT operation clears that permission. The change preserves the received permission for a consumer that checks RIFS relations; it adds no RIFS transmission scheduler. + +IEEE 802.11 Block Ack wire producers +----------------------------------- + +The serializer decodes the complete four-bit Type field. Type 0 retains Basic compatibility; Type 2 selects Compressed. Other Types produce invalid input. Basic producers must keep the two low bits of their reserved field zero because those bits overlap Type. The serializer rejects inconsistent Basic objects. Compressed reserved fields write zero; received reserved values do not affect the decoded result. + +Compressed producers must supply Fragment Number zero and a 64-bit bitmap. A Compressed Block Ack header chunk contains 28 bytes before FCS. Both BAR profiles occupy 24 bytes with FCS. Keep the corresponding header chunk and trailer lengths consistent. The serializer rejects malformed wire fields before runtime processing. + +IEEE 802.11 reversible DATA preparation +------------------------------------- + +Custom QoS data services must implement the reversible preparation operations in ``IOriginatorQosMacDataService``. ``prepareFrame()`` creates an owned preview and retains original MSDUs in their queue. ``isPreparationValid()`` checks current source identity, eligibility, lifetime, sequence state, and aggregation permission. ``commitPreparedFrame()`` transfers the same preview pointer only after successful validation. Failure must preserve the preview and all original queue frames. ``InProgressFrames::adoptPreparedFrame()`` accepts a committed frame without another extraction. + +IEEE 802.11 Compressed Block Ack extension points +------------------------------------------------ + +HCF defaults to ``mac.hcf.blockAckVariant = "compressed"``. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. + +Compressed agreements require usable local HT support and directional peer HT state. An HT PHY mode alone does not supply that state. Without it, HCF retains Normal Ack DATA. Explicit ``blockAckVariant = "basic"`` retains historical Basic compatibility without that HT requirement. Compressed agreements suppress fragmentation for their peer and TID. Fragmentation outside those agreements retains its existing threshold. + +Compressed supports immediate Block Ack only. Keep ``delayedAckPolicySupported = false`` in both agreement policies. HCF rejects either true value during initialization when Compressed support is enabled. Basic retains its existing delayed-policy configuration contract. + +Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. HCF supplies the request's actual position in the TXOP. Select a legal non-HT mode from the applicable BSS or peer rates. A configured ``controlFrameBitrate`` must satisfy that rule. The existing Basic rate-selection methods remain available. + +Custom recipient ACK policies must implement ``computeCompressedBlockAckDurationField()``. This operation subtracts SIFS and the actual Compressed response airtime from the request reservation. Compressed Block Ack contains 32 bytes with FCS; Basic contains 152 bytes. Retain the existing Basic operation. + +Custom recipient data services must accept ``bool interframe = false`` in ``dataFrameReceived()``. The value states whether HCF established the required SIFS or RIFS relation. A false value supplies no interval evidence. Preserve that value through the recipient reorder path. Custom callers that omit it retain false. + +Custom frame sequences must supply HCF's resolved profile to ``QoSContext``. Its constructor retains Basic for callers that omit the profile. Custom agreement creation must set the recipient agreement's variant before use. An agreement must retain that variant throughout its lifetime. + +Custom frame sequences must preserve BAR arbitration before DATA preparation. The HCF preparation callback distinguishes the legacy path, accepted preparation, and admission refusal. Refusal must end the sequence without a DATA failure. Keep the selected DATA mode through RTS/CTS. Validate that decision without another adaptive rate query. Admit the complete exchange before source removal. + +The active Compressed path reserves only the current exchange under single protection. A later BAR can require fresh contention. Before DATA commitment, failed RTS updates QSRC and the contention window without a provisional sequence counter. QSRC is the short retry counter for an access category. QSRC rollover does not discard queued DATA. Original lifetimes bound repeated RTS attempts. + +Basic and outside-agreement fallback retain their existing preparation and recovery paths. + +IEEE 802.11 Basic Block Ack extension points +------------------------------------------ + +HCF previews the next eligible DATA before BAR selection. Preview must preserve queue ownership and the next sequence number. Custom QoS data services must implement ``IOriginatorQosMacDataService``. This contract extends ``IOriginatorMacDataService`` with ``peekEligibleFrame()``, ``getNextSequenceNumber()``, and ``hasAMsduAggregation()``. + +Basic single protection reserves only an already prepared successor. HCF ends the TXOP before further DATA extraction. A queued unit can change size or require agreement setup before transmission. Custom frame sequences must preserve this boundary when they use the same forecast. + +Custom QoS ACK policies must add ``IOriginatorBlockAckAgreementHandler *agreements`` to ``isBlockAckReqNeeded()`` and ``computeBlockAckReqParameters()``. The argument supplies current agreement windows. Policies must keep peer and TID identities separate. The supplied policy can request BAR without a DATA candidate. + +Callers must supply the agreement handler explicitly through both the interface and concrete policy types. An omitted argument fails compilation. Pass ``nullptr`` explicitly when no agreement handler is available; the supplied policy then requests no BAR. Custom overrides must also require the argument. + +Calls to ``QosAckHandler::processTransmittedBlockAckReq()`` must supply the originator agreement explicitly. The agreement restricts processing to the negotiated window. An explicit ``nullptr`` retains bitmap-only coverage without that restriction. Production HCF always supplies the active agreement. + +Custom ``InProgressFrames`` classes must add ``bool extract = true`` to their ``getPendingFrameFor()`` override. When ``extract`` is false, the method must return a borrowed candidate without extraction or sequence assignment. The default value preserves ordinary extraction. Custom candidate sources must also implement ``peekFrameToTransmit()`` without extraction. + TCP Algorithms -------------- @@ -158,6 +276,51 @@ The protected ``getEarliestCompleteMsduOrAMsduIfExists()`` helper remains availa No configuration change is necessary. Gap release, window movement, and BAR before DATA can change delivery times and fingerprints in affected Basic Block Ack scenarios. These repairs do not enable Compressed Block Ack. +IEEE 802.11 Block Ack Agreement Lifecycle +------------------------------------------ + +HCF now defers new DATA extraction for a peer and TID during Block Ack setup or recovery. Already numbered DATA resolves before HCF creates ADDBA. The first DATA under an accepted agreement therefore retains the request sequence number. Both supported profiles use this lifecycle contract. + +The recipient response now determines the buffer size and inactivity timeout. The built-in recipient selects the smaller of 64 and its configured maximum. A request size of zero supplies no size guidance. The response copies the request dialog token. Both negotiated A-MSDU flags must permit aggregation under an active agreement. + +The originator starts ``mac.hcf.originatorBlockAckAgreementPolicy.addbaFailureTimeout`` after the request receives its ACK. Its default is 100 ms. The value must be positive and finite. + +If the response does not arrive, HCF sends DELBA before it releases surviving unsent DATA. DATA lifetime separately limits retained DATA. Failed DELBA disposes of the current deferred batch. Later DATA starts one fresh recovery exchange. + +At ordinary teardown, the originator discards every transmitted, unacknowledged MPDU for the selected agreement. This includes DATA whose lifetime remains valid when inactivity expires. It also discards unsent fragments through the highest transmitted allocation index, even if an earlier fragment received an ACK. This local index records allocation order before fragmentation. Later unsent DATA retains sequence continuity. + +For example, fragment 0 of sequence 4095 enters transmission before teardown. Teardown discards its unsent fragments and preserves the intact successor with sequence 0. The recipient delivers complete buffered units in cyclic order before it releases resources. It discards incomplete units. For example, complete units 101 and 102 can reach the upper process after teardown across absent unit 100. + +Custom originator code must call ``recordTransmittedData(allocationIndex)`` instead of ``markFirstDataTransmitted()`` or the former cyclic-sequence overload. The QoS data service assigns an increasing local index before fragmentation. All fragments retain this index in ``Ieee80211DataSequenceTag``. Custom QoS data services must supply the same metadata. The agreement exposes its highest transmitted index through ``getHighestTransmittedDataIndex()``, which replaces ``getHighestTransmittedSequence()``. HCF records this index when the transmitter accepts DATA. + +The index remains available after ACK processing removes a frame. Teardown compares allocation indices to remove unsent fragments of a partially transmitted unit. This comparison remains valid across sequence gaps and wraparound. For example, transmission of sequences 0 and 2048 updates the boundary correctly when intervening DATA receives allocation indices but never transmits. Retransmission of older DATA cannot move the boundary backward. + +Association reset instead discards all buffered units from the old association. Management calls ``IBlockAckAgreementLifecycle::resetPeerAgreements()`` with the old peer address before replacement. The MAC implements this contract and forwards the reset to HCF. Custom management implementations must supply this handoff for association replacement and loss. A rate-state notification alone cannot identify replacement with identical capabilities. + +With Block Ack support enabled, peer reset also discards queued and in-progress QoS DATA addressed to the old peer. This includes unsent Normal Ack DATA without an agreement. Accepted physical exchanges retain their context until safe completion. Reset preserves other peers' queued DATA and lifetime records. + +Custom MAC implementations that use these management modules must implement ``IBlockAckAgreementLifecycle``. The reset must complete both agreement roles' cleanup before it reports deletion. It must preserve an unrelated management exchange that already holds an accepted transmission. + +``blockAckAgreementDeleted`` now supplies a nonnull agreement after removal from lookup and completion of the required cleanup. The payload identifies the selected role through its agreement class. The payload remains valid only during the synchronous callback. Listeners must not retain or modify it. Association reset removes both roles before the first callback. Ordinary directional teardown preserves the opposite role. + +Custom QoS recipient services must return accepted, complete Actions from ``managementFrameReceived()``. HCF owns their agreement dispatch after duplicate checks. Services must implement directional retirement and peer cleanup through the recipient-service contract. Custom agreement policies must implement ``isProfileAllowed()`` with the supplied typed profile and usable capability inputs. Basic admission must preserve its historical non-HT compatibility. + +Custom agreement handlers must implement the new procedure and retirement operations in their handler contracts. The originator handler owns setup intent, request identity, response deadlines, teardown, and recovery state. Its ``noteEligibleDataTransmission(peer, tid)`` operation records ``WAITING_FOR_SUCCESSOR`` after an eligible DATA transmission. This phase permits ordinary DATA access and has no request or deadline. A later queued successor promotes the phase to setup intent before extraction. Peer reset clears the phase. + +The recipient handler owns its active agreements and pending teardown. Detachment removes an agreement from lookup and transfers it to the caller. The caller retains the object through its deletion callback. Const observations expose current state without a second agreement store. + +Custom implementations of ``IBlockAckAgreementHandlerCallback`` must implement ``scheduleInactivityTimer()`` without a deadline argument. Agreement handlers request recomputation after their state changes. HCF queries both roles and schedules their earliest absolute deadline. It cancels the timer when neither role has a finite deadline. Custom callers must remove the old deadline argument. + +Custom subclasses of ``OriginatorQosMacDataService`` must adapt the protected ``aMsduAggregateIfNeeded(queue, selected)`` helper. The selected packet is the first eligible packet in service order. An aggregate candidate list must start with that packet. The service validates every candidate before source removal and calls the aggregation policy once. + +Custom queues need ``inet::queueing::ISelectivePacketCollection`` only when a deferred next frame hides eligible work. HCF retains ordinary dequeue when the advertised next frame is eligible. It waits when every frame is deferred. It reports the missing capability before mutation when eligible work requires a bypass. + +The optional ``peekEligiblePacket(predicate)`` operation returns a borrowed packet in configured service order without mutation. ``extractPacket(packet)`` transfers that exact packet to its caller. Implementations must preserve their ordinary pull effects, including ownership, residence metadata, events, statistics, and applicable animation. Built-in simple queues, priority schedulers, and compound queues support this contract. Custom subclasses that change pull effects must adapt selected extraction. The optional contract supports complete packets only. + +Providers that cannot perform a selected operation must throw ``inet::queueing::UnsupportedPacketSelection``. This exception permits ordinary service and collection inspection without the optional capability. Genuine queue and predicate errors must retain their original type. + +External frame-sequence handlers must implement ``interruptFrameSequence()``. The handler owns the interruption outcome after recipient processing. It suppresses failure feedback when the transmitted DATA or management frame no longer belongs to the in-progress queue. Other exchanges retain ordinary failure feedback. Cleanup must destroy the exchange context before it deletes deferred frame copies. These repairs do not add MAC stop/start recovery or final-transfer teardown. + IEEE 802.11 Response Query Roles -------------------------------- diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index f4569882f96..01b9dd836f9 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -533,7 +533,7 @@ The first exchange proceeds even when its cost exceeds a positive TXOP limit. Th A zero limit permits the initial exchange and its existing RTS/CTS and immediate response. Each later fragment or unrelated unit needs a fresh TXOP. Prepared fragments and A-MSDUs use the same complete-cost check under positive limits. -``isBlockAckSupported = true`` disables complete-cost continuation admission, even without a Block Ack agreement. Multiple protection also retains its separate admission policy. Both paths use shared TXNAV. Basic Block Ack admission, BAR forecasts, zero-limit companion rules, and optional overrun exceptions remain outside this repair. +With Block Ack support enabled, active Compressed agreements use the admission described in :ref:`ug:sec:80211:blockack`. Basic and traffic outside those agreements retain their existing admission policy. Those retained paths can exceed DATA exchange bounds. All paths use shared TXNAV. Basic Block Ack admission and BAR forecasts remain outside the Compressed correction. All single protection paths classify group frames before the current-frame ACK equation. With Block Ack support disabled, the current one-frame group sequence advertises zero. With support enabled, its legacy forecast can reserve a queued successor that the sequence cannot transmit. This limit applies to group management and non-QoS DATA too. @@ -607,13 +607,9 @@ The standard separately requires AIFSN of at least 1 for APs and at least 2 for example, a non-AP station with AIFSN 1 meets the ownership condition but violates the standard's minimum. -Multiple protection does not check a complete exchange against the remaining budget. -For example, its admission policy can start a 120 us exchange with only -100 us left. The reference scope assumes whole-microsecond positive limits, integral initial PPDU -airtime, and integral legacy RTS airtime. It adds no A-MPDU, Compressed Block Ack, new -fragmentation, CTS-to-self, dual CTS, PIFS recovery, or TXOP sharing. It also adds no reverse -direction, beamforming, S1G, multi-user, or general MAC lifecycle support. It does not repair -general rate selection, recipient response timing, or TXOP overruns. +Outside active Compressed agreements, multiple protection does not check complete exchange cost against the remaining budget. For example, that retained policy can start a 120 us exchange with only 100 us left. Active Compressed agreements use their separate complete-cost admission. The reference scope assumes whole-microsecond positive limits, integral initial PPDU airtime, and integral legacy RTS airtime. + +Multiple protection 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: @@ -646,17 +642,47 @@ 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 also leaves A-MPDU transmission disabled. The HT policy below enables A-MPDU transmission with implicit Compressed Block Ack. .. code-block:: ini **.wlan[*].mac.qosStation = true **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 3000 +.. _ug:sec:80211:htampdu: + +HT A-MPDU and implicit Block Ack +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +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 + + **.wlan[*].opMode = "n(mixed-2.4Ghz)" + **.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" + +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. + +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. + +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 supply cached per-range outcomes under scalar SNIR reduction. This abstraction does not model correlated FEC errors or interference changes within a member. 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: Acknowledgement and retries @@ -700,16 +726,35 @@ station then sets up an agreement with its peer automatically (with ADDBA frames) when it sends a QoS data frame shorter than :par:`maxBlockAckPolicyFrameLength` (default 1000 bytes). After that, the frames of that traffic class go out without an immediate acknowledgement, -and the station sends a Block Ack Request after :par:`blockAckReqThreshold` -(default 5) unacknowledged frames. Both parameters are on -``mac.hcf.originatorAckPolicy``. +and the station normally sends BAR after :par:`blockAckReqThreshold` unacknowledged frames. The default threshold is five. Both parameters are on ``mac.hcf.originatorAckPolicy``. + +The station applies the threshold separately to each peer and TID. It sends BAR below the threshold when the next eligible DATA belongs to another agreement or cannot fit the transmit window. The station also sends BAR when no eligible DATA remains. For example, a burst of four frames ends with BAR even with the default threshold of five. A negotiated buffer size of one requires BAR before the second sequence number. + +BAR can obtain channel access without a queued DATA frame. A later BAR within a TXOP must fit the applicable TXOP and TXNAV bounds with its response. If BAR cannot fit, the station retains that work for another contention. + +After the final exchange, the originator sends DELBA when its agreement has no queued DATA, in-progress DATA, or active BAR exchange. A zero inactivity timeout does not disable this transfer-completion rule. DATA that arrives after this boundary waits for DELBA completion. A later transfer can require another ADDBA exchange. If the last unresolved DATA expires, the originator uses the same teardown path. Ordinary recipient teardown releases complete units behind a receive gap before it releases agreement resources. + +The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size. ``maximumAllowedBufferSize`` defaults to 64. Both negotiated ``aMsduSupported`` flags control A-MSDU use; each defaults to ``true``. ``blockAckTimeoutValue`` defaults to ``0s``, which disables the inactivity timeout. Transfer completion and DATA lifetime expiry retain their separate teardown rules. + +HCF defaults to ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. + +Compressed agreements require usable local HT support and directional peer HT state. Infrastructure association supplies that peer state for eligible HT links and the supported ``ac`` path. An HT radio mode alone does not supply peer capabilities. Without usable state, DATA uses Normal Ack. Local non-HT operation produces a warning when Block Ack support is enabled. HT ad hoc links without peer state retain Normal Ack. + +Explicit ``blockAckVariant = "basic"`` selects historical Basic compatibility on both endpoints. Basic retains its 64 fragment bitmap entries and its 152-byte response with FCS. IEEE Std 802.11e-2005 introduced this format. IEEE Std 802.11-2024 reserves its Type 0 value. INET retains Basic for existing studies; this compatibility does not establish complete historical conformance. + +Both endpoints must select the same profile because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Configure matching profiles before a new run. Malformed frames follow input rejection instead of this configuration diagnostic. + +Compressed agreements suppress fragmentation for their peer and TID. The recipient rejects fragmented DATA under those agreements before receipt or reorder updates. Basic retains fragmentation. Outside a Compressed agreement, the configured fragmentation threshold retains its existing effect. + +Keep ``delayedAckPolicySupported = false`` in both agreement policies for Compressed support. HCF rejects true at initialization because the Compressed path supplies immediate responses only. Retained Basic delayed-policy configuration does not establish a complete delayed exchange implementation. + +The Compressed originator sends BAR with a non-HT PHY mode. Initial BAR uses the applicable BSS basic or mandatory rate rule. Later BAR uses the peer operational rate rule. A configured ``controlFrameBitrate`` must satisfy that rule. The response selector uses the actual 32-byte response length when a legal alternate rate is available. These rules retain separate Basic, ACK, and CTS contracts. + +Active Compressed agreements check the complete DATA exchange before source commitment. If an aggregate cannot qualify, HCF evaluates its first ordinary MSDU with the same selected DATA mode. If neither form qualifies, HCF ends the sequence without queue removal, sequence assignment, or deadline replacement. RTS/CTS retains the selected mode. Expiry or capability changes before DATA cancel invalid preparation. + +An initial unaggregated MSDU can use the applicable TXOP overrun exception. A same-size single-MPDU retransmission can also use its exception. That retransmission can carry an A-MSDU. Each exception permits at most one Data or Management frame in that TXOP. A new A-MSDU must fit a positive limit. A zero limit permits one DATA PPDU with one MSDU or A-MSDU, plus its required protection and response. -The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and -``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size -(:par:`maximumAllowedBufferSize`, default 64), the A-MSDU support -(:par:`aMsduSupported`, default ``true``) and the inactivity timeout -(:par:`blockAckTimeoutValue`, default ``0s``, which means no timeout). Only -the basic Block Ack variant is modeled. +Single protection reserves only the current Compressed exchange. A later BAR can require fresh contention. This policy can change airtime and delivery times. Before DATA commitment, failed RTS updates QSRC and the contention window without a provisional sequence counter. QSRC is the short retry counter for an access category. Original MSDU lifetimes bound repeated attempts; QSRC rollover does not discard them. .. code-block:: ini @@ -1026,9 +1071,9 @@ The model does not contain: - PCF and HCCA, the polled channel access methods. The module :ned:`Hcca` exists as a placeholder. -- A-MPDU aggregation, the MSDU lifetime limit, and self-CTS protection. +- HT40 transmit aggregation, VHT aggregates, and self-CTS protection. -- Compressed and multi-TID block acknowledgement. +- Multi-TID block acknowledgement and delayed Compressed Block Ack. - Operational HT widths above 20 MHz, and a primary/secondary channel relationship for wider VHT modes. diff --git a/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" diff --git a/plan/done/ieee80211-compressed-blockack-s3b-execution.md b/plan/done/ieee80211-compressed-blockack-s3b-execution.md new file mode 100644 index 00000000000..b8076b55859 --- /dev/null +++ b/plan/done/ieee80211-compressed-blockack-s3b-execution.md @@ -0,0 +1,279 @@ +# S3b execution record + +## Completed S3b + +S3b is complete on branch `work/compressed-blockack-s3b` in [PR #1343](https://github.com/inet-framework/inet/pull/1343). The current source revision is `8646a9e6808ea8d21ac28bfc1271537469225fe9`, above PR base `bba1db4c8180a592894e0ab2a2dc056d9cb08082`. The broad verification below covers `8ba7c9e47bc3ded924cfa3917c8b899baccf789d`; a separate record covers the later allocation-index repair. Branch completion does not assert a merge into `master`. S4, A5, and production Compressed exchanges remain outside this completed stage. + +The six implementation commits supply optional selected extraction, lifecycle integration, first-DATA collision retirement, frame-sequence interruption ownership, fragment retirement, and allocation-index comparisons. A separate documentation commit records stage completion. The [S3b summary](ieee80211-compressed-blockack-s3b-summary.md) explains the resulting behavior. The [migration guide](../../doc/src/migration-guide/index.rst) defines custom-module changes. Basic remains the default; HCF rejects Compressed selection during initialization. + +The latest repair retains the highest transmitted allocation index in the agreement. The QoS data service assigns this local index before fragmentation; all fragments retain it in `Ieee80211DataSequenceTag`. Ordinary teardown discards unsent fragments through that index, even after ACK processing removes an earlier fragment. Older retransmissions cannot move the index backward, and wire sequence numbers remain unchanged. + +The fragment fixture transmits DATA with sequence 0. It allocates intervening DATA through the production data service and deletes that DATA without transmission. Fragment 0 of sequence 2048 then transmits. The index advances correctly; teardown removes unsent siblings of sequence 2048 and preserves later intact DATA. This case tests an allocation gap; it does not test lifetime expiry of the intervening DATA. + +| Verification at source revision `8ba7c9e47bc3ded924cfa3917c8b899baccf789d` | Result | Saved local artifact | +| --- | --- | --- | +| Debug compilation | Pass | `/tmp/pr1343-publish-debug.log` | +| Release compilation | Pass | `/tmp/pr1343-publish-release.log` | +| Selected debug module fixtures | 26 pass | `/tmp/pr1343-publish-modules.log` | +| Functional fingerprint cases, without graphical `tyf` ingredients | 19 pass | `/tmp/pr1343-publish-fingerprints.log` | +| Fragment reproduction against `53edefe95a6cc9671ef8d4864c913c2a5669306d` | Fails at the retirement assertion | `/tmp/pr1343-fragment-before.err` | +| Fragment fixture after the repair | Four scenarios pass | `/tmp/pr1343-fragment-after.log` | +| Commit, classification, include, parameter, and PR-description checks | Pass | `/tmp/pr1343-publish-mechanical.log` | +| Source-seal check | Pass | `/tmp/pr1343-publish-seals.log` | + +The commands for this earlier verification run from the repository root after environment setup: + +```sh +make MODE=debug -j12 +make MODE=release -j12 +inet_run_module_tests -m debug -f '(PacketSelectiveExtraction_1|Ieee80211BlockAck.*_1|Ieee80211EdcaDataLifetime.*|Ieee80211HcfInternalCollision_1)\.test$' +``` + +The functional fingerprint command runs from `tests/fingerprint`: + +```sh +./fingerprinttest -d -F tyf -t 4 -m 'showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)/|examples/wireless/qos/.*MacQos|examples/adhoc/qos/.*MacQos' examples.csv showcases.csv +``` + +Seven approved functional fingerprint rows belong to the lifecycle implementation. Later repairs retain those values and add no baseline change. Twelve controls retain their functional expectations. Graphical expectations remain unchanged and outside this functional result. The earlier verification at `667ae51dc9fbfbfac9ea68122bf65bc4f2d3e1df` includes 37 module fixtures, six unit fixtures, and 13 Basic demonstrations. Its protocol selection records four passes and four declared expected failures. Those earlier results do not establish final-head coverage for the same selections. + +At `8ba7c9e47b`, whole-project architecture and naming checks retain the findings present at the PR base. The interface check retains 15 findings and nine review notes; the two new interfaces pass. The changed source paths are unsealed. Raw `/tmp` artifacts are local evidence; they are not part of the published repository. + +### Allocation-index repair at `ea20a2880e` + +The series now places the allocation-index repair before the final plan commit. The repair maps from `ea20a2880e0ca607e680def1a875c6702c4ef249` to `8646a9e6808ea8d21ac28bfc1271537469225fe9` without source or test changes. The results below retain their original revision. The history comparison establishes content equivalence; it is not a new behavioral test. + +Commit `ea20a2880e0ca607e680def1a875c6702c4ef249` replaces cyclic comparisons with allocation indices. It adds a MSG definition for local metadata, without a wire-format change. Custom QoS data services must supply the tag, as the migration guide requires. + +| Recorded verification for the allocation-index repair | Result | Saved local artifact | +| --- | --- | --- | +| Debug compilation | Pass | `/tmp/s3b-push-debug.log` | +| Release compilation | Pass | `/tmp/s3b-push-release.log` | +| Fragment-retirement, lifecycle, association-reset, and DATA-selection fixtures in debug mode | Four fixtures pass | `/tmp/s3b-push-tests.log` | + +The commit records 50 successful simulation runs across these four fixtures. The fragment fixture supplies 20 runs with these parameters: + +- The initial sequence is 0 or 4095. +- The allocation distance is 0, 2047, 2048, 2049, or 4096. +- Teardown occurs during transmission or after the first fragment receives its Normal Ack. + +The fixture supplies accepted agreements and ACKs; production code performs fragmentation, expiry, retirement, and transmission. Later intact DATA and unrelated-peer DATA remain controls. + +These archived results do not extend the earlier 26-fixture or 19-fingerprint results to this revision. Release-mode behavioral tests, sanitizers, and other platforms remain unverified at this revision. The later audit checks the repair and documentation claims through source inspection; it is not a full independent correctness audit. + +This document correction changes no production source or recorded expectation. It repeats no build or simulation test. The checkout's existing `AGENTS.md`, four pending plans, and planning history remain under their existing owners. + +## Earlier execution snapshot + +The sections below retain the earlier implementation record, before publication and later repairs. Their candidate numbers and source pins refer to that earlier work. Their statements about absent commits, release checks, or baseline approval apply only to that snapshot. The completed-stage results above supersede those status statements. + +The original implementation parent is `41eee1d61978d62cfaf9d0d9c242b9fee590c20e`. The branch is `work/compressed-blockack-s3b`. The working copies of replacement plan revision 39 and lifecycle plan revision 29 define that scope. The user authorized S3b execution on 15 October 2026. + +## Implementation contract + +[HCF](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) owns agreement coordination and frame exchange order. The [originator](../../src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc) and [recipient](../../src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc) handlers own their agreements and transient procedures. The MIB owns capability state. Management supplies an explicit peer reset before association replacement. The [recipient data service](../../src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc) owns duplicate checks and delivery. Queue components own extraction effects. + +The implementation retains Basic compatibility without an HT admission requirement. HCF continues to reject Compressed selection during initialization. Isolated policies verify Compressed admission with explicit capability inputs. S3b does not implement S4 BAR arbitration, A5 transfer completion, or Compressed exchanges. + +1. A1 returns complete accepted Block Ack Actions from the recipient data service. HCF dispatches each Action once after duplicate checks. A correlated Action resolves its procedure before obsolete exchange failure. +2. A2 passes the selected profile to admission policies. The handlers correlate peer, TID, and dialog token. Accepted response values determine both agreement owners' parameters. The data service enforces both A-MSDU flags. +3. A3a detaches the selected agreement before cleanup and deletion notification. Ordinary recipient teardown delivers complete buffered units in cyclic order. It discards incomplete units. HCF uses one absolute inactivity deadline across both roles. +4. A3b clears both roles and obsolete peer work before deletion notifications. Management supplies the old peer identity before association replacement. The MIB keeps its boolean notification and coherent committed state. +5. A3c routes Action disposal to its owner. HCF invalidates obsolete queued and in-progress Actions. An accepted transmission retains its context until safe completion. Valid DATA or BAR without an agreement causes recipient-role DELBA. +6. A4 defers extraction during setup and recovery. The data service supplies a non-consuming sequence query. Optional selected extraction preserves queue ownership, residence metadata, and pull events. Contention uses the same eligibility condition. + +HCF obtains both roles' earliest active absolute deadlines after each agreement mutation. It schedules their minimum. It cancels the timer when neither role has a finite deadline. Expiration detaches every overdue agreement before the next deadline calculation. + +The delayed-successor interpretation preserves the existing setup trigger across separate DATA arrivals. A completed eligible Normal Ack transmission supplies the first condition. A later unexpired unnumbered successor supplies the second condition. The handler retains the first condition in its procedure map without an Action or timer. HCF promotes that state to setup intent before successor extraction. A single frame without a successor therefore causes no ADDBA. + +The implementation checks peer, TID, direction, modulo-4096 sequence order, and absolute versus relative time at each changed boundary. Duplicate Actions retain immediate ACK behavior. Invalid responses cannot resolve setup. Queue disposal, retry failure, response timeout, inactivity, received DELBA, and association reset have separate terminal paths. No MAC stop/start recovery claim applies. + +## Source assessment + +The parent recipient data service deletes Actions before it returns its accepted frames. HCF nevertheless dispatches the original management header after that call. Thus, duplicate detection does not control agreement dispatch. During a response wait, HCF passes an addressed Action to the active receive step instead of its agreement handler. + +The parent policies accept unchecked setup parameters. The recipient stores request size and timeout before it builds a response. The response omits the request dialog token. The originator policy asserts that an agreement exists but performs no response correlation. + +Both handlers supply absolute inactivity deadlines. HCF passes those deadlines to `rescheduleAfter`. The handlers select expiration with equality instead of an overdue comparison. Recipient DATA reception does not refresh the stored deadline. Received recipient DELBA uses the local receiver address for removal. + +The parent collision controller tests frame counts. This test does not establish agreement eligibility. A4 requires a production reproduction or a cancellation trace before a shared arbitration change. + +## Verification status + +The separate lifecycle review accepted the A1–A4 scope against the pinned parent. It required the shared inactivity minimum and explicit peer reset before equal-state reassociation. The review supplied source evidence; it supplied no implementation or runtime verdict. + +The parent debug build passed with `make MODE=debug -j12`. The command ran from the repository root and returned zero. Its log is `/tmp/s3b-parent-build.log`. + +The parent reproduction command was `inet_run_module_tests -m debug -f 'Ieee80211BlockAck(ActionDelivery|ActionDispatch|Negotiation)_1.test'`. Its log is `/tmp/s3b-parent-reproductions.log`. The Action delivery fixture failed its returned-frame count assertion. The negotiation fixture failed its dialog-token assertion. Both failures reached the production component after test initialization. + +The HCF dispatch fixture initially lacked a received-mode indication and a valid request sequence number. Those fixture setup errors supplied no product evidence. After correction, `inet_run_module_tests -m debug -f 'Ieee80211BlockAckActionDispatch_1.test'` reproduced duplicate dispatch. The fixture required one dispatch after an original request and its MAC retry. The parent dispatched both. Its log is `/tmp/s3b-parent-dispatch.log`. + +The generated dispatch executable also ran independently with `-u Cmdenv -r 1` from `tests/module/work/Ieee80211BlockAckActionDispatch_1`. At 1 ms, the fixture supplied a 5 ms absolute deadline. The parent scheduled 6 ms. The arrival-time assertion failed with exit status one. Its log is `/tmp/s3b-parent-inactivity.log`. + +New fixtures cover directional cleanup, association reset, response-wait order, deferred extraction accounting, and contention. Existing Basic recipient, selector, header, queue-disposal, lifetime, capability, and exchange fixtures remain controls. The candidate results below identify their commands and evidence limits. + +The parent control command selected nine module fixtures and passed all nine. Its command was `inet_run_module_tests -m debug -f 'Ieee80211(BlockAck(Reordering|Variant)|HcfHeader|RecipientNav|HcfMultipleProtection_2|SingleProtectionGroup|MgmtApHcfQueueDrop|MgmtRatePublication|MgmtStateCommit|EdcaDataLifetimeArbitration)_.*test'`. Its log is `/tmp/s3b-parent-controls.log`. This expression did not select the multiple-protection fixture because that alternative already contained its suffix. + +A second command selected that fixture and the Basic exchange control: `inet_run_module_tests -m debug -f 'Ieee80211(HcfMultipleProtection_2|BasicCompatibility_1)\.test'`. Both fixtures passed. Its log is `/tmp/s3b-parent-basic-controls.log`. Together, these results cover eleven fixtures, including all eight S1 recipient scenarios. + +The parent library hash was `ed6af79596a80e6192f4ca1cb8bf91f8a5f682d508eef58e95603796aa384a6e`. The lifecycle and response-wait fixtures also reproduced nine parent failures with that library. Directional teardown emitted a null deletion payload. Absent-agreement DATA reached upper delivery. Absent-agreement BAR caused no DELBA. An incoming Action received no immediate ACK during an active request ACK wait. `/tmp/s3b-regression-parent/` retains the exact executables, inputs, hashes, and individual run logs. Later fixture changes require fresh compilation before candidate verification. + +The selected protocol command was `inet_run_protocol_tests -p inet -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|N_CompressedBa|N_Amsdu|Ac_BlockAck|WifiQosDelayedBa|Ac_MultiTidBa|N_Ampdu|Ac_Ampdu)\.test$'`. It returned zero with four passes and four declared expected failures. Its log is `/tmp/s3b-parent-protocol.log`. This run used the unchanged parent library while source implementation began. The protocol support library includes no changed agreement or HCF declaration. A new unused timeout parameter declaration was present; this result does not establish candidate behavior. + +The delayed-policy fixture failed its immediate-ACK assertion after BAR. The Multi-TID fixture missed its response deadline. Both A-MPDU fixtures missed their subframe-header observation. `/tmp/s3b-parent-reproductions/protocol-failure-causes.json` preserves these first failure causes. These outcomes remain unresolved protocol obligations. + +## Additional production reproductions + +Candidate 6 exposed two A4 failures before the final contention change. A real VO transmission held an ADDBA Request in its queue. A later VO arrival evicted that Request. HCF cleared setup, but surviving BE DATA obtained no new contention request. Action-disposal case 6 reproduced this failure without another BE arrival. `/tmp/s3b-actiondisposal-build6-reproduction/` retains the evidence. + +A separate fixture expired a VI agreement at 33 microseconds. Its queued DATA became ineligible before coincident VI and BE grants at 34 microseconds. VO had its next grant at 70 microseconds. The controller still counted VI and charged BE a collision retry. With retry limit one, BE discarded its valid DATA. `/tmp/s3b-deferred-collision-before-isolated.log` records this isolated failure. + +The first contention trace also contained a legitimate VO grant at 34 microseconds. That trace supplies no evidence for the shared correction. The corrected fixture changes only VO AIFSN to six. Its eligible-VI control retains the real collision and retry disposal. Both corrected cases use seed zero and the same binary and library. + +HCF now supplies each EDCAF with a query for current frame eligibility. The collision controller uses this query before it selects contenders. The query performs no expiration, extraction, or protocol mutation. HCF retains the existing Basic readiness condition; S4 still owns BAR-only work. After an Action terminal outcome, HCF requests contention for every category with eligible work. + +Candidate 7 passes both corrected contention cases. The deferred-VI case transmits BE DATA without a collision or retry disposal. The eligible-VI control retains its collision and BE retry disposal. Late Request eviction and successful DELBA acknowledgment also release BE DATA without another BE arrival. `/tmp/s3b-deferral-selection-collision-final.log` and `/tmp/s3b-regression-build7-pass/` retain these results. + +Candidate 7 also reproduces an Action recovery-state defect. An owned ADDBA Request causes a real RTS transmission under a low RTS threshold. The missing CTS charges its short retry counter before any Action transmission. Peer reset then leaves that counter at one because Action-attempt state remains false. `/tmp/s3b-actiondisposal-build7-rts-reproduction/` retains the exact failure. Cleanup must distinguish an untouched Action from an Action whose RTS exchange owns retry state. + +The retained `MacQosWithBlockAck` demonstration exposes a separate setup trigger defect. A-MSDU aggregation drains all queued DATA for each AP transmission. The next same-TID arrival occurs after that transmission, so the immediate successor check never starts AP setup. Candidate 7 therefore supplies only the client-to-AP Basic link. The required AP-to-server link remains absent. The delayed-successor state addresses this condition without an empty setup request or changed traffic. + +## Controlled setup measurements + +The deferral fixture compares response timeouts of 100 ms and 250 ms under equal traffic, rates, seed, and response queue residence. It holds the real recipient response in its queue for approximately 150 ms. It does not inject a response directly. Both cases use three DATA packets and seed zero. DATA uses 6 Mbps; Actions use 24 Mbps. + +| Observed interval or count | 100 ms timeout | 250 ms timeout | +| --- | --- | --- | +| Request queue admission to PHY start | 34 microseconds | 34 microseconds | +| Request ACK reception | 0.000684020016 s | 0.000684020016 s | +| Request ACK to accepted response | No accepted response | 150.070003336 ms | +| Response queue residence | 100.080006672 ms before invalidation | 150.078003336 ms before transmission | +| DELBA queue admission to ACK completion | 80.006672 microseconds | No DELBA | +| Surviving DATA PHY start | 0.100780026688 s | 0.150832023352 s | +| Recipient MAC receives surviving DATA | 0.100980030024 s | 0.151032026688 s | +| Transmitted management frames | One Request and one DELBA | One Request and one Response | +| Management airtime | 72 microseconds | 72 microseconds | +| ACK airtime for management exchanges | 56 microseconds | 56 microseconds | + +The 100 ms deadline starts recovery before the queued response becomes eligible. The recipient invalidates that response when DELBA arrives. DELBA acknowledgment releases surviving DATA under Normal Ack. The 250 ms deadline permits the delayed response and releases DATA under Block Ack. That final DATA has no BAR within the fixture, so MAC reception does not establish upper delivery. + +`/tmp/s3b-build11-timing-100ms.log` and `/tmp/s3b-build11-timing-250ms.log` contain the exact events and PHY durations. `/tmp/s3b-build11-setup-timing.json` preserves source lines and event numbers. The fixture does not independently timestamp the transition into setup deferral. These observations establish timeout order and separate exchange costs; they establish no delivery guarantee or preferred timeout. + +Application measurements use the unchanged Block Ack showcase, General configuration, run zero, seed zero, and a fixed 0.5-second interval. The source sends 5,000 application packets of 700 bytes at 0.1 ms intervals. The debug parent delivers 330 packets, or 231,000 useful bytes. Final debug candidate 11 delivers 324 packets, or 226,800 useful bytes. Application goodput is therefore 3.696 Mbps and 3.6288 Mbps, respectively. + +Both application runs use `inet --debug -u Cmdenv -c General -r 0 --seed-set=0 --sim-time-limit=0.5s --record-vector-results=false --record-scalar-results=true --cmdenv-express-mode=true`. Their result directories are `/tmp/s3b-metric-parent-debug` and `/tmp/s3b-metric-candidate-11`. `opp_scavetool export -F CSV-R` exports both scalar files into `/tmp/s3b-metric-results-final.csv`. Useful bytes come from the destination application's `packetReceived:sum(packetBytes)` scalar. Earlier runs without an explicit mode supply no matched-mode comparison. + +The comparison parent checkout is `9b4b22258fff87401943d5046e0d18c18b70a558`. Its source and feature declarations equal the implementation parent. Its ignored build profile differs in unrelated optional features. This one fixed-seed comparison establishes only the observed application counts. It establishes no general performance improvement, optional-retry benefit, or agreement-reuse benefit. S4 retains transfer-completion and idle-gap comparisons. + +## Final source and focused verification + +Final candidate 11 contains A1, A2, A3a, A3b, A3c, and A4. HCF retains Basic as the default and rejects Compressed during initialization. The source review found no remaining correctness defect or unresolved architecture exception. No commit, publication, recorded baseline change, or seal change forms part of this execution. + +The final debug build command is `make MODE=debug -j12`. It returns zero; `/tmp/s3b-candidate-build-11.log` records the build. The library SHA256 is `f1ec379bd582e9c84b87e548059068231f9381d0dbd67bc6c4908f9e174e4608`. `/tmp/s3b-final-source-manifest.json` identifies all 72 changed source, test, and migration files, including untracked headers. Its parent remains `41eee1d61978d62cfaf9d0d9c242b9fee590c20e`. + +The final implementation separates Action transmission from ownership of RTS retry state. An RTS failure records ownership of the short retry counter. It does not mark the Action as transmitted. Invalidation and every terminal Action outcome clear that owned counter. A separate control preserves another peer's counter when an untouched Action uses the same Sequence Control field. The existing shared recovery map still uses that field without the peer address; this change does not redesign that map. + +The delayed-successor cases now defer later individual DATA and later A-MSDU batches before extraction. Both negotiate starting sequence number one and transmit first DATA with number one. The single-frame controls send no ADDBA. Real AP reassociation removes the waiting state under both legacy and HT configurations. A lone frame in the new association cannot inherit the old trigger. + +| Final debug selection | Result | Evidence | +| --- | --- | --- | +| Related module fixtures and existing controls | 29 fixtures pass | `/tmp/s3b-final-modules-6.log` | +| Lifecycle, response wait, association reset, Action disposal, retired DATA | Five fixtures; 50 scenarios pass | `/tmp/s3b-regression-build11-runner.log`, `/tmp/s3b-regression-build11-pass/` | +| Deferred selection, setup, lifetime, collision controls | Three fixtures; 18 scenarios pass | `/tmp/s3b-build11-deferral-selection-collision.log`, `/tmp/s3b-build11-*` | +| Related unit fixtures | Six fixtures pass | `/tmp/s3b-final-unit-4.log` | +| Retained Basic demonstrations | 13 cases pass | `/tmp/s3b-final-demonstrations-11/evidence.json` | +| Selected protocol requirements | Four pass; four declared expected failures | `/tmp/s3b-final-protocol-4.log` | + +The module selections total 37 distinct fixtures. The lifecycle and deferral snapshots retain generated code, configuration, executables, output, and hashes. Their simulation outputs contain no runtime warnings, errors, or undisposed objects. Runner notices about optional Python packages do not describe simulation failures. All cases use debug mode; deterministic new scenarios use seed zero. + +The [migration guide](../../doc/src/migration-guide/index.rst) explains the changed custom-module contracts. The [selected-extraction contract](../../src/inet/queueing/contract/ISelectivePacketCollection.h) defines queue ownership and pull effects. The [Action disposal fixture](../../tests/module/Ieee80211BlockAckActionDisposal_1.test) covers queue loss and RTS cleanup. The [deferral fixture](../../tests/module/Ieee80211BlockAckDeferral_1.test) covers timeout order and delayed successors. The [collision fixture](../../tests/module/Ieee80211BlockAckDeferredCollision_1.test) separates deferred contention from a genuine collision. + +The exact root module selector is: + +```sh +inet_run_module_tests -m debug -f '(Ieee80211BlockAck(ActionDelivery|ActionDispatch|Admission|Negotiation)_1|PacketSelectiveExtraction_1|Ieee80211(BasicCompatibility_1|BlockAckVariant_1|BlockAckReordering_1|HcfHeader_1|HcfMultipleProtection_[12]|SingleProtectionGroup_1|RecipientNav_1|PreparedCancellation_1|Txnav_1|HcfSingleProtection_1|VhtHtSupport_1|MgmtApHcfQueueDrop_1|MgmtRatePublication_1|MgmtStateCommit_1|EdcaDataLifetime.*))\.test$' +``` + +The additional module and unit selectors are: + +```sh +inet_run_module_tests -m debug -f 'Ieee80211BlockAck(Lifecycle|ResponseWait|AssociationReset|ActionDisposal|RetiredData)_1.test' +inet_run_module_tests -m debug -f 'Ieee80211BlockAck(DataSelection|Deferral|DeferredCollision)_1\.test' +inet_run_unit_tests -m debug -f 'Ieee80211(HtModeSet_1|VhtHtCapabilities_1|ResponseModeSelection_1|MibPeerTeardown_1|QosDuplicateRemoval_1|BlockAckActionWire_1)\.test$' +``` + +All category commands run from the repository root. The Basic observer command is `python3 tests/module/basic-compatibility-evidence.py --output /tmp/s3b-final-demonstrations-11`. It retains the original topologies, traffic, aggregation settings, fragmentation cases, parameter studies, and Basic assertions. The AP-to-server link now supplies real setup, DATA, BAR, and positive receipt bits. This result closes the two-hop regression without a weaker assertion. + +The final protocol command repeats the eight-case parent selector exactly. Its runner returns zero with four expected failures. Delayed-policy verification now fails at step one because admission refuses delayed policy. Its parent first failed at the later immediate-ACK requirement. Multi-TID still misses step two; both A-MPDU cases still miss the step-zero subframe-header requirement. + +`/tmp/s3b-final-protocol-failure-causes.json` preserves those first failure causes. Their expected-failure markers remain unchanged. These results establish no delayed, Multi-TID, A-MPDU, or Compressed support. The passing `N_CompressedBa` fixture still exercises the explicit Basic compatibility path in this stage. + +Scoped architecture checks pass for `src/inet/linklayer/ieee80211` and `src/inet/queueing`. Their logs are `/tmp/s3b-final-architecture-wifi.log` and `/tmp/s3b-final-architecture-queue.log`. `check-source-seals.sh --diff` passes; `/tmp/s3b-final-source-seals.log` records the result. Every changed source target is unsealed. `git diff --check` also passes for the owned changes. + +The independent gate comparison uses the actual implementation parent. Wi-Fi naming retains the same 32 findings; queue naming retains the same one finding. Wi-Fi interfaces retain the two existing findings under `AV-CONTRACT-02`; queue interfaces pass. New compliant interfaces increase those counts from 48 to 49 and from 24 to 25. `/tmp/s3b-review-{parent,candidate}-{naming,interfaces}-{0,1}.log` retains both sides; zero denotes Wi-Fi and one denotes queueing. No new mechanical finding or exception-ledger change is required. + +The C++ checker could not execute its analysis for either subtree. Both `check-cpp.sh` calls return two because this checkout has no `compile_commands.json`. `/tmp/s3b-final-cpp-wifi.log` and `/tmp/s3b-final-cpp-queue.log` retain that prerequisite error. These calls supply no clang-tidy verdict. This execution supplies debug compilation and source review; release compilation and the full pre-push gates remain outside this unpublished work. + +## Queue traversal cost + +Candidate 9 exceeded the CPU limit in both ten-second ad hoc fingerprint cases. A bounded Block Ack-disabled comparison reached the same event 104692 at 0.8 seconds. The parent used 3.56 CPU seconds; candidate 9 used 7.56 CPU seconds. Source inspection identified a new membership check that continued after it found each aggregation candidate. + +An external counter measured indexed queue reads without a repository edit. The captured production stack reached that membership check through the normal QoS extraction path. Each indexed read starts at the linked-list head. The full scan therefore repeated increasingly long walks for every candidate. This cost also affected traffic with Block Ack disabled. + +| Same-event profile | Parent | Candidate 9 | Candidate 10 | +| --- | --- | --- | --- | +| Indexed queue reads | 861 | 861,869 | 2,605 | +| Sum of requested indices | 1,285 | 775,865,982 | 2,142 | +| Largest requested index | 3 | 3,114 | 3 | + +Candidate 10 stops each membership search at its first match. The service still checks every candidate's presence, uniqueness, eligibility, and A-MSDU permission before any source removal. The repair adds no state or interface. The final profile retains event 104692 and removes high-index traversal from this run. Its plain run uses 4.24 CPU seconds; concurrent host load limits interpretation of that timing. + +`/tmp/s3b-baoff-profile-parent.log`, `/tmp/s3b-baoff-profile-candidate.log`, and `/tmp/s3b-baoff-profile-candidate10.log` retain the profiles. `/tmp/s3b-queue-profile.cc` supplies the external counter. The source review accepts the bounded early-break correction. Candidate 10 passed its focused checks after that correction. Candidate 11 retains the same early break. + +The Block Ack-enabled profile then found another repeated indexed scan in HCF's DATA existence query. At 0.8 seconds and event 102123, candidate 10 made 7,894,493 indexed reads. Candidate 11 makes 12,236 reads at the same event. Time inside high-index traversal falls from 14.37 seconds to 0.061 seconds. `/tmp/s3b-baon-profile-candidate10.log` and `/tmp/s3b-baon-profile-candidate11.log` retain these profiles. + +HCF now uses the optional predicate query to find unexpired DATA for a peer and TID. PacketQueue traverses its linked queue with an iterator. A composite reports `UnsupportedPacketSelection` when its provider cannot supply that query. HCF catches only that outcome and retains its collection fallback. Unrelated queue or predicate errors still propagate. This adds no capability cache or protocol state. + +The data service first checks the ordinary advertised head at both selection points. An eligible head therefore needs no selected-extraction capability. If all frames defer, the service waits without mutation. If a deferred head hides eligible work, the service requires selected extraction before it changes ownership or sequence state. The three new composite cases verify these distinctions through a provider without the optional interface. + +## Accepted exchanges across reset + +The final reset checks also repair physical statistics and RTS-protected identity. Candidate 10 omits EDCAF sent or received observations after logical retirement, although accepted DATA and its ACK complete physically. The final HCF emits each physical observation once before the retired-state guard. That guard still suppresses obsolete acknowledgment, recovery, agreement, and rate effects. The three DATA completion cases verify one sent observation and one received ACK observation. + +A separate reproduction resets the peer after RTS acceptance and before DATA submission. The old DATA and its lifetime leave active ownership. After CTS, candidate 10 nevertheless accepts that retired DATA because it checks no active-frame membership. Its management sequences instead select again, which can produce null or another peer's frame. `/tmp/s3b-retired-rts-build10-reproduction/` retains six clean failure cases. + +Candidate 11 retains the original protected management pointer after CTS, as the DATA sequence already does. HCF refuses DATA or management submission when that packet no longer belongs to active in-progress work. The accepted RTS exchange can finish safely, but it cannot authorize a retired packet or substitute another peer's packet. All six reset-during-RTS cases pass with no post-reset protected submission. They also preserve the other peer's DATA lifetime. + +The independent review closes every reported source finding in candidate 11. Its general checklist reports 20 passes and 14 inapplicable items. Its WLAN checklist reports 12 passes and one inapplicable item. Neither checklist retains a flag or question. The review adds no exception-ledger or seal change. + +## Final fingerprint comparison + +All 19 final debug cases complete without a CPU-limit error. The repository expectations yield one pass and 18 mismatches. The actual parent yields four passes and 15 graphical-only mismatches. Twelve final cases match every calculated parent ingredient. Seven Basic cases change functional ingredients; the table below identifies them. + +| Case | Calculated `tplx` | Calculated `~tNl` | Calculated `~tND` | Calculated `tyf` | +| --- | --- | --- | --- | --- | +| Ad hoc `MacQos`, run 1 | `5fbb-9fd9` | `c0a4-97f6` | `2373-df01` | `49b6-dbab` | +| Infrastructure `MacQosWithBlockAck`, run 0 | `f8a4-9474` | `1cba-cccc` | `538e-67df` | `69e0-5af3` | +| Block Ack `Fragmentation`, run 0 | `021a-d29a` | `1597-ec6a` | `feff-7e88` | Not selected | +| Block Ack `MixedTraffic`, run 0 | `5b9e-801f` | `8731-2504` | `9724-d1ce` | Not selected | +| Block Ack `NoFragmentation`, run 0 | `b828-6bd6` | `026f-2c6d` | `2cd4-4795` | Not selected | +| Fragmentation `HCFfragblockack`, run 0 | `f509-65f1` | `6b3a-0d65` | `c474-7174` | `4cfb-b3c7` | +| TXOP `General`, run 0 | `f58d-d2fd` | `38f7-f642` | Not selected | `e520-9a34` | + +Paired production traces confirm the first changed exchanges. In `NoFragmentation`, both versions transmit DATA 0 at 0.210 ms and complete it at 0.496 ms. Both also number DATA 1 through the existing successor forecast. The parent immediately requests starting number one. The candidate first resolves numbered DATA 1 under Normal Ack, then requests starting number two. This is A4's required drain-before-setup rule. + +The TXOP trace likewise changes the request number from two to three after numbered DATA resolves. The fragmentation trace completes the numbered fragmented unit before ADDBA. The parent starts setup after fragment zero. The ad hoc trace changes the request number from two to three after numbered Voice fragments resolve. The infrastructure trace suppresses empty-successor setup after ARP and starts setup when later WWW DATA arrives. The retained demonstrations still show successful Basic exchanges, positive receipt bits, and fragment bits where required. + +`/tmp/s3b-fp-first-divergences.json` and `/tmp/s3b-fp-qos-first-divergences.json` retain times, events, commands, fields, and source lines. These paired windows establish the changed setup boundaries. They do not attribute every later counter difference to one mechanism. The final 19 ingredients equal candidate 10 exactly, so the final cost and reset repairs add no further fingerprint change. + +The parent command ran from its own `tests/fingerprint` directory after its `setenv` file. It used debug mode, four threads, and the same 19-case selector. `/tmp/s3b-parent-fingerprints-2.log` retains its complete result. `/tmp/s3b-parent-fingerprint-inventory.json` preserves its calculated ingredients before later jobs overwrote shared result paths. Interrupted invocations and overwritten shared outputs supply no comparison evidence. + +The final candidate uses private copies of the unchanged input CSV files in `/tmp/s3b-final-fingerprint-inputs-11`. `input-hashes.json` records their byte hashes. Older jobs cannot replace these comparison inputs or outputs. The exact command runs from `tests/fingerprint`: + +```sh +PYTHONUNBUFFERED=1 ./fingerprinttest -d -t 4 -m 'showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)/|examples/wireless/qos/.*MacQos|examples/adhoc/qos/.*MacQos' -l /tmp/s3b-final-fingerprint-inputs-11/runner.log /tmp/s3b-final-fingerprint-inputs-11/examples.csv /tmp/s3b-final-fingerprint-inputs-11/showcases.csv +``` + +`/tmp/s3b-candidate-fingerprints-6.log` records all 19 results and the nonzero mismatch status. `/tmp/s3b-fingerprint-comparison-build11.json` records the parent and candidate 10 comparisons. `/tmp/s3b-final-verification.md` supplies the regression analysis. The seven calculated rows above are evidence, not approved baseline values. Every recorded repository expectation remains unchanged. + +The separate capability observer also passes all eight inventory cases on candidate 11. Its command is `python3 tests/module/basic-compatibility-evidence.py --protocol-inventory --output /tmp/s3b-final-protocol-inventory-11`. The evidence file records actual HT state independently of the protocol assertions. Passing inventory observations do not convert the four expected protocol failures into supported features. diff --git a/plan/done/ieee80211-compressed-blockack-s3b-summary.md b/plan/done/ieee80211-compressed-blockack-s3b-summary.md new file mode 100644 index 00000000000..c09a1e343e9 --- /dev/null +++ b/plan/done/ieee80211-compressed-blockack-s3b-summary.md @@ -0,0 +1,251 @@ +# S3b: safe Block Ack agreement setup and teardown + +> **Kind:** reference · **Status:** snapshot 2026-10-15 · **Seal:** none · **Owns:** — · **Stands on:** [replacement summary](../pending/ieee80211-compressed-blockack-replacement-summary.md), [lifecycle plan](../pending/ieee80211-blockack-agreement-lifecycle.md), [execution record](ieee80211-compressed-blockack-s3b-execution.md) + +S3b repairs the agreement lifecycle that Compressed Block Ack will need. It controls when DATA can use an agreement, how agreement Actions change state, and how cleanup affects queued and received DATA. The immediate production target is Basic Block Ack. Production HCF still rejects Compressed selection at initialization. + +S3b is complete on branch `work/compressed-blockack-s3b` in [PR #1343](https://github.com/inet-framework/inet/pull/1343). The current source revision is `8646a9e6808ea8d21ac28bfc1271537469225fe9`, above base `bba1db4c8180a592894e0ab2a2dc056d9cb08082`. The [completion record](ieee80211-compressed-blockack-s3b-execution.md#completed-s3b) separates broad verification at `8ba7c9e47b` from focused verification of the later allocation-index repair. This status records branch completion; it does not assert a merge into `master`. The diagrams explain implemented behavior; they do not represent measured frame traces. + +## Why this stage exists + +A Block Ack agreement contains the shared parameters that permit Block Ack operation between an originator and a recipient. The originator sends DATA; the recipient receives DATA. ADDBA establishes the agreement through a request and response. DELBA ends the agreement through an Action frame. A BAR is a control frame that requests a Block Ack response. + +Safe DATA transmission requires more than a correct Block Ack bitmap. Both stations must agree on the parameters and on whether an agreement exists. Retries, lost responses, queue disposal, inactivity, and association replacement can change that state. S3b gives each event an owner and a defined effect on DATA. + +The parent reproductions expose concrete causes. The recipient service deletes accepted Actions, while HCF dispatches the original header without the duplicate result. The recipient response omits the dialog token that identifies its request. HCF also treats an absolute inactivity deadline as a relative delay. The [execution record](ieee80211-compressed-blockack-s3b-execution.md#source-assessment) gives the source assessment and reproduction limits. + +```mermaid +flowchart LR + P["S1, S2, R1, S3a
Recipient progress, DATA lifetime,
management rates, profile selection"] + L["S3b
Agreement lifecycle
A1 through A4"] + B["S4
BAR-only channel access
and transfer completion"] + C["S5: C1 through C4
Compressed frame format,
exchanges, timing checks, default"] + P --> L --> B --> C +``` + +S3b supplies the lifecycle foundation. S4 supplies the final BAR exchange and transfer-completion behavior. S5 supplies complete Compressed exchanges and their verification. The [replacement summary](../pending/ieee80211-compressed-blockack-replacement-summary.md) owns this stage order. The replacement and lifecycle plans remain pending because their S4 and S5 work remains incomplete. + +## Who owns the state + +Agreement identity includes the local role, peer, and traffic identifier (TID). One station can hold both roles for the same peer and TID. These records describe opposite DATA directions. Teardown of one role must preserve the other role. + +| Component | Responsibility in S3b | +| --- | --- | +| HCF, the hybrid coordination function | Coordinates agreement Actions, frame exchanges, timers, DATA eligibility, and cleanup order. | +| Originator agreement handler | Owns originator agreements, setup procedures, response deadlines, and recovery state. | +| Recipient agreement handler | Owns recipient agreements and recipient teardown procedures. | +| Agreement policies | Check setup parameters and profile admission. | +| Originator QoS data service | Selects eligible DATA, checks A-MSDU permission, and assigns sequence numbers after extraction. | +| Recipient QoS data service | Checks duplicates, returns accepted Actions, and prepares received DATA for delivery or disposal. | +| Reorder owner | Holds received DATA behind sequence gaps and supplies ordered complete units at teardown. | +| Queue components | Own extraction, ownership transfer, queue residence metadata, and pull events. | +| Management and the MIB | Management identifies association reset. The management information base (MIB) owns capability and rate state. | + +The [execution contract](ieee80211-compressed-blockack-s3b-execution.md#implementation-contract) defines these responsibilities. HCF coordinates the existing owners; queues do not keep a second copy of agreement state. + +## A1: apply each accepted Action once + +HCF must distinguish reception of an Action from acceptance of that Action for agreement processing. A valid MAC retry still needs its immediate ACK. It must not create another agreement or apply DELBA twice. The recipient data service therefore returns only complete, nonduplicate Actions for HCF dispatch. + +```mermaid +flowchart TD + RX["Addressed ADDBA or DELBA arrives"] --> ACK["Retain immediate ACK behavior
for valid received frames"] + ACK --> CHECK["Recipient data service checks
complete-frame state and duplicates"] + CHECK -->|Duplicate| STOP["No agreement dispatch"] + CHECK -->|Accepted complete Action| RETURN["Return Action to HCF"] + RETURN --> OWNER["Agreement owner checks identity
and applies the Action once"] +``` + +This diagram separates decisions; it does not prescribe ACK timing relative to dispatch. An Action during an active response wait needs an additional order rule. HCF first lets the agreement owner determine whether that Action resolves the pending procedure. HCF then ends the obsolete exchange without an obsolete terminal failure. The retained valid Action still receives its ACK. + +Hypothetical example: the peer receives ADDBA Request, but the request ACK is lost. A matched successful ADDBA Response arrives during the request's final ACK wait. The originator accepts the response before request-failure notification. Cleanup must not turn that success into teardown, DATA disposal, or another request retry. An unrelated Action does not suppress the interrupted exchange's ordinary failure result. + +The [Action contract](../pending/ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) owns this order. The [HCF source](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc) contains dispatch and response-wait integration. The frame-sequence handler owns the interruption outcome after recipient processing. It suppresses obsolete failure feedback for retired frames. An Action attempt that Tx already accepts retains its context until safe completion; logical retirement does not permit immediate destruction of that context. + +## A2: use the parameters that both stations accept + +An ACK for ADDBA Request confirms request reception. It does not establish that the originator accepted an agreement. The originator activates its agreement only after a matched successful ADDBA Response passes validation. The response must match the pending peer, TID, and dialog token. It must also specify immediate policy and supported parameters. + +```mermaid +sequenceDiagram + participant O as Originator + participant R as Recipient + O->>R: ADDBA Request: TID, token, first sequence number + R-->>O: Immediate ACK for request + Note over R: Validate request and build accepted response + Note over R: Create recipient agreement from response values + R->>O: ADDBA Response: same token, accepted parameters + Note over O: Match peer, TID, token, and parameters + Note over O: Activate originator agreement + O-->>R: Immediate ACK for response + O->>R: DATA under the accepted agreement +``` + +The built-in recipient policy chooses `min(64, maximumAllowedBufferSize)` as the accepted buffer size. The request size gives guidance; it does not cap that choice. Both agreement owners use the response timeout value for inactivity. A zero response timeout disables inactivity expiration. The recipient keeps the request's first sequence number for its reorder state. + +Hypothetical example: the request suggests eight positions, but the recipient configuration permits 64. The response advertises 64, so both agreement owners use 64. The recipient must not retain eight while the originator uses 64. A rejected modification request leaves the existing agreement unchanged. + +A-MSDU permission also belongs to negotiation. An A-MSDU combines multiple MSDUs within one MPDU. Both the request and response flags must permit it before the originator constructs an A-MSDU under the agreement. If either flag denies permission, individual DATA can still use Block Ack. Normal Ack must not bypass a denial for an A-MSDU that already exists. + +Basic admission preserves non-HT and legacy ad hoc compatibility. Compressed admission requires local HT operation and valid peer HT state for the relevant direction. S3b checks Compressed admission through isolated policies; this does not enable production Compressed exchanges. The [negotiation contract](../pending/ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) defines these conditions and policy choices. + +## A3: end the correct agreement without stale work + +A3a covers directional teardown and inactivity. A3b covers peer reset. A3c covers Action disposal and recovery when no agreement exists. These are review units within one lifecycle contract. The first retirement path must also invalidate obsolete Actions that could later change the retired state. + +### Directional teardown preserves the opposite role + +For received DELBA, HCF uses the transmitter address as the peer. The Initiator field identifies which local role must end. If the peer sends DELBA as originator, the local recipient agreement ends. If the peer sends DELBA as recipient, the local originator agreement ends. + +Ordinary recipient teardown first removes the agreement from lookup. The recipient data service then preserves duplicate state and obtains complete buffered units in cyclic sequence order. HCF delivers those units before the deletion event and resource release. Incomplete Basic units undergo disposal. Association reset uses a different policy: it discards old buffered units. + +```mermaid +flowchart TD + START["Recipient agreement ends"] --> DETACH["Remove agreement from lookup
and invalidate obsolete Actions"] + DETACH --> KIND{"Association reset?"} + KIND -->|No| ORDER["Collect complete buffered units
in cyclic sequence order"] + ORDER --> DELIVER["Deliver complete units once
and discard incomplete units"] + KIND -->|Yes| DISCARD["Discard old buffered units
without upper delivery"] + DELIVER --> EVENT["Emit deletion event
and release agreement resources"] + DISCARD --> EVENT +``` + +Hypothetical Basic example: sequence 100 is absent, while complete units 101 and 102 remain behind the gap. Ordinary recipient teardown delivers 101 followed by 102 before cleanup. Acknowledgment of those units alone does not establish upper delivery. This release across a gap is an INET policy choice. The [migration guide](../../doc/src/migration-guide/index.rst) explains the implemented teardown policy. + +At ordinary originator teardown, HCF discards transmitted, unacknowledged DATA for the selected agreement. HCF also discards unsent fragments through the highest transmitted allocation index. The QoS data service assigns this local index before fragmentation; all fragments retain the same index. The agreement retains the highest transmitted index after ACK processing removes an earlier fragment. Allocation indices preserve order across large sequence gaps and wraparound; older retransmissions cannot move the boundary backward. Later intact DATA and another peer's DATA remain eligible. + +The fragment regression supplies a concrete example. Fragment 0 of sequence 4095 enters transmission before inactivity expires. HCF discards its unsent siblings at teardown and preserves the intact successor with sequence 0. The fixture also checks expiration after fragment 0 receives its Normal Ack. A separate collision regression checks disposal of the first DATA before transmission; HCF retires that agreement before its successor can use it. + +```mermaid +flowchart LR + F0["Sequence 4095, fragment 0
Transmission accepted"] --> E["Inactivity expires
Retire originator agreement"] + E --> D["Discard unsent fragments
of sequence 4095"] + E --> K["Keep intact successor
with sequence 0"] + E --> P["Keep another peer's DATA"] +``` + +### Two timers serve different purposes + +The ADDBA response timer bounds setup after the request receives its Normal Ack. The inactivity timer bounds an active agreement without relevant activity. S3b uses separate timers because setup uncertainty and agreement inactivity require different state transitions. + +| Timer | Start or refresh condition | Expiration effect | +| --- | --- | --- | +| ADDBA response timer | Request ACK starts that peer/TID deadline. The default INET interval is 100 ms. | HCF starts teardown because the peer can hold an agreement. | +| Agreement inactivity timer | Agreement activation starts its deadline. Relevant traffic refreshes it. | HCF retires each overdue agreement and starts DELBA. | + +HCF schedules the earliest absolute deadline across both agreement roles. It cancels the inactivity timer when no finite deadline exists. Expiration selects deadlines at or before the current time, so a late callback does not miss an agreement. + +```mermaid +flowchart LR + O["Originator role
earliest active deadline"] --> MIN["HCF selects the minimum
absolute deadline"] + R["Recipient role
earliest active deadline"] --> MIN + MIN -->|Finite deadline| AT["Schedule at that time
or now if overdue"] + MIN -->|No finite deadline| CANCEL["Cancel inactivity timer"] +``` + +Hypothetical timing example: the current time is 1 ms and the absolute deadline is 5 ms. HCF schedules expiration at 5 ms. A relative delay of 5 ms would incorrectly schedule 6 ms. The execution record reports this exact parent failure. The [timeout contract](../pending/ieee80211-blockack-agreement-lifecycle.md#response-timeout-and-data-lifetime) distinguishes the INET response timeout from DATA lifetime. + +### Peer reset clears both roles before callbacks + +Association replacement ends the old association even when the replacement publishes identical rates and HT capabilities. A rate-change notification alone cannot identify that event. Management therefore passes the peer identity to HCF before replacement. HCF clears both roles and old peer work before the first agreement deletion callback. + +```mermaid +sequenceDiagram + participant M as Management + participant H as HCF + participant A as Agreement owners + participant D as DATA and Action owners + participant V as Deletion observers + M->>H: Reset old peer before association replacement + H->>A: Detach both roles and pending procedures + H->>D: Invalidate old Actions and clear old peer DATA + H->>D: Discard old receive buffers and duplicate state + H->>V: Emit agreement deletion events + Note over V: Neither old role remains visible + M->>M: Complete association state replacement +``` + +With Block Ack enabled, reset discards the old peer's queued and in-progress QoS DATA, including unsent Normal Ack DATA without an agreement. Another peer retains its agreements, DATA, and lifetime records. The MIB continues to publish coherent committed state through its existing boolean notification. The [association-reset contract](../pending/ieee80211-blockack-agreement-lifecycle.md#reassociation-consumers-and-callback-order) explains why the separate reset handoff is necessary. + +An accepted physical exchange retains its context until completion. Physical statistics still record that exchange, but its callbacks cannot submit retired DATA after CTS. The frame-sequence handler destroys the context before it deletes deferred frame copies. + +### Queue loss and uncertain setup require explicit recovery + +HCF tracks each locally generated ADDBA or DELBA Action by frame identity, peer, TID, and role. Queue rejection, eviction, retry failure, and acknowledgment reach that Action's owner. Each Action produces one terminal transition. Intentional invalidation removes its procedure identity before queue disposal can report a second failure. + +If Tx never accepts a request attempt, local cancellation can release DATA under Normal Ack. After a possible request transmission, no response does not prove that the peer lacks an agreement. HCF must defer DATA until teardown confirms a common state. A successful recipient agreement remains active if its response suffers disposal; response loss does not undo recipient acceptance. + +```mermaid +flowchart TD + P["ADDBA pending
DATA deferred"] -->|Matching success| A["Active agreement
release eligible DATA"] + P -->|Initial rejection or cancellation
before any accepted request attempt| N["No agreement
Normal Ack DATA can resume"] + P -->|Attempted request fails
or response deadline expires| T["DELBA pending
DATA remains deferred"] + T -->|DELBA receives ACK| N + T -->|Originator DELBA fails
or suffers queue disposal| R["Drop deferred batch
retain recovery-needed state"] + R -->|New DATA arrives| T +``` + +S3b treats a DELBA ACK as teardown confirmation for this INET profile. This policy assumes that the peer processes a valid DELBA. Failed originator DELBA drops the current deferred batch and leaves recovery-needed state without a periodic retry timer. New DATA triggers one fresh DELBA exchange; DATA that arrives during that exchange shares its result. + +Failed recipient-role DELBA clears its teardown procedure without an originator recovery record or disposal of opposite-direction DATA. Valid Block Ack-policy DATA or a valid BAR without an agreement triggers recipient-role DELBA. That DATA does not reach upper delivery, and that BAR does not create receive state. Malformed BAR input does not trigger this recovery path. The [lifecycle contract](../pending/ieee80211-blockack-agreement-lifecycle.md#3-agreement-behavior) owns these separate outcomes. + +## A4: preserve the first sequence number and unrelated DATA access + +The ADDBA request specifies the first sequence number for the agreement. Setup must not consume that number through premature extraction. After the first eligible DATA transmission, more unnumbered DATA for that peer and TID can trigger setup intent. Already numbered DATA first completes or drops under its existing policy. HCF then queries the next sequence number without consumption and creates ADDBA Request. + +Hypothetical example: the next unassigned number is 100. HCF requests an agreement with first number 100 while the affected DATA remains queued. After a matching response, extraction assigns 100 to the first DATA. If DATA 100 suffers terminal disposal before transmission, HCF starts teardown before DATA 101 can use that agreement. Extraction alone does not count as first transmission. + +Deferral applies to one peer and TID. It must not prevent another peer, another TID, or eligible management traffic from access. The data service selects the first eligible frame in the configured queue service order. HCF uses the same eligibility condition for DATA contention. A nonempty queue that contains only deferred DATA does not justify a new DATA contention request. + +```mermaid +flowchart LR + subgraph Q["Configured queue service order"] + A["Peer A, TID 2
setup pending
deferred"] + B["Peer B, TID 5
eligible"] + C["Peer A, TID 2
deferred"] + A --- B --- C + end + B --> X["Extract B through its queue owner
and assign its sequence number"] + A -.-> W["Remain queued
with existing lifetime state"] + C -.-> W +``` + +Selected extraction must preserve ownership, queue residence metadata, pull events, and compound queue accounting. Arbitrary removal has different effects and cannot replace extraction. The optional `ISelectivePacketCollection` interface supplies an eligibility query and extraction of the selected frame. Built-in support covers `PacketQueue`, `PriorityScheduler`, and `CompoundPacketQueueBase`. + +An external queue needs this interface only when deferred head DATA hides eligible work. Its ordinary dequeue path remains usable when its next frame is eligible. If every frame is deferred, the service waits. If eligible work requires an unsupported bypass, the service reports a diagnostic before mutation. The [selected-extraction contract](../../src/inet/queueing/contract/ISelectivePacketCollection.h) defines these operations. + +Before A-MSDU construction, the service checks one proposed source list. The first source must match the selected frame; every source must remain eligible and have permission. An invalid list causes individual extraction of the selected frame. Other sources retain their queue, lifetime, and sequence state. This prevents aggregation that bypasses deferral or the configured service order. + +## Completed verification + +The [completion record](ieee80211-compressed-blockack-s3b-execution.md#completed-s3b) reports broad results for source revision `8ba7c9e47bc3ded924cfa3917c8b899baccf789d`. Debug and release compilation pass at that revision. All 26 selected debug module fixtures and 19 functional fingerprint cases pass at that revision. The functional selection excludes graphical `tyf` expectations. + +The later allocation-index repair records debug and release compilation and four debug fixtures with 50 successful simulation runs. Those fixtures cover fragment retirement, lifecycle behavior, association reset, and DATA selection. The fragment fixture supplies 20 runs across sequence gaps, wraparound, and teardown before or after ACK reception. These results belong to original revision `ea20a2880e`, which maps to `8646a9e680` without source or test changes. The history comparison establishes content equivalence; it is not new behavioral evidence. This document correction repeats no build or simulation test. + +| Verified behavior | Relevant fixtures in this checkout | +| --- | --- | +| Accepted Actions return once; duplicates retain ACK behavior | [Action delivery](../../tests/module/Ieee80211BlockAckActionDelivery_1.test), [Action dispatch](../../tests/module/Ieee80211BlockAckActionDispatch_1.test) | +| A matching Action resolves setup before obsolete exchange failure | [Response wait](../../tests/module/Ieee80211BlockAckResponseWait_1.test) | +| Accepted parameters and profile admission reach their owners | [Negotiation](../../tests/module/Ieee80211BlockAckNegotiation_1.test), [Admission](../../tests/module/Ieee80211BlockAckAdmission_1.test) | +| Directional cleanup and absent-agreement recovery preserve other state | [Lifecycle](../../tests/module/Ieee80211BlockAckLifecycle_1.test) | +| Association replacement clears both roles before observers run | [Association reset](../../tests/module/Ieee80211BlockAckAssociationReset_1.test) | +| Setup deferral preserves sequence state and unrelated progress | [Deferral](../../tests/module/Ieee80211BlockAckDeferral_1.test), [DATA selection](../../tests/module/Ieee80211BlockAckDataSelection_1.test) | +| Selected extraction preserves queue effects | [Selective extraction](../../tests/module/PacketSelectiveExtraction_1.test) | +| First-DATA disposal retires the agreement before successor transmission | [Deferred collision](../../tests/module/Ieee80211BlockAckDeferredCollision_1.test) | +| Teardown removes unsent fragments through the highest transmitted allocation index | [Fragment retirement](../../tests/module/Ieee80211BlockAckFragmentRetirement_1.test) | + +The earlier source revision `667ae51dc9fbfbfac9ea68122bf65bc4f2d3e1df` supplies broader module, unit, protocol, and Basic demonstration results. Those results remain separate from verification of the allocation-index repair. The later focused results also do not extend the earlier 26-fixture or 19-fingerprint results to the current source revision. Seven approved functional fingerprint rows belong to the lifecycle implementation; the later repairs change no baseline. Graphical expectations remain unchanged. + +The later audits inspect the repair and documentation claims; they do not establish a full independent correctness audit. The archived local results establish no release-mode behavioral tests, sanitizer results, or other-platform results for the allocation-index repair. CI results remain separate from these local records. + +## Scope limits + +S3b does not establish these later capabilities: + +- Production Compressed BAR and Block Ack exchanges, serialization, or receipt-state behavior. +- S4 BAR arbitration, BAR-only channel access, or teardown after the final Block Ack exchange. +- Compressed response-rate and airtime correctness. +- General MAC stop/start recovery. +- A performance guarantee across repeated setup and teardown. + +S3b supplies a defined agreement lifecycle with safe DATA eligibility and cleanup. Later stages can add Compressed exchanges without a separate lifecycle mechanism. Their own production evidence remains necessary. 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 new file mode 100644 index 00000000000..fac32f5f6fc --- /dev/null +++ b/plan/pending/ieee80211-ampdu-implicit-blockack-summary.md @@ -0,0 +1,378 @@ +# 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 revision 12 of the [feature plan](ieee80211-ampdu-implicit-blockack.md) and revision 11 of the [PHY plan](ieee80211-ampdu-phy-reception.md), dated 2026-10-17. Their design reference is branch `work/compressed-blockack-s5` at `4fa5cc0de15ee57388bcb3f20e866cab5972a670`. The diagrams describe the design, not executed simulation traces. + +Execution status, 2026-10-18: the user authorizes A1 through A7 on base `56b75c959af104b5db931bef022bdfe5578eead9` before prerequisite merge confirmation. Those stages are complete within the declared profile and verification limits. The user subsequently authorizes commit, push, and PR creation. The [execution record](ieee80211-ampdu-implicit-blockack-execution.md) supplies current evidence and limits. + +A0 remains open. Expected-result changes, commits, and publication require separate authority. The user separately approves removal of the historical HT aggregate test marker after its focused pass. + +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. + +Execution status, 2026-10-17: PHY P0 through P4 and gates D1 and D2 are complete under the user's approved merge exception. The [PHY execution record](ieee80211-ampdu-phy-execution.md) states the checks and limits. P3 verifies NIST/Yans range probabilities and seeded outcomes. P4 compares 576 ordinary reception records with the pinned parent. All records match exactly. The MAC execution above adds aggregate receipt and implicit Block Ack. + +Without the recorded exception, implementation waits for merged PRs [#1343](https://github.com/inet-framework/inet/pull/1343), [#1344](https://github.com/inet-framework/inet/pull/1344), and [#1345](https://github.com/inet-framework/inet/pull/1345). Both plans normally require a refreshed merged base before source implementation. The exception above permits this execution on the approved S5 base. + +## 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. HCF supplies those facts through copied timing history. 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["Merged #1343, #1344, #1345
Refresh the implementation base"] --> 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 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. + +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 user's approved revision covers these documents only. It adds no implementation approval or runtime evidence. The two linked plans own the detailed decisions, source references, standard clauses, commands, and verification limits. diff --git a/plan/pending/ieee80211-ampdu-implicit-blockack.md b/plan/pending/ieee80211-ampdu-implicit-blockack.md new file mode 100644 index 00000000000..fac347ce63f --- /dev/null +++ b/plan/pending/ieee80211-ampdu-implicit-blockack.md @@ -0,0 +1,647 @@ +# 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](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-compressed-blockack-s5`, branch `work/compressed-blockack-s5`, HEAD `4fa5cc0de15ee57388bcb3f20e866cab5972a670`. + +Plan revision: 12, 2026-10-17. The user's `proceed, including the optional simplification` authorizes the reviewed document corrections. This revision defines PSDU-based RTS selection, recovery before DATA submission, and a boolean probe requirement with a derived access category. This document accompanies PHY revision 11 and the revised summary. The delivery gates and PHY behavior retain their current scope. Implementation, expectation changes, commits, and publication remain outside this authorization. + +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. + +Implementation normally waits for merged PRs [#1343](https://github.com/inet-framework/inet/pull/1343), [#1344](https://github.com/inet-framework/inet/pull/1344), and [#1345](https://github.com/inet-framework/inet/pull/1345). The assessment on 2026-10-16 confirmed all three as open. The exception above permits A1 through A7 on the approved base. Read-only A0 checks can occur before merge. A0 cannot close until the merged dependency chain and refreshed implementation base exist. + +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 come from the pinned source. The [S5 record](ieee80211-compressed-blockack-s5-execution.md) supplies historical verification; this plan supplies no new runtime 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. The MAC also invokes HCF's corruption callback, which clears interframe history. | 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 [HCF history](../../src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h) | S5 supplies individual-MPDU interframe evidence. The reorder call accepts one interframe 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. Reuse the S5 physical timing snapshots for real PPDU intervals. HCF retains previous aggregate delimiter facts and completed Block Ack history through its current history owner. The reorder service remains the owner of delivery decisions. + +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` 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. + +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 step becomes one coherent commit or a short series 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. + +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. | Check source claims and record dependency heads now. Before implementation, require merged #1343, #1344, and #1345. Pin their integrated base and compare contracts. | Confirm the commented call, default policy, owners, and resolved choices. Record the feature-off baseline before source changes. Revise changed contracts or owners. | Read-only checks can start now. Closure requires all three merges. | +| 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. + +## 7. Verification methods and expected results + +### Proposed direct checks + +The seven fixtures below belong to this feature plan. A1 implements `Ieee80211AmpduCapabilities_1.test` with the existing association and PHY controls. The other six fixtures remain proposed. A1 also adds the unit fixtures `Ieee80211AmpduCapabilityEncoding_1.test` and `Ieee80211AmpduConstruction_1.test`. The [A1 execution record](ieee80211-ampdu-implicit-blockack-execution.md) states their coverage and limits. + +The [PHY plan](ieee80211-ampdu-phy-reception.md#7-verification-methods-and-expected-results) owns three implemented fixtures: `Ieee80211AmpduWire_1.test`, `Ieee80211AmpduPhyReception_1.test`, and `Ieee80211AmpduPhyCompatibility_1.test`. P4 adds the third fixture for parent-compatible ordinary reception comparisons. The combined inventory is twelve. The [PHY execution record](ieee80211-ampdu-phy-execution.md) states the completed PHY evidence and its 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. + +| 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 paragraphs below record the plan review on 2026-10-17. The [A1 execution record](ieee80211-ampdu-implicit-blockack-execution.md) supplies the later implementation and test evidence on its approved base. A1 completion does not establish the later MAC stages. + +The source assessment checked the commented invocation, error-model APIs, mandatory release text, HT duration table, and prerequisite PR state. These are source and standards observations. No INET build, module test, protocol test, or simulation ran for this revision. Document checks cannot establish PHY or MAC behavior. + +All 144 local links resolve across the three documents. Changed prose passed sentence-length and paragraph checks. Hash checks confirmed preservation of the seven unrelated local files. These checks establish document properties only. + +The documents contain 12 Mermaid diagrams. One diagram label now states the aggregate RTS failure rule. No parser or browser check ran for this revision. + +## 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) + +inet_run_module_tests -m debug \ + -f 'Ieee80211Ampdu(Capabilities|Preparation|Reordering|Exchange|Recovery|Admission)_1\.test$' + +inet_run_module_tests -m debug \ + -f 'Ieee80211(CompressedBlockAck.*|BlockAck.*|Edca.*Lifetime.*)_1\.test$' + +inet_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 + +( cd tests/fingerprint && ./fingerprinttest \ + -m '(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless/qos|adhoc/qos))' ) +``` + +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-execution.md b/plan/pending/ieee80211-ampdu-phy-execution.md new file mode 100644 index 00000000000..7c4deb650a2 --- /dev/null +++ b/plan/pending/ieee80211-ampdu-phy-execution.md @@ -0,0 +1,154 @@ +# PHY P0 through P4 execution record + +The user authorizes P0, P1, and P2 on `4fa5cc0de15ee57388bcb3f20e866cab5972a670` before PRs #1343, #1344, and #1345 merge. This exception replaces the merge prerequisite for this execution only. The user also authorizes commits and a branch push. No baseline change forms part of this task. + +The active checkout is `/home/user/omnetpp_ws/inet-ieee80211-ampdu-implicit-blockack`, on branch `work/ieee80211-ampdu-implicit-blockack`. The implementation uses the approved base. The commit series separates the plans, P1 representation, and P2 reception. + +P0 through P4 are complete on 2026-10-17. The implementation supplies HT-SIG bytes, validation, bounded ranges, and cached partial-reception outcomes. P3 verifies NIST/Yans probabilities and seeded outcomes. P4 closes ordinary reception comparisons with the pinned parent. D1 and D2 are complete for the declared PHY profile. MAC acceptance and implicit Block Ack remain pending. + +## Implementation contract + +The radio owns HT-SIG fields and CRC construction. Its serializer preserves all 48 bits, including the received CRC. A shared validator rejects invalid CRC and unsupported fields before any range calculation. The existing mode and PSDU length remain the duration inputs. + +The shared delimiter inspector owns bounded range discovery. It checks delimiter bytes and advances at each scan. Delimiters and complete MPDUs have separate ranges. Alignment padding has no range outcome. MAC acceptance remains outside P1 and P2. + +The WLAN receiver owns PHY outcomes. The medium cache owns the PREAMBLE, HEADER, DATA, and WHOLE decisions. Direct success queries reuse those decisions. DATA retains copied range values; the result tag copies those values without reception pointers. A partial DATA failure preserves the aggregate bytes. + +The optional WLAN error-model contract supplies pure support queries and range probabilities. The shared WLAN base implements this contract for NIST and Yans. The MAC supplies its exact capability inputs before the MIB update. An absent model retains the current common PHY checks without error draws. + +The proposed wire fixture checks independent bytes, CRC, bounded ranges, and singleton identity. The proposed receiver fixture checks actual radio reception, range loss, query reuse, failure boundaries, metadata, and compatibility. Existing HT/VHT controls check ordinary reception. Debug compilation precedes behavioral checks. Scoped architecture checks cover both WLAN subtrees. + +The contract matches the inspected callers and owners. Source edits exclude the sealed `src/inet/common/packet/` subtree. Required input changes include MSG fields, serializer registration consumers, NED contracts, the MAC capability handoff, and migration text. + +## P0 source audit + +The cache key contains receiver, transmission, and signal part. `RadioMedium::getReceptionDecision()` inserts a decision after a cache miss. `CommunicationCacheBase::setCachedReceptionDecision()` deletes a replaced decision. `ReceptionCacheEntry` deletes all retained decisions at destruction. `receivePacket()` removes and deletes the result but retains the decisions. The result owns its packet and decision-vector container; it does not own the decisions. + +The direct `RadioMedium::isReceptionSuccessful()` call bypasses the decision lookup. The WLAN override must route aggregate queries through that lookup. PREAMBLE cannot calculate HEADER or DATA. Separate-part radio reception aborts after PREAMBLE or HEADER failure. Whole-signal result construction requests WHOLE. + +The HT header mode declares 48 bits. The transmitter derives duration from the selected mode and `lengthField`. The empty parent serializer does not alter these duration inputs. + +The metadata audit found three error-rate result filters. Each filter forwards present values without arithmetic. The radio attaches histogram recorders. OMNeT++ `StatisticsRecorder::collect()` ignores `NaN` for unweighted statistics. The receiver fixture must exercise these configured consumers before D1 closes. + +The direct reader in `L3NetworkConfiguratorBase` constructs an ordinary MTU-sized probe. It calls radio encapsulation without an aggregate request or delimiter content. Thus, it does not consume aggregate probability metadata. Ordinary metadata retains its current calculation. Other source matches either produce tags or concern unrelated error-model interfaces. + +BerTable's NED enum rejects the radio's `n(mixed-2.4Ghz)` string before initialization. An isolated BerTable with the allowed `n` value reaches its initializer, which also rejects HT. Both failures precede the new role check. Its packet-level PER calculation also dereferences the transmission bit model. The parent scalar control initializes with a valid table, then crashes at that null access. The feature library produces the same crash. This result establishes a prior limitation, not supported BerTable reception. The inherited receiver stage count covers the physical-layer check. + +## Evidence + +The existing HT/VHT module control exposed a required compatibility correction. `Ieee80211VhtSignalMode::createHeader()` returns an HT header in the parent. HT field construction therefore receives VHT's absent HT MCS value and refuses it. The VHT mode must create its existing VHT header type. This change preserves VHT duration and probability calculations and adds no VHT serializer implementation. The affected mode header is unsealed. The existing HT/VHT control verifies this required extension of the change surface. + +The wire fixture verifies golden bytes from IEEE Std 802.11-2024, Figure 19-6 and Clause 19.3.9.4.4. A polynomial long-division calculation supplied the fixed CRC constants independently of the production shift register. Fresh byte-backed packets establish decode without the original typed header. Cases cover standalone and singleton identity, short GI, invalid CRC, unsupported fields, incomplete headers, and improper representation. HT Length 65535 and MCS 31 survive conversion; length 65536 causes refusal. + +Delimiter cases cover lengths 0 and 4095, refusal of 4096, all alignment remainders, CRC and signature errors, truncation, final padding, and bounded recovery. An overrun delimiter cannot supply a valid member boundary. + +The receiver fixture uses the real radio and medium with scalar and dimensional reception. It executes 31 scenarios in both reception modes, for 124 seed-0 runs. The scenarios cover ordinary NIST/Yans reception, an absent model, partial loss, delimiter and spacing-delimiter failure, total member failure, and failed common parts. Invalid CRC, unsupported fields, and structural refusal use distinct paths. Short-GI aggregate reception preserves the copied indication. An absent-model case with an unmet SNIR threshold supplies no accepted header or ranges and consumes no draws. + +Repeated PREAMBLE, HEADER, DATA, WHOLE, direct success, and result queries add no draws or probability calls. The fixed-probability case consumes seven draws for seven ranges. Endpoint probabilities consume no draws. The fixture checks PREAMBLE before any HEADER or range call. It retains a copied indication after normal cache cleanup. The result preserves power, SNIR, time, mode, channel, and transmission identity. + +The capability cases create a production WirelessHost. A missing role fails at the physical-layer check. Incomplete short-GI coverage fails before the MAC updates the MIB. Complete one-stream coverage passes despite unsupported catalog modes above the antenna limit. An absent model passes, including the unit-disk configuration. A non-HT model without the role retains ordinary reception; its runtime change to an HT set fails. + +Separate cases verify BerTable's NED refusal, initializer refusal, and non-HT initialization. + +The configured radio filters and histogram recorders receive aggregate metadata. The recorded aggregate probability histograms have zero samples because their input values are `NaN`. Ordinary NIST/Yans controls retain a zero PER and unknown BER/SER. Finite-SNIR controls without a model retain no error-rate tag. This evidence closes the metadata consumer audit for the configured paths. + +The isolated parent checkout is `/tmp/inet-ampdu-parent-4fa5cc0de1`. Its six ordinary HT runs pass with NIST, Yans, and no model, in both reception modes. The feature's corresponding scalar controls retain 170 microseconds for a 100-byte PSDU at HT MCS 0 with long GI. NIST/Yans retain PER zero and BER/SER `NaN`. Every corresponding control retains the next RNG value `2357136044`. + +## Commands and artifacts + +Commands run from the active repository root unless this record states another directory. Debug is the behavioral test mode. Every new fixture uses seed 0. Existing fixtures retain their declared runs and seeds. Source changes require a fresh matching library before dependent tests. + +Fixture commands use `MAKEFLAGS=--jobserver-style=pipe` and `MPLCONFIGDIR=/tmp/ampdu-matplotlib`. `/tmp/ampdu-source-sha256.txt` identifies the 22 changed source and test files. Its SHA256 is `6ab4156726c29065523a250443ade2cc67cbedceb220c70627001d7ca1017e16`. The manifest includes the final wire fixture and receiver fixture. Documentation does not form part of that source manifest. + +| Check | Command or selection | Status and artifact | +| --- | --- | --- | +| Debug build | `make MODE=debug -j8` with `MAKEFLAGS=--jobserver-style=pipe`. | Pass, exit 0; `/tmp/ampdu-debug.log`. | +| Release build | `make MODE=release -j6`, followed by `make MODE=release -j8` with the same `MAKEFLAGS`. | Both pass, exit 0; `/tmp/ampdu-release.log` and `/tmp/ampdu-release-final.log`. | +| Wire fixture | `inet_run_unit_tests -m debug -f 'Ieee80211AmpduWire_1\.test$' --no-concurrent`. | Pass, exit 0; `/tmp/ampdu-wire.log`. | +| Receiver fixture | `inet_run_module_tests -m debug -f 'Ieee80211AmpduPhyReception_1\.test$' --no-concurrent`. | All 124 runs pass, exit 0; `/tmp/ampdu-phy.log`. | +| Unit controls | `inet_run_unit_tests -m debug -f 'Ieee80211(AmpduWire\|MpduSubframeHeaderSerializer\|MpduDelimiterWire\|HtModeSet\|HtModeCache\|VhtHtCapabilities)_1\.test$' --no-concurrent`. | Six fixtures pass; `/tmp/ampdu-final-unit.log`. The wire fixture has a later focused refresh. | +| Module controls | `inet_run_module_tests -m debug -f 'Ieee80211(AmpduPhyReception\|VhtHtSupport\|HtAssociation\|HtAntennaRateControl)_1\.test$' --no-concurrent`. | Four fixtures pass, exit 0; `/tmp/ampdu-final-module.log`. That run includes 120 receiver scenarios. The later focused receiver run adds four absent-model SNIR controls. | +| Functional fingerprints | From `tests/fingerprint`: `./fingerprinttest -d -F tyf -t 3 -m 'showcases/wireless/(aggregation\|qos)/' showcases.csv`. | All five configurations pass, exit 0; `/tmp/ampdu-fingerprints.log`. Graphical ingredients are excluded. No expectation changes occur. | +| Architecture | `doc/project/enforcement/check-architecture.sh` for each WLAN subtree. | Both pass; `/tmp/ampdu-phy-architecture.log` and `/tmp/ampdu-mac-architecture.log`. | +| New interface | `doc/project/enforcement/check-interfaces.sh src/inet/physicallayer/wireless/ieee80211/contract`. | One interface passes; `/tmp/ampdu-interfaces.log`. | +| Source protection | `doc/project/enforcement/check-source-seals.sh --diff`. | Pass; `/tmp/ampdu-seals.log`. No sealed source path changes. | +| Naming | Scoped naming checks in the active and parent checkouts. | The same three PHY and 32 MAC candidates remain. The PHY log differs only in one line number. `/tmp/ampdu-phy-naming.log`, `/tmp/ampdu-mac-naming.log`, and their `ampdu-parent-*` controls retain the comparison. | + +The parent build command is `make MODE=debug -j12` from the isolated checkout. Its runner uses the same receiver-fixture selector and debug mode after `source setenv -q`. `/tmp/ampdu-clean-parent-debug.log` and `/tmp/ampdu-parent-controls.log` retain its results. The parent fixture contains only the ordinary controls and the same node setup. + +The isolated BerTable reproduction is `tests/module/Ieee80211AmpduBerTableControl_1.test` in the parent checkout. The parent runner records the failure in `/tmp/ampdu-parent-bertable.log`. The same generated executable also runs with the feature library first in `LD_LIBRARY_PATH`. `/tmp/ampdu-feature-bertable-libraries.log` confirms that library path. `/tmp/ampdu-feature-bertable.log` records the same null-bit-model failure. The fixture supplies all eight table rates. These failures do not count as passing reception controls. + +The runner's dry-run path fails with `ValueError('list.index(x): x not in list')`. Those previews establish no behavioral result. Normal filtered runs select and execute the named fixtures. A FIFO collision also affected an early parallel test build. Later commands use `MAKEFLAGS=--jobserver-style=pipe`. Fixture setup corrections precede the recorded passing runs. + +## Standards evidence and limits + +The local corpus identifies `ieee80211-2024` as IEEE Std 802.11-2024. Table 19-11 spans physical PDF pages 3431–3432. Clause 19.3.9.4.4 is on page 3434. Its Figure 19-8 reference resolves. The corpus cannot resolve Clause 9.7.1, so PDF text on pages 1841–1843 supplies delimiter evidence. PDF text on pages 3486–3487 confirms invalid-CRC and unsupported-mode behavior. These clauses are normative. The corpus linter reports unrelated heading ambiguities and unresolved references; the selected CRC node is unambiguous. Local artifacts `/tmp/ampdu-htsig-clause.json`, `/tmp/ampdu-htsig-table.json`, `/tmp/ampdu-htsig-refs.json`, and `/tmp/ampdu-standards-lint.json` retain the retrieved evidence. + +The implementation does not enable MAC receipt, MPDU release, or an implicit Block Ack response. Feature A2 and A4 retain those acceptance duties. A failed MPDU range must become that member's bit-error flag before the normal Rx FCS check. The aggregate probability fields are not member estimates. P3 and P4 supply the additional evidence below. The commit and push authorization does not permit baseline changes. + +Independent range draws at the current scalar SNIR are a packet-level model choice. This abstraction does not model correlated FEC errors or interference changes within one member. Alignment padding contributes duration but has no separate outcome. These checks establish representation, integration, and selected regression behavior. They establish no new PHY accuracy result. Release-mode behavioral tests and feature-isolation builds remain unverified. + +The final self-audit traces cache ownership, prerequisite order, result copies, header refusal, structural refusal, capability publication, and the ordinary MPDU branches. It finds no unresolved defect in the implemented scope. The audit uses the current architecture, naming, quality, test, and WLAN guidance. It is a self-audit, not an independent review. + +## Publication checks + +The first publication attempt rejected Git metadata writes with `Read-only file system`. A separate repository at `/tmp/inet-ampdu-publication` held the commit series. Its source and test files matched the tested checkout. The separate `AGENTS.md` edit remained outside the series. Shell access could not resolve GitHub, so that attempt required the GitHub connector. + +The final debug and release builds pass in `/tmp/ampdu-push-debug.log` and `/tmp/ampdu-push-release.log`. The repeated filters pass six unit fixtures and four module fixtures. Their logs are `/tmp/ampdu-push-unit.log` and `/tmp/ampdu-push-module.log`. The new series passes commit, classification, include, and NED parameter checks. The audit supplies no PR description because this task creates no PR. Global architecture, naming, and interface findings match the pinned parent. Source seals pass. These checks establish final-tree behavior; intermediate commits have no separate compilation result. + +The repeated fingerprint filter passes all five controls without expectation changes. Its log is `/tmp/ampdu-push-fingerprints.log`. The full series since `origin/master` also passes its mechanical checks, with notes. The first publication attempt did not complete. The connector rejected the first tree request because tool approval was required and the approval policy was `never`. No remote branch update occurred. The original checkout retained its previous HEAD and local files. + +The user requested a retry after the permissions changed. Git metadata writes and shell access to GitHub now succeed. All 28 prepared files match the active checkout before commit transfer. The active branch now contains the three prepared commits. The separate `AGENTS.md` edit remains outside those commits. Only this publication record changes after the transfer; the tested source and test files remain unchanged. + +## P3 and P4 execution + +The user requested P3 and P4 after publication of P1 and P2. The active HEAD is `0221daf515e1bdc7dcaa79b12b6547c11a977d75`. The comparison parent remains `4fa5cc0de15ee57388bcb3f20e866cab5972a670`. The active checkout and branch remain those recorded above. This execution changes tests, migration text, and plan records. It changes no production source, interface, ownership rule, model assumption, or stored fingerprint expectation. The separate `AGENTS.md` edit remains unchanged. + +The additional module fixture separates ordinary reception from the new aggregate interfaces. The same fixture source compiles on both revisions. This necessary test separation adds one PHY fixture, so the combined PHY and MAC inventory increases from nine to ten. The MAC feature's seven fixtures remain proposed. + +### P3: NIST/Yans probabilities and seeded outcomes + +The extended `Ieee80211AmpduPhyReception_1.test` executes 468 runs. Its original 31 scenarios now run with seeds 0, 1, and 2. Eight additional scenarios cover NIST and Yans, MCS 0 and 7, and long GI and short GI. Every scenario uses scalar and dimensional reception with whole and separate signal parts. The bounded campaign adds no distributional or error-accuracy claim. + +Test subclasses call the unchanged NIST and Yans implementations through the production receiver. Each range probability must equal the existing per-length DATA calculation at the configured scalar SNIR. The subclasses observe calls; they do not substitute probabilities. A separate reference matrix checks all 64 HT20 modes, including MCS 0 through 31 with both guard intervals. Each mode uses seven SNIR inputs and lengths 4, 100, 101, 102, 4095, and 65535 bytes. The minimum and mean reductions use distinct synthetic SNIR values. The four matrix instances execute 10752 exact comparisons without random draws. This matrix establishes calculation coverage; actual aggregate radio runs use MCS 0 and 7. + +For example, a three-member aggregate includes a spacing delimiter and three alignment bytes. Seven scored ranges use the real model probabilities. An independent generator with the same seed replays the HEADER draw and each eligible range draw. The fixture compares every retained outcome, draw count, and subsequent random value with that reference. Partial member failure preserves the envelope and copied outcomes. Alignment padding causes no eighth range or extra draw. + +At the PREAMBLE boundary, repeated cached and direct success queries cause no HEADER or DATA call or draw. At the result boundary, repeated queries reuse all retained outcomes and metadata. The fixture also checks indication values after cache cleanup. All 96 real-model records match between whole and separate signal parts. A repeated execution produces the same 96 records. The campaign contains 76 all-good records and 20 partial-failure records. Both models produce partial failure in this campaign. + +### P4: Ordinary reception and compatibility limits + +`Ieee80211AmpduPhyCompatibility_1.test` executes 576 runs on each revision. The inputs cover NIST, Yans, an absent model, and a non-HT custom model without the aggregate role. The HT controls select MCS 0 or 7 with either guard interval. The custom model selects ERP OFDM at 6 Mbit/s. Each input uses scalar or dimensional reception, whole or separate signal parts, and seeds 0, 1, and 2. The non-HT controls repeat the unused MCS and GI labels but always select that same ERP mode. + +Three noise levels distinguish all-good controls, fractional probabilities, and failures. The low-noise power is -110 dBm. The two higher powers are -30 and -28 dBm for MCS 0, or -50 and -43 dBm for MCS 7. The receiver's energy threshold exceeds the configured noise power. This condition avoids an initialization failure that would supply no reception evidence. The SNIR threshold remains -10 dB for these bounded controls. + +All 576 records match the parent exactly. The comparison includes delivery count, packet error, duration, PER, BER, SER, received power, minimum SNIR, reception endpoints, draw count, and subsequent random value. Assertions also check mode, channel presence, and transmission identity availability. The records include 498 successful receptions, 66 delivered failure containers, and 12 attempts without a delivered container. NIST supplies the twelve separate-part header aborts. The absent and non-HT custom models add no error draws. + +The ordinary record stream has SHA-256 `f62dbea0e379e33e13df3246a2213d53ba6e52cc4a46852510f7f61bd5a01869`. `/tmp/ampdu-p4-comparison.json` records the fixture hash, revisions, counts, and equality result. The parent executable resolves `libINET_dbg.so` and `libtest_dbg.so` from the parent checkout. The active executable resolves the corresponding active libraries. + +The extended reception fixture retains the unsupported-model checks from P2. The production MAC handoff refuses a missing role and incomplete short-GI coverage before its MIB update. It accepts unsupported modes outside the effective NSS limit. The fixture retains absent-model, runtime mode-set, non-HT custom, and BerTable initialization controls. The migration guide now states independent range draws, scalar SNIR reduction, guard intervals, padding treatment, and accuracy limits. + +The separate BerTable reproduction still exits with SIGSEGV, status 139, on both revisions. Both logs identify `bitModel->getDataLength()` at line 62 with a null bit model. They are `/tmp/ampdu-p4-bertable-parent.log` and `/tmp/ampdu-p4-bertable-feature.log`. This comparison confirms the documented prior limitation. It is not a passing reception result. P4 does not repair BerTable or claim support for that configuration. + +### Commands and evidence + +All active commands run from `/home/user/omnetpp_ws/inet-ieee80211-ampdu-implicit-blockack` unless a command names another directory. Debug compilation precedes the tests. Release compilation also passes. The final source tree equals the published P2 source tree. The debug library has SHA-256 `3df8db6dba916c23bada8ea83583b41e9a425c81fc5a3bf90c83f7b75566044d`. The parent debug library has SHA-256 `877fa150f8cc41a21c765277aeb0ec478f99a6a9092a60462110496a68c99841`. + +```sh +MAKEFLAGS=--jobserver-style=pipe make MODE=debug -j8 +MAKEFLAGS=--jobserver-style=pipe make MODE=release -j8 +MAKEFLAGS=--jobserver-style=pipe MPLCONFIGDIR=/tmp/ampdu-matplotlib inet_run_unit_tests -m debug -f 'Ieee80211(AmpduWire|MpduSubframeHeaderSerializer|MpduDelimiterWire|HtModeSet|HtModeCache|VhtHtCapabilities)_1\.test$' --no-concurrent +MAKEFLAGS=--jobserver-style=pipe MPLCONFIGDIR=/tmp/ampdu-matplotlib inet_run_module_tests -m debug -f 'Ieee80211(AmpduPhyReception|AmpduPhyCompatibility|VhtHtSupport|HtAssociation|HtAntennaRateControl)_1\.test$' --no-concurrent +``` + +The build logs are `/tmp/ampdu-p34-debug.log` and `/tmp/ampdu-p34-release.log`. Six unit fixtures and five module fixtures pass. The unit log is `/tmp/ampdu-p34-units.log`. The final module log is `/tmp/ampdu-p34-final-modules.log`. The direct compatibility logs are `/tmp/ampdu-p4-feature.log` and `/tmp/ampdu-p4-parent.log`. + +The parent command uses the identical compatibility fixture at `/tmp/inet-ampdu-parent-4fa5cc0de1`, after `source setenv -q`, with the explicit filter `Ieee80211AmpduPhyCompatibility_1\.test$`. Its debug build remains the clean P0 parent build. The receiver's first model records are `/tmp/ampdu-p3-first-model-results.txt`. Later records match them exactly. + +The module fixture writes its detailed output under `tests/module/work//test.out` in each checkout. To reproduce the comparison, select the lines that start with `CONTROL ` from each compatibility output. Require exactly 576 lines in each file. Compare the lines byte for byte in their original order. The fixture's post-run checker separately requires all 576 successful simulation runs and unique input combinations. The reception fixture's checker requires all 468 successful simulation runs and equality across signal-part modes. + +Scoped architecture checks pass for both WLAN subtrees. Their logs are `/tmp/ampdu-p34-architecture-phy.log` and `/tmp/ampdu-p34-architecture-mac.log`. The source-seal check reports no changed production files. `git diff --check` passes. The earlier filtered fingerprint results remain evidence for the unchanged production source. This test-only extension requires no new fingerprint baseline or broad fingerprint campaign. + +Early fixture revisions failed on invalid noise thresholds, typename expression syntax, and an incorrect ERP header-length assumption. A post-run check also exposed insufficient ordinary failure coverage. The final fixture corrects those setup and coverage defects. The earlier mixed batches retain their actual failure status. They do not replace the final clean runs. + +The self-audit checks actual model dispatch, byte-to-bit conversion, mode coverage, SNIR reduction, probability bounds, cache order, sample reuse, metadata, and normal cleanup. It also checks identical parent inputs and library selection. It finds no unresolved defect in the requested scope. This result is a self-audit. Release behavioral tests, feature-isolation builds, correlated FEC behavior, and MAC acceptance remain outside this evidence. P4 closes D2 for the declared PHY profile, not the complete MAC feature. diff --git a/plan/pending/ieee80211-ampdu-phy-reception.md b/plan/pending/ieee80211-ampdu-phy-reception.md new file mode 100644 index 00000000000..5851cf79ce9 --- /dev/null +++ b/plan/pending/ieee80211-ampdu-phy-reception.md @@ -0,0 +1,364 @@ +# Implementation plan: partial PHY reception for HT A-MPDU + +> **Kind:** procedure · **Status:** draft · **Seal:** none · **Owns:** — · **Stands on:** [S5 execution record](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-compressed-blockack-s5`, branch `work/compressed-blockack-s5`, HEAD `4fa5cc0de15ee57388bcb3f20e866cab5972a670`. + +Plan revision: 11, 2026-10-17. The user's `proceed, including the optional simplification` authorizes the reviewed document corrections. This document accompanies feature revision 12 and its revised summary. The feature plan defines PSDU-based RTS selection, recovery before DATA submission, and a boolean probe requirement with a derived access category. This PHY revision synchronizes those references; its behavior and delivery gates remain unchanged. Implementation, expectation changes, commits, and publication remain outside this authorization. + +The project plan procedure remains a draft. + +Execution status, 2026-10-17: P0 through P4 are complete under the user's approved exception to the merge prerequisite. D1 and D2 are complete for the declared PHY profile. The [execution record](ieee80211-ampdu-phy-execution.md) identifies the active checkout, checks, and limits. P4 adds one parent-compatible module fixture for exact ordinary reception comparisons. This test addition changes no production contract. The proposal below retains its original design context. MAC acceptance and the feature stages 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 human summary are local draft documents. Other local changes affect `AGENTS.md` and earlier Block Ack plans and records. This plan preserves those unrelated files. No source change or new runtime result supports the proposed behavior yet. + +## 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. | Retain the agreed merge gate. Pin the integrated 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. | Read-only preparation can start now. Closure requires all three merges. | +| P1: Define representation and scored ranges. | Extend typed HT-SIG state and its serializer together. Define the shared stateless validator. Define range kinds, offsets, outcomes, and member-error projection. Harden shared delimiter inspection. Fix scoring and evaluation order. | 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. Other wire cases prove limits, truncation, alignment, and singleton identity. Spacing delimiters receive outcomes; padding has no outcome or draw. | P0. | +| 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. | + +```mermaid +flowchart TD + M["Merged #1343, #1344, #1345
P0: Refresh the base"] --> P1 + P1["P1: Representation and bounded ranges"] --> 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 + +The PHY plan now owns three fixtures. P1 and P2 supplied the wire and partial-reception fixtures. P4 adds `Ieee80211AmpduPhyCompatibility_1.test` because its ordinary controls must compile on the parent without the new PHY interfaces. The feature plan owns seven proposed fixtures, so the combined inventory is ten. The execution record separates current PHY evidence from proposed MAC evidence. + +| Proposed fixture | Boundary and required observations | +| --- | --- | +| `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. A separate bounds case refuses content shorter than valid HT Length. Real delimiter/member layouts prove CRC/signature, lengths 0/4095/4096, every alignment remainder, no final HT padding, truncation, and bounded resynchronization. | +| `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) + +inet_run_unit_tests -m debug -f 'Ieee80211AmpduWire_1\.test$' + +inet_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 [feature plan's evidence status](ieee80211-ampdu-implicit-blockack.md#evidence-status) records this revision's document checks. No build, fixture, simulation, or noisy-channel campaign ran. + +## 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. + +The full merge gate is the agreed delivery policy for one integrated S5 baseline. It keeps both plans on the same contracts and verification parent. Pure PHY calculations do not inherently require the MAC agreement lifecycle changes. An independent PHY base would need its own pinned dependencies and compatibility evidence. + +Implementation requires merged PRs [#1343](https://github.com/inet-framework/inet/pull/1343), [#1344](https://github.com/inet-framework/inet/pull/1344), and [#1345](https://github.com/inet-framework/inet/pull/1345). P0 must pin their integrated base. Read-only preparation does not satisfy that gate. A changed owner, result contract, or model-detail assumption requires a plan revision before the dependent implementation. diff --git a/plan/pending/ieee80211-compressed-blockack-s4-execution.md b/plan/pending/ieee80211-compressed-blockack-s4-execution.md new file mode 100644 index 00000000000..dc83901f766 --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s4-execution.md @@ -0,0 +1,178 @@ +# S4 execution record + +S4 completes Basic BAR work independently of DATA extraction. The initial source parent is `b29151fc8b`. The user authorized S4 implementation on 15 October 2026. Existing local edits to `AGENTS.md` and the prerequisite plans remain outside this change. + +## Implementation contract + +HCF previews the next eligible frame before a frame sequence extracts DATA. The originator ACK policy selects outstanding frames by peer and TID. The policy permits threshold deferral only for a candidate within that agreement's transmit window. The agreement owns the window start and size. Resolved DATA permits window progress; a received response number alone does not. + +HCF treats BAR work as contention work for its access category. Frame sequences permit BAR without a DATA pointer. Internal collision retains BAR work without DATA retry feedback. At an exchange boundary, HCF queues DELBA when an accepted agreement has no queued DATA, in-progress DATA, or active BAR exchange. Expiration uses the same teardown path after unresolved DATA disappears. + +The affected components are HCF, the QoS ACK policy, the ACK handler, the originator agreement, the in-progress queue, and HCF frame sequences. The optional QoS data-service contract exposes preview without a concrete dependency from the shared queue. Queue preview borrows a Packet pointer. Extraction retains the existing ownership transfer and sequence assignment. DCF extraction remains unchanged. S5 owns Compressed response validation and integration. + +The checks use focused module fixtures through HCF, frame sequences, Tx, and production response callbacks. They cover small windows, short bursts, peer/TID isolation, failed BAR recovery, collision, final expiration, and transfer completion. Timing checks compare Basic BAR exchanges with the pinned parent under single and multiple protection. A new unlawful continuation requires an admission correction before completion. + +The initial contract review checked source, ownership, protection, and callers before the first source edit. The affected source paths have no seal. S1 recipient progress and S3b teardown exist in the pinned parent. + +## Standards evidence + +The local corpus supplies IEEE Std 802.11-2024 normative Clauses 10.25.4, 10.25.6.7.1, and 10.23.2.9. Their canonical identifiers are `ieee80211-2024:clause:`. Their physical PDF pages are 2056, 2068–2070, and 2012–2014. Corpus status reports fresh input. The corpus linter result is `/tmp/s4-standard-lint.json`. Direct PDF inspection was unnecessary. + +## Verification + +Failed BAR recovery also requires the Retry bit on the next DATA attempt. Source inspection found that the old BAR failure path only changed ACK status. HCF now derives that bit from the failed Block Ack status before retransmission. IEEE Std 802.11-2024, Clause 9.2.4.1.6, physical PDF page 665, supplies this requirement for individual DATA. + +### Direct behavior checks + +The fresh debug build passes. The final focused selection passes 31 module fixtures and six unit fixtures. The module log is `/tmp/s4-module-final-limit.log`. The unit log is `/tmp/s4-unit-final-limit.log`. The new BAR fixture executes 30 scenarios across single and multiple protection. These results establish the recorded Basic paths. They do not establish Compressed support. + +The unit fixture checks peer/TID selection, sequence wrap, window boundaries, response identity, failed-response release, and exact lifetime eligibility. The production fixture exercises HCF, EDCA, frame sequences, Tx, and response callbacks. Its cases cover thresholds below five, accepted windows one and four, unrelated peers, negative bitmaps, timeout, and final expiration. The collision cases require BAR contention without a DATA retry entry. The nonzero contention-window case requires contention-window growth after collision. The short-limit case permits the initial Control-frame exception. + +BAR failure updates the access category's short retry counter and contention window. The one-failure limit case requires growth on the first failure. The unit fixture requires reset on the next failure. A successful Block Ack response resets both values. These checks require no DATA retry entry. + +The final-expiration case creates an incomplete fragment unit before two complete units. The production recipient retains those complete units behind the gap. Final expiration triggers DELBA. The recipient delivers both complete units before it deletes the agreement. This connects A5 to the existing recipient teardown path. + +Three existing fixtures require changed expectations because a short burst now resolves its final BAR and DELBA. The deferral fixture checks both remaining original DATA frames. Both retain their lifetime obligation after unsent disposal. The fragment-retirement fixture answers the unrelated peer's final BAR before its successor DATA. The multiple-protection fixture separates the DELBA management ACK from DATA ACKs. + +### Basic timing comparison + +The comparison parent is `b29151fc8b8953564dbbf61b48ece12658a1475c`. A detached checkout at `/tmp/inet-s4-parent` supplies its separately built debug library. The timing observer records actual transmit modes, preceding IFS, response airtime, TXOP deadline, and DATA count. The parent fixtures supply observations without S4 assertions. Their successful runner result alone supplies no S4 behavior proof. + +For the 1160-microsecond TXOP, the parent transmits a final BAR after five DATA frames. That BAR exchange ends at 2.270 ms. The deadline is 2.230 ms. This 40-microsecond overrun occurs with both protection modes. The S4 admission check rejects that continuation. EDCA later contends for a standalone BAR. The direct fixture requires every later BAR exchange to fit its applicable bounds. + +A separate case gives a standalone BAR a 50-microsecond TXOP limit. The initial BAR exchange takes 120 microseconds. Its 70-microsecond overrun uses the permitted initial Control-frame exception. It has no preceding DATA or Management frame. The admission check preserves this case. + +The diagnostic showcase traces also find DATA continuations beyond the TXOP bound in both parent and candidate. Each recorded violation ends with DATA. S4 does not repair general DATA admission. These observations limit the result to the changed Basic BAR paths. They do not establish general TXOP or NAV conformance. + +### Completion cost + +The cost fixture sends twelve UDP payloads of 700 bytes over a fixed 100-ms observation interval. One configuration sends one continuous burst. The other sends three bursts of four, with 20-ms gaps. Both configurations deliver all twelve payloads without a MAC drop. The payload goodput is 672000 bit/s in each configuration. + +| Candidate measurement | Continuous burst | Three bursts | +| --- | ---: | ---: | +| ADDBA requests / responses / DELBA frames | 1 / 1 / 1 | 3 / 3 / 3 | +| Management airtime | 108 microseconds | 324 microseconds | +| Total measured deferral | 280.133424 microseconds | 822.400272 microseconds | +| Request contention | 61 microseconds | 156 microseconds | +| Response time after request | 159.066712 microseconds | 486.200136 microseconds | +| DELBA completion time | 114.066712 microseconds | 387.200136 microseconds | +| Sum of payload delivery delays | 147.632334920 ms | 43.933601088 ms | + +The parent sends one request and one response in each configuration. It sends no completion DELBA. Its management airtime is 72 microseconds in each configuration. Both parent configurations deliver the same payload total. The parent and candidate also differ in extraction and BAR behavior. Thus, this comparison does not isolate an agreement-reuse cost. It supplies no general throughput or latency claim. + +### Regression and source checks + +The parent matches all nineteen original fingerprint rows. The evidence is `/tmp/s4-parent-fingerprints.log` and `/tmp/s4-parent-fingerprints-retry.log`. The selection preserves each original configuration, run number, simulation limit, and requested fingerprint ingredient. The final candidate run completes all nineteen rows in 227.330 seconds. Twelve rows match their baselines. Seven rows report fingerprint differences in configurations with Basic Block Ack enabled. + +The final log is `/tmp/s4-fingerprints-final-limit.log`. Its runner exits with status 1 because the seven recorded baselines differ. Every simulation reaches its required limit, including the enabled ad hoc row at ten seconds. Earlier attempts hit its 600-second CPU limit. Those partial attempts supply no replacement baseline. The final retry-limit correction preserves all nineteen results from the preceding full run. + +The following values are review candidates only. This execution does not approve or change a recorded baseline. + +| Configuration | Candidate fingerprint values | +| --- | --- | +| `NoFragmentation` | `4a83-1e6f/tplx;09db-9f72/~tNl;0596-3a23/~tND` | +| `Fragmentation` | `91a8-1fb6/tplx;f43e-1f17/~tNl;aaae-de01/~tND` | +| `MixedTraffic` | `202c-1e73/tplx;4e7c-be6a/~tNl;5544-6a00/~tND` | +| `HCFfragblockack` | `140a-28bf/tplx;233d-c918/~tNl;5890-e570/~tND` | +| TXOP `General` | `06db-3079/tplx;3727-bbbd/~tNl` | +| Ad hoc `MacQos`, run 1 | `491a-01b6/tplx;9558-1598/~tNl;6970-8509/~tND` | +| `MacQosWithBlockAck` | `cdb0-57dc/tplx;aa99-d171/~tNl;1368-030c/~tND` | + +The architecture check passes. The source-seal check permits every changed source path. The naming check reports the same 32 findings as the parent. The interface check retains the parent's two frame-step findings. The new QoS service interface passes that check. The logs use the `/tmp/s4-` prefix. No recorded fingerprint baseline changes belong to this implementation. + +The protocol selection executes eight fixtures. Four pass. Delayed Block Ack, Multi-TID Block Ack, and both A-MPDU fixtures retain four declared expected failures. `N_CompressedBa` selects Basic in this branch. Its pass does not establish Compressed behavior. The log is `/tmp/s4-protocol-final-limit.log`. + +The frame observer compares the first changed transmission in each affected configuration. It preserves actual bytes, sequence, TID, Retry, Duration, and selected mode. Its configuration adds an observer and a shorter observation limit. Thus, these observations explain local mechanisms; they do not replace the full fingerprint runs. + +| Configuration | First observed change | Confirmed mechanism | +| --- | --- | --- | +| `NoFragmentation` | ADDBA request replaces the next Normal Ack DATA at sequence 1. | Preview avoids speculative DATA extraction before setup. | +| `MixedTraffic` | ADDBA request replaces the next large Normal Ack DATA at sequence 1. | The same extraction change permits setup before the successor. | +| `Txop` | ADDBA request precedes the next large DATA at sequence 2. | The same extraction change preserves the setup boundary. | +| `Fragmentation` | Final fragment 15 of sequence 1 reserves 464 instead of 116 microseconds. | Preview reads the queued unit before aggregation or fragmentation. | +| `HCFfragblockack` | Retransmitted fragment 2 of sequence 1 sets Retry. | Failed Block Ack status now sets the DATA Retry bit. | +| `MacQosWithBlockAck` | The first changed DATA has a 332-byte A-MSDU instead of a 166-byte unit. | Later extraction changes the available aggregation set. | +| Ad hoc `MacQos`, run 1 | Fragment 2 of sequence 1 reserves 484 instead of 628 microseconds. | Preview no longer extracts the next aggregate for the Duration estimate. | + +These Duration observations do not prove complete NAV correctness. The existing Basic forecast does not model every BAR boundary. The direct timing assertions establish the separate BAR admission result. + +The saturated ad hoc case also exposes a host execution cost in transfer-completion checks. A temporary queue observer records 5120 predicate checks in each unrelated queue. Its stack trace identifies `completeTransfers` through `hasQueuedData`. HCF now checks the category from EDCA's existing TID mapping first. It retains the other categories as a fallback for custom classification. This changes search order without new persistent state or a different existence predicate. + +The temporary HCF observer records the same event count and fingerprint values before and after that search-order change. Its observed time in `processLowerFrame` falls from 41.0384 to 4.68354 seconds. Its observed time in `frameSequenceFinished` falls from 8.56017 to 0.226506 seconds. Host load differs between these observations. These values locate the cost; they supply no controlled speed benchmark. + +### Reproduction and retained evidence + +The local evidence directory is `audit/2026-10-15-s4/`. It retains the raw logs, parent observations, fingerprint selection, and temporary diagnostic sources. The diagnostic sources remain outside the delivered test suite. The directory also retains source hashes for this result. The build uses OMNeT++ from `/home/user/omnetpp-6.4.0aipre2` and the checkout's `setenv`. + +The category runners use these selections from the repository root: + +```sh +make MODE=debug -j8 +inet_run_unit_tests -m debug -f 'Ieee80211(BlockAckScheduling|MultipleProtection|HtModeSet|HtModeCache|VhtHtCapabilities|ResponseModeSelection)_1\.test' +inet_run_module_tests -m debug -f 'Ieee80211(BlockAck(ActionDelivery|ActionDisposal|ActionDispatch|AssociationReset|DataSelection|Deferral|DeferredCollision|FragmentRetirement|Lifecycle|Negotiation|RetiredData|Reordering|ResponseWait|Admission|Variant|Scheduling|CompletionCost)|EdcaDataLifetime(Arbitration|Aggregation|Congestion|Defaults)?|Hcf(Header|SingleProtection|MultipleProtection|InternalCollision)|BasicCompatibility|RecipientNav|SingleProtectionGroup|VhtHtSupport)_[12]\.test' +inet_run_protocol_tests -p inet -m debug -w wifi -f '(N_BlockAck|Ac_BlockAck|N_Amsdu|N_CompressedBa|WifiQosDelayedBa|Ac_MultiTidBa|N_Ampdu|Ac_Ampdu)\.test$' +``` + +The fingerprint command runs from `tests/fingerprint`. The retained CSV contains the exact nineteen original rows. + +```sh +./fingerprinttest -d -t4 -m '^/(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless|adhoc)/qos)/ ' -f tplx -f '~tNl' -f '~tND' -l /tmp/s4-fingerprints-final-limit.out /tmp/s4-fingerprint-final-limit.csv +``` + +### Scope limits + +S5 owns Compressed response integration. Delayed Block Ack, Multi-TID Block Ack, and A-MPDU remain outside this result. Recorded fingerprint baselines remain unchanged. The existing prerequisite plans and unrelated local files retain their prior edits. The initial implementation checks precede the commit and push request. The publication checks extend that initial evidence. + +## Initial checks before publication + +The user authorized commit and push on 15 October 2026. The series separates DATA preview from its first BAR selection consumer. The preview commit is `e2d20d6be6`. Its isolated debug build passes. Five focused module fixtures pass, including the direct preview checks for ownership and sequence assignment. The behavior commit contains BAR selection, window progress, transfer completion, and their direct fixtures. + +The final debug build passes. Its fresh selection passes 31 module fixtures and six unit fixtures. A separate scheduling run also passes after removal of the temporary parent-comparison directive. These checks retain the same runtime assertions. The logs are `/tmp/s4-publish-debug-build.log`, `/tmp/s4-publish-modules.log`, `/tmp/s4-publish-units.log`, and `/tmp/s4-publish-scheduling-clean.log`. + +The preview commit matches all nineteen retained fingerprint rows. Its log is `/tmp/s4-preview-fingerprints.log`. The final protocol rerun returns four passes and four declared expected failures across eight fixtures. Its log is `/tmp/s4-publish-protocol.log`. + +The final release compilation also passes. Its log is `/tmp/s4-publish-release-build.log`. The final fingerprint rerun completes nineteen rows. Twelve match their baselines; seven retain exactly the candidate values in this record. Its runner exits with status 1 for those differences. Its log is `/tmp/s4-publish-fingerprints.log`. + +The whole-tree architecture gate reports 36 findings. The whole-tree naming gate reports 23 findings. The interface gate reports 15 findings. Each sorted finding set matches the selected parent. The source-seal check passes. + +The series gates pass against `origin/master`; their two notes concern the earlier lifecycle commit `667ae51dc`. No pull request description check runs because this task creates no pull request. + +The C++ quality gate does not run because this checkout lacks `src/compile_commands.json`. Its setup exits with status 2. The log is `/tmp/s4-publish-cpp.log`. The compiler checks and the other gates do not replace this static analysis. + +## Rebase onto the updated S3b branch + +The user authorized this rebase after the initial publication. The pinned S3b target is `f6f12c5c9fed341f5834d4ccdeedeec7a562e102`. The original S4 head is `65804c762e524f64d07cfa336da0ceeb0d2bd425`. The backup ref is `refs/backup/compressed-blockack-s4-before-s3b-rebase-20261015`. The two S4 commits retain their order and purpose. + +The target adds local allocation indices for DATA retirement across large sequence gaps. The rebase retains that production mechanism alongside S4 window progress and completion DELBA. The collision fixture now records the extracted frame's allocation index. The fragment-retirement fixture retains all twenty gap scenarios and the Basic response path. A deferred successor keeps the earlier transfer open until the fixture advances the allocator. This prevents completion DELBA before the intended inactivity check. + +The user permits builds and tests only at the final commit. No intermediate commit receives a build or test. The earlier results above remain historical evidence. Final verification uses the last rebased commit. Its results and preserved-file hashes belong to `audit/2026-10-15-s4-rebase/`. + +## Approved fingerprints and final Basic boundary + +The user approved the seven final fingerprint changes on 2026-10-15 after review of their causes in PR #1344. This approval covers the non-graphical ingredients in the table below. Graphical `tyf` values remain unchanged and unverified. This section supersedes the earlier candidate values and the statements that S4 includes no baseline update. + +The final correction limits Basic single protection to an already prepared successor. HCF ends the TXOP before further DATA extraction can change aggregation, fragmentation, or agreement setup. Prepared fragments retain their continuation path. Final DATA in a zero-limit TXOP reserves only an immediate response when required. The policy can require more contention; it preserves standalone BAR progress. + +The source and tests at `8745330c1cc398f3dbc0a43be0c60cd7c2d5c529` contain this correction. Debug and release builds pass at that revision. All 31 module fixtures and six unit fixtures pass. Protocol execution reports four passes and four declared expected failures. The nineteen fingerprint simulations complete; twelve match the previous baselines and seven produce the approved values below. The raw evidence belongs to `audit/2026-10-15-s4-duration/`. + +The final observer shows zero Duration for sequence 1, fragment 15, in the fragmentation showcase. The TXOP ends with that DATA; BAR follows in a separate TXOP. The ad hoc observer shows 44 microseconds for sequence 1, fragment 2. Its ACK advertises zero Duration, and the TXOP ends. These observations replace the earlier unverified 464- and 484-microsecond forecasts. They do not establish general Basic DATA admission or complete NAV conformance. + +| Configuration | Approved fingerprints | Cause of the movement | +| --- | --- | --- | +| Block Ack `NoFragmentation`, run 0 | `9161-625d/tplx;6a02-09aa/~tNl;cbc0-3b0a/~tND` | Final Normal Ack DATA reserves only its ACK; setup and BAR exchange order also change. | +| Block Ack `Fragmentation`, run 0 | `36f4-5c22/tplx;66b0-94cd/~tNl;45f3-af4f/~tND` | Final Block Ack DATA reserves zero; BAR obtains another TXOP. | +| Block Ack `MixedTraffic`, run 0 | `351b-d4ba/tplx;5c1f-69a8/~tNl;6182-6da3/~tND` | The current reservation excludes the unextracted large successor; setup can precede that DATA. | +| Fragmentation `HCFfragblockack`, run 0 | `2597-1e2e/tplx;d269-c6ae/~tNl;2f5c-4cca/~tND` | Failed Block Ack feedback sets Retry on retransmitted DATA; the final burst boundary also changes. | +| TXOP `General`, run 0 | `ab91-87f3/tplx;f30c-6aa4/~tNl` | The reservation excludes an unextracted successor, and the frame sequence enforces that boundary. | +| Ad hoc `MacQos`, run 1 | `4fd2-bb56/tplx;ecaf-89f7/~tNl;9398-0fae/~tND` | Final Normal Ack DATA reserves SIFS plus ACK; further extraction requires another TXOP. | +| Wireless `MacQosWithBlockAck`, run 0 | `6edb-0326/tplx;2a8a-50ee/~tNl;4301-882d/~tND` | ADDBA reserves its immediate ACK without unrelated queued DATA; later extraction can change aggregation. | + +The final history keeps the preview preparation separate. One behavior commit contains BAR selection, the final Duration boundary, and these seven baseline rows. Its production source and tests match `8745330c1c`. This placement avoids an intermediate behavior commit with obsolete expectations. Final verification after the baseline update uses only the final commit, as requested. + +## Explicit agreement arguments + +BAR policy calls now require an explicit agreement handler through both the interface and concrete policy types. Transmitted-BAR processing also requires an explicit originator agreement. An omitted argument fails compilation instead of silently selecting behavior without agreement state. Explicit null values retain their documented behavior. Production callers already supply these arguments, so their runtime behavior stays unchanged. + +Compile-time checks cover omitted arguments through both policy types and the ACK handler. The scheduling fixture also checks explicit null policy calls with outstanding DATA. Custom test policies follow the same explicit contract. The migration guide describes the source compatibility change. The seven approved fingerprint rows stay unchanged. + +The source-equivalence statement in the preceding section describes baseline acceptance at `fcedba79e3`. This later correction changes API declarations, compile-time checks, and documentation. It changes no production C++ function body. diff --git a/plan/pending/ieee80211-compressed-blockack-s5-execution.md b/plan/pending/ieee80211-compressed-blockack-s5-execution.md new file mode 100644 index 00000000000..e89c9e74f7d --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s5-execution.md @@ -0,0 +1,227 @@ +# S5 execution: Compressed Block Ack beside Basic compatibility + +S5 is complete through C1, C2, C3, and C4. The authorized TXOP correction removes the active Compressed timing blocker. HCF now defaults to Compressed. Explicit Basic compatibility remains available. + +The comparison parent is `75559a49661e33f727fa93a8c81c021596cfb4c9`. The active checkout is `/home/user/omnetpp_ws/inet-compressed-blockack-s5`. The maintainer requested S5 execution on 16 October 2026. That request authorizes C1 through C4 in the [replacement plan](ieee80211-compressed-blockack-replacement.md#compressed-replacement-steps). + +The checkout contained changes to `AGENTS.md` and four prerequisite plans before execution. It also contained an untracked planning history. This work preserves those changes. The pinned parent contains the S4 implementation. The [S4 execution record](ieee80211-compressed-blockack-s4-execution.md) supplies its earlier evidence and limits. + +## Implementation contract + +HCF owns the immutable profile and validates received frame formats before agreement updates. An active agreement identifies one peer and TID. A valid conflicting BAR stops the simulation because ADDBA does not negotiate the profile. Malformed input cannot cause that diagnostic, receipt updates, or agreement recovery. + +The recipient record owns explicit Compressed receipt bits and its scoreboard window. The reorder service owns delivery order and its separate window. Delivery must preserve receipt bits. An absent receipt produces zero, including positions before the first received DATA. Basic retains its fragment bitmap and existing compatibility behavior. + +The rate selector consumes the MIB rate sets. It selects a non-HT Compressed response and distinguishes initial BAR from later BAR. Frame sequences supply the actual request profile. Protection, timeouts, and duration calculations use the corresponding response length. Compressed responses contain 32 bytes with FCS; Basic responses contain 152 bytes. + +For an active Compressed agreement, the originator suppresses fragmentation for that peer and TID. The recipient rejects fragmented DATA before receipt or reorder updates. Normal Ack outside that agreement retains its existing fragmentation behavior. + +## Recipient interframe design + +The recipient must distinguish receipt evidence from the interframe relation. An agreement with start 100 and size four first receives Normal Ack DATA 100. Delivery advances the reorder start to 101. DATA 104 advances the scoreboard start to 101 and preserves receipt evidence for 100. A qualifying interframe relation permits delivery of 104 despite the gap. A false receipt bit or absent relation retains 104. + +Local receive endpoints alone cannot establish the ACK-to-DATA relation with propagation delay. The medium therefore supplies one copied snapshot of physical endpoints. The snapshot contains transmission identity, transmitter identity, transmission endpoints, and cached arrival endpoints. It retains no frame, transmission, arrival, or radio pointer. + +The PHY derives the interval at the current transmitter. For the same transmitter, it subtracts the preceding transmission end from the current transmission start. Otherwise, it subtracts the preceding signal's cached arrival end at the current transmitter. This calculation uses the propagation result that the medium already produced. It does not assume equal outward and return delay. + +HCF owns local event history. A pending candidate preserves the interval across PHY reception completion and later MAC delivery. A receiver-created transmission identifier connects those events. Unrelated receptions replace prior history. Failed, abandoned, and overlapping receptions must not preserve stale eligibility. Every local transmission clears prior history; completed ACK transmission can establish new history. + +SIFS recognition uses the reconstructed interval after signal extension. The current PHY model uses baseline timing without coverage-class adjustment. The permitted bound is one tenth of the slot time. Integer tick division rounds that bound inward. Physical propagation remains separate from this MAC timing parameter. + +RIFS recognition requires permitted HT operation, the same transmitter and RA, and no preceding SIFS response obligation. It uses the preceding PPDU endpoint without signal extension. Management must preserve the advertised RIFS Mode. VHT operation does not permit RIFS. This recipient rule adds no RIFS transmitter scheduler. + +The review found that ordinary HT transmission at 2.4 GHz omitted the six-microsecond signal extension. The correction belongs in the authoritative PHY duration. HCF must not add it again. Existing short-GI duration differences remain inherited PHY fidelity limits; they do not independently establish an interval error. + +## Standards evidence + +The standards source is IEEE Std 802.11-2024. The fresh local corpus is `/home/user/omnetpp_ws/inet/standards/processed`. The corpus lint output is `/tmp/inet-s5-standards-lint.json`. + +| Canonical node | Physical PDF pages | Requirement | +| --- | --- | --- | +| `ieee80211-2024:table:9-36` | 719–720 | BAR variant Type encoding. | +| `ieee80211-2024:table:9-37` | 722–723 | Block Ack variant Type encoding. | +| `ieee80211-2024:clause:10.25.6.3` | 2060–2061 | Full-state recipient receipt state. | +| `ieee80211-2024:clause:10.25.6.6.2.1` | 2065–2066 | Recipient delivery and interframe conditions. | +| `ieee80211-2024:clause:10.3.2.3.3` | 1889–1890 | SIFS definition and tolerance. | +| `ieee80211-2024:clause:10.3.2.3.2` | 1888–1889 | RIFS definition and conditions. | +| `ieee80211-2024:clause:10.22.5` | 1997–1998 | Coverage-class timing behavior. | +| `ieee80211-2024:table:19-25` | 3493–3494 | Band-specific HT signal extension. | + +These provisions are normative. The selected corpus clauses resolve uniquely. The review inspected the original PDF for Clause 11.38.1 on physical page 2886. That clause requires a VHT AP to clear RIFS Mode. The corpus result without children does not include all later prose in that clause. + +## Verification boundary + +The initial full debug build and subsequent focused rebuilds pass. A build supplies no exchange or timing proof. The focused wire fixture passes independent octet checks. The constructed serializer fixture passes 278 cases. The HCF input fixture passes both configured profiles with supplied recipient agreements. Those results do not establish negotiation or PHY interval recognition. + +Five initial protocol simulations pass across the selected n/ac fixtures. They retain Basic coverage and add actual Compressed exchanges. This initial boundary precedes complete infrastructure timing classification and final interframe execution. Later sections record those results and the authorized correction. + +An isolated observer reproduces a DATA continuation overrun in the pinned parent. With a one-millisecond TXOP, the third DATA ends 178 microseconds after the bound. The candidate reaches the same endpoints. The parent also reproduces the first observed A-MSDU overrun. These observations identify inherited DATA admission limits. They do not establish general TXOP conformance or excuse a new Compressed violation. + +No recorded fingerprint baseline changed. This record does not authorize a baseline change or publication. + +## Confirmed timing blocker + +The candidate before the TXOP correction introduced a failing supported case that the exact parent did not reproduce. General run 6 uses HT operation, 24 Mbit/s DATA, single protection, aggregation, a one-millisecond TXOP, and seed zero. That candidate transmits two later A-MSDUs beyond their TXOP bounds. Each overrun is 310 microseconds. The affected TXOP contains two DATA frames, so the initial-MSDU exception does not apply. + +The second DATA ends at 1.014426 seconds. Its TXOP bound is 1.014116 seconds. The violation exists before any Block Ack response. The aggregate contains 1532 bytes in both versions. The exact parent has no aggregate overrun in run 6. Parent run 14 demonstrates a similar admission defect under different conditions. That other case does not make candidate run 6 acceptable. + +A direct run 6 reproduction also fails with the final reviewed debug library. Its output is `/tmp/inet-s5-reviewed-run6.out`; its error is `/tmp/inet-s5-reviewed-run6.err`. That run stops at the first later A-MSDU violation, at 1.014426 seconds. The earlier non-fatal observer campaign records both violations. + +The inherited admission mechanism causes the new result. `HcfFs` tests positive remaining time before a later exchange. It does not establish that the complete exchange fits. HCF excludes Block Ack support from its existing complete-cost DATA guard. The corrected HT control airtime and Compressed responses change the trajectory. The comparison does not isolate their separate contributions. + +All sixteen initial parent and candidate runs deliver thirty payloads, with 21000 payload bytes per run. All observed BAR/Block Ack exchanges remain within their applicable bounds. Those successes do not establish DATA containment. An independent review rejected that candidate under the [plan's timing condition](ieee80211-compressed-blockack-replacement.md#airtime-acceptance-and-admission-dependencies). The later correction and C3 acceptance below supersede that rejection. + +The [separate correction proposal](ieee80211-compressed-blockack-txop-admission.md) addresses preparation and admission. A final transmit guard alone cannot solve the problem. Aggregation already deletes constituent frames before HCF selects the actual mode. An oversized aggregate can therefore remain prepared and fail every later attempt. The proposal must preserve progress through ordinary MSDUs and the permitted initial-MSDU exception. + +## Focused verification after source review + +The reviewed debug library has SHA-256 `1635793563ff1c0912a4a0e55a70507879af2a20ef83a0b88502562748a347db`. Its build log is `/tmp/inet-s5-reviewed-debug-build.log`. The interframe fixture and direct run 6 reproduction use this library. The earlier mixed-interface regression batch is discarded. + +The unit and legacy results below use their recorded fresh libraries before the final bounded PHY review corrections. Those corrections repair empty-cache lookup, remove an unused virtual, and expose a watch. The protocol campaign uses library `1c57b8f562dbeb5cddbba49b77a3a676042fea7aef9b9e5578e1980c63109f56`. These earlier results retain their recorded source boundaries; they are not new executions against the final relink. + +| Check | Result | Evidence | +| --- | --- | --- | +| Focused units | Nine fixtures pass. | `/tmp/inet-s5-final-unit.log` | +| HT Operation conversion | One fixture passes both RIFS permission values and operation equality checks. | `/tmp/inet-s5-ht-operation-unit.log` | +| Legacy and HCF input modules | Thirty fixtures pass across the main batch and two repaired-fixture reruns. | `/tmp/inet-s5-final-module.log`, `/tmp/inet-s5-repaired-module.log` | +| EDCA DATA lifetime modules | Nine fixtures pass. | `/tmp/inet-s5-lifetime-module.log` | +| Production interframe fixture | All 28 runs pass with seed zero. | `/tmp/inet-s5-interframe-final/` | +| Constructed serializers | All 278 cases pass. | `/tmp/inet-s5-serializer-roundtrip.log` | +| n/ac protocol fixtures | Five simulations pass with Basic and Compressed coverage. | `/tmp/inet-s5-exchange-protocol-final.log` | +| Lowest response mode | Two real infrastructure runs pass at 6 Mbit/s. | `/tmp/inet-s5-exchange-lowest-config.out` | + +The two legacy fixtures required valid synthetic frames. The Action-dispatch fixture now supplies the BAR trailer. The fragment-retirement fixture now supplies all 64 fragment bitmap entries. Their behavioral assertions remain intact. + +The interframe fixture supplies accepted agreements. It uses real PHY reception and HCF ACK completion. It verifies SIFS and RIFS bounds, sequence wrap, prior receipt truth, unrelated traffic, retirement, and cache expiry. Its BUSY control uses a signal below reception sensitivity but above energy detection. Its 168 malformed BAR receptions each leave zero pending candidates. Separate infrastructure tests establish real association and ADDBA; the interframe fixture does not claim negotiation coverage. + +The source reviewers report no remaining confirmed correctness defect in their bounded core and interframe scopes. Architecture gates pass for the changed MAC and PHY subtrees. Review supplies no runtime proof beyond the executed fixtures. + +## Extension contracts in this candidate + +These obligations describe the current candidate. They require migration guidance before publication. They do not change the C3 blocker or activate the default. + +| Contract | Obligation for a custom implementation | +| --- | --- | +| `IQosRateSelection::computeCompressedBlockAckReqMode()` | Select a legal non-HT BAR mode from the supplied initial-or-later TXOP position and current rate sets. | +| `IRecipientQosAckPolicy::computeCompressedBlockAckDurationField()` | Subtract SIFS and the actual 32-byte response airtime from the request reservation. Preserve the Basic method. | +| `IRecipientQosMacDataService::dataFrameReceived()` | Accept the new interframe argument. Preserve its meaning through recipient processing. A false value supplies no interval evidence. | +| `QoSContext` | Supply HCF's resolved profile to the exchange. Existing callers retain the Basic default unless they explicitly supply Compressed. | +| `RecipientBlockAckAgreement::setVariant()` | Set the profile during agreement creation. Do not change it during that agreement's lifetime. Compressed creation initializes explicit receipt state. | +| `IRadioMedium` | Implement the new snapshot and gap queries with copied physical facts. Unavailable transmissions or arrival endpoints produce an absent result. | +| `ICommunicationCache::findCachedArrival()` | Return a cached arrival without insertion, resizing, geometry recalculation, or other state changes. | +| `IIeee80211Mode::getSignalExtensionTime()` | Supply the nominal extension for the mode. `Ieee80211ModeBase` supplies zero; ERP and HT override it where necessary. | + +## Common regression result + +All nineteen selected rows match their recorded non-graphical fingerprints. The runner uses debug mode and the original configuration, run, time limit, and requested non-graphical ingredients. It completes in 182.173 seconds with exit status zero. The selected input is `/tmp/inet-s5-fingerprint-selection.csv`; the log is `/tmp/inet-s5-fingerprint.log`. + +The command runs from `tests/fingerprint`: + +```sh +./fingerprinttest -d \ + -m '(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless/qos|adhoc/qos))' \ + -f tplx -f '~tNl' -f '~tND' -t 6 \ + /tmp/inet-s5-fingerprint-selection.csv +``` + +The graphical `tyf` ingredient remains unverified. No fingerprint baseline changed. These matches preserve the selected legacy regression evidence. They do not override the direct Compressed timing failure. + +These initial regression matches precede the separate TXOP correction and C4. No commit or publication occurred during this execution. + +## Build state before the authorized TXOP correction + +Both debug and release builds pass. The final release freshness check uses `make MODE=release -j6` from the repository root. Its log is `/tmp/inet-s5-reviewed-release-build.log`. The final debug build uses `make MODE=debug -j6`; its log is `/tmp/inet-s5-reviewed-debug-build.log`. The later NED edit corrects a comment and retains the Basic default. + +The separate TXOP proposal passed a consistency review as a scope request. On 2026-10-16, the maintainer authorized that correction and subsequent S5 completion. Source validation and independent review resolved the RTS boundary, lifetime ownership, and mode validation before implementation. The retained strict fixture preserves the inherited outside-agreement failure separately from active Compressed acceptance. + +## Authorized TXOP correction and C3 acceptance + +The [TXOP contract](ieee80211-compressed-blockack-txop-admission.md#resolved-implementation-contract) defines source ownership and each event boundary. The service creates duplicate previews without source removal. HCF selects one DATA mode, evaluates the aggregate, and evaluates an ordinary fallback with that same mode. Commitment transfers the same preview pointer only before DATA submission. Refusal preserves queue frames, deadlines, sequence state, and allocation state. + +RTS failure before DATA updates QSRC and the contention window without a provisional sequence counter. Original MSDU lifetimes bound repeated attempts. Expiry, capability loss, or changed response inputs cancel invalid preparation. Single protection reserves only the current exchange. A later BAR can require fresh contention. Basic and traffic outside active Compressed agreements retain their existing preparation and admission. + +Runtime verification found two effective selection callers that bypassed the first implementation. `RtsCtsFs` still extracted originals after admission. The multicast repeat predicate could also extract a prepared unicast successor before it ended the group sequence. Both callers now use the context-selected frame. Their focused regression cases preserve original queue state when no DATA submission occurs. + +The first retry policy required containment for all committed frames. A real BAR collision exposed a progress failure at 6 Mbit/s. One ordinary protected exchange needs 1.166 milliseconds under a nominal one-millisecond limit. Its first attempt qualifies for an exception, but conservative retry containment refused every later attempt. The implementation now uses the standard same-size single-MPDU exception with no earlier Data or Management frame. The supported path preserves committed MPDU size across retries. + +New previews clear Retry from the previous wireless hop. Committed retries derive Retry from local acknowledgment state. An oversized first-attempt A-MSDU still cannot use the retry exception. Internal collision commits only ordinary queued DATA before its retained recovery action. Multiple protection reserves the actual complete exchange for a permitted overrun. + +The corrected debug library has SHA-256 `0298dbd4c9625cd6f306865f5c1cfd0a3412230c6339b3a84289eb68eba713df`. The release library has SHA-256 `4b32335cdb2f558d60ada0761e46fde84c9879889e1c46b3c3f69bd11d9ae780`. Debug build 4 and the release build pass. Their logs are `/tmp/inet-s5-txop-debug-build-4.log` and `/tmp/inet-s5-txop-release-build.log`. + +The focused admission fixture passes all 24 cases with seed zero. It checks refusal, ordinary fallback, ownership, deadlines, sequence state, one mode query, and RTS cancellation. It also checks QSRC rollover, lifetime expiry, capability changes, internal collision, and multicast-to-unicast refusal. Cases 22 and 23 require actual ordinary and A-MSDU retries with unchanged size and DATA airtime above one millisecond. Evidence is `/tmp/inet-s5-admission24.out` and `/tmp/inet-s5-admission24.err`. + +All sixteen explicit-profile infrastructure cases pass the scoped timing assertions. The observer records agreement scope at DATA submission, before later teardown can change lookup results. Active Compressed checks include DATA with Normal Ack. No active later DATA or ACK overrun remains. All 98 matched BAR/Block Ack exchanges finish within their bounds. All 32 hop summaries report zero Block Ack overruns. + +The former run 6 failure now passes with zero DATA, ACK, or Block Ack overruns on both hops. Those hops transmit 23 and 11 active aggregates. The separate two-run lowest-response campaign records 29 actual 32-byte responses at 6 Mbit/s. Their airtime is 74 microseconds, with exactly 10 microseconds of SIFS. No Block Ack response exceeds its bound. + +The infrastructure observer separately records inherited violations outside active agreements. `StrictAllTiming` retains the original aggregate assertions and reproduces the pre-agreement aggregate failure in run 4. This acceptance establishes no timing compliance for those retained paths. The default change does not repair Basic or outside-agreement admission. + +The cleanup assertion permits only the idle `WAITING_FOR_SUCCESSOR` procedure with `SIMTIME_MAX` response deadline. Queues, in-progress frames, agreements, pending BAR state, and agreement timers must remain empty or inactive. That phase records a possible future setup after a completed Normal Ack transfer. It holds no unresolved exchange. + +Independent review accepts C3 for this scope and permits C4. The verdict is `/tmp/inet-s5-c3-acceptance-review.md`. Explicit-profile evidence is `/tmp/inet-s5-exchange-scoped-general16.out` and `/tmp/inet-s5-exchange-scoped-lowest2.out`. The scope table is `/tmp/inet-s5-exchange-scoped-general16.csv`. The unchanged strict failure remains in `/tmp/inet-s5-exchange-strict-all-inherited.out` and its `.err` file. + +## C4 default and migration verification + +`Hcf.ned` now defaults to Compressed. The infrastructure fixture omits the selector after C3 and requires actual Compressed frames. All sixteen default cases pass. Both lowest-response cases also pass after default activation. Evidence is `/tmp/inet-s5-default-exchange16.out` and `/tmp/inet-s5-default-lowest2.out`. + +All six external-style migration cases pass without an explicit selector. Legacy g and a links, and HT ad hoc links without peer records, deliver twelve UDP payloads through Normal Ack. Support-disabled controls produce no irrelevant warning. The fixture checks the resolved default, fallback diagnostics, absent ADDBA/BAR/Block Ack, and empty active state. Evidence is `/tmp/inet-s5-default-migration6.out`. + +All twelve initialization cases pass after default activation. They cover the omitted selector, explicit Basic, explicit Compressed, unknown values, and both delayed-policy guards. Evidence is `/tmp/inet-s5-default-variant12.out`. Retained Basic fragment, bitmap, and public API checks remain separate regression obligations. + +Both `N_BlockAck` and `Ac_BlockAck` also pass three consumer runs: explicit Basic, explicit Compressed, and the NED default. The third run uses bare INI `default`, which invokes OMNeT++ `acceptDefault()`. It verifies the resolved profile at all three HCF instances. Actual Compressed BAR/Block Ack and positive receipt bits complete that run. All six simulations and both diagnostic scripts pass. Evidence is `/tmp/inet-s5-default-consumer-protocol6.log` and `/tmp/inet-s5-default-consumer-{n3,ac3}.out`. + +Both final build freshness checks pass after the NED change. Their logs are `/tmp/inet-s5-c4-debug-build.log` and `/tmp/inet-s5-c4-release-build.log`. The C4 change affects the runtime NED default; the recorded debug and release library hashes remain unchanged. + +## Final regression evidence + +All final checks use the active checkout and seed zero where a simulation requires a seed. Debug mode retains runtime assertions. The release build supplies a separate compilation check. No fingerprint expectation changed. + +| Check | Final result | Evidence | +| --- | --- | --- | +| Unit fixtures | All ten pass. | `/tmp/inet-s5-c4-unit.log` | +| Module regression batch | Forty-one pass initially. The retained Basic deferral fixture passes after its explicit profile correction. All 42 selected fixtures therefore pass across the batch and focused rerun. | `/tmp/inet-s5-c4-module-regression.log`, `/tmp/inet-s5-c4-deferral-module.log` | +| Default infrastructure and migration | Both additional module fixtures pass: sixteen infrastructure cases and six migration cases. Both lowest-response cases pass. | `/tmp/inet-s5-default-module.log`, `/tmp/inet-s5-default-lowest2.out` | +| Serializer | All 278 constructed cases pass, with zero unexpected or stale coverage entries. The separate golden-octet unit also passes. | `/tmp/inet-s5-c4-serializer.log`, `/tmp/inet-s5-c4-unit.log` | +| Wi-Fi protocol files | Five supported files pass. Four deferred files retain expected failures. No unexpected result occurs. | `/tmp/inet-s5-default-plan-protocol9.log`, `/tmp/inet-s5-default-protocol-deferred.txt` | +| Fingerprint rows | All nineteen selected rows match in 153.811 seconds. | `/tmp/inet-s5-c4-fingerprint.log` | +| Architecture | All three scoped checks pass. | `/tmp/inet-s5-c4-architecture-mac.log`, `/tmp/inet-s5-c4-architecture-common.log`, `/tmp/inet-s5-c4-architecture-wifi.log` | + +The module union contains 44 distinct fixtures. It preserves Basic teardown, fragmentation, NAV, setup, association reset, response modes, and lifetime checks. It repeats the 28 physical interframe cases, 24 admission cases, and 76 scheduling cases after default activation. The scheduling fixture retains all original Basic cases. It adds actual Compressed shifted responses through active frame sequences and small-window arbitration. + +The recipient integration checks DATA 100, 102, and 101 through HCF. Delivery order is 100, 101, and 102. A subsequent BAR produces three positive receipt bits and zero holes after delivery. The six focused originator cases call the HCF control boundary directly. The scheduling fixture separately supplies actual response matching. These fixtures supply accepted agreements; the infrastructure and protocol fixtures prove negotiation separately. + +The legacy deferral fixture omitted a selector while it used non-HT mode `a`. The Compressed default correctly prevented its expected Basic setup. The fixture now selects Basic explicitly. All twelve original scenarios pass without assertion changes. This correction preserves the fixture's intended historical profile. + +The deferred protocol results remain explicit. `N_Ampdu` and `Ac_Ampdu` first miss an A-MPDU subframe. `Ac_MultiTidBa` passes both TID observations before it misses Multi-TID Block Ack. `WifiQosDelayedBa` observes a delayed request before recipient delayed acceptance fails. No expectation changed, and these results establish no support for those features. + +The interface gates report two MAC findings and three PHY findings. Their output is byte-identical to the pinned parent's output. The implicated source files remain unchanged. Logs are `/tmp/inet-s5-c4-interfaces-mac.log` and `/tmp/inet-s5-c4-interfaces-phy.log`, with corresponding `/tmp/inet-s5-parent-interfaces-*.log` files. This work adds no interface-gate finding. + +The final unit command runs from the checkout root: + +```sh +inet_run_unit_tests -m debug -f '(Ieee80211BlockAck(Wire|Scheduling|ActionWire)_1|Ieee80211CompressedBlockAck(Rate|Record)_1|Ieee80211(ResponseModeSelection|ResponseTimeout|HtModeSet|HtCapabilities|VhtHtCapabilities)_1)\.test$' +``` + +The module regression command runs from the checkout root: + +```sh +inet_run_module_tests -m debug -f 'Ieee80211(BlockAck.*|EdcaDataLifetime.*|PreparedCancellation_1|RecipientNav_1|Txnav_1|Hcf(SingleProtection_1|MultipleProtection_[12]|Header_1)|SingleProtectionGroup_1|VhtHtSupport_1|HtAssociation_1|Mgmt(StateCommit|RatePublication)_1|CompressedBlockAck(Input|Interframe|Admission|Originator)_1)\.test$' +inet_run_module_tests -m debug -f 'Ieee80211BlockAckDeferral_1\.test$' +inet_run_module_tests -m debug -f '(Ieee80211CompressedBlockAckExchange_1|Ieee80211CompressedBlockAckMigration_1|Ieee80211BlockAckVariant_1)\.test$' +``` + +The serializer and protocol commands run from the checkout root: + +```sh +inet_run_serializer_tests -m debug -f 'serializer_chunk_roundtrip\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|N_CompressedBa|N_Amsdu|N_Ampdu|Ac_BlockAck|Ac_BlockAckFallback|Ac_Ampdu|Ac_MultiTidBa|WifiQosDelayedBa)\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|Ac_BlockAck)\.test$' +``` + +The final fingerprint command repeats the nineteen-row command above from `tests/fingerprint`. It uses the original configurations, runs, time limits, and selected non-graphical ingredients. Graphical `tyf` remains unverified. No CSV or other recorded baseline changed. + +Current user and migration guides describe the default, fallback, profile conflict, fragmentation boundary, and added interface obligations. Release notes preserve the supported `ac` path and all four deferred protocol requirements. The Basic showcase retains its explicit profile, topology, and result rows. + +The evidence remains limited to the recorded deterministic cases and seed. Production HT catalogs provide no distinct legal alternate response for the tested 32-byte frame. The selector unit verifies that alternate-rate contract separately. The recipient prefix release remains an interpretation of ordered delivery together with the individual-MPDU release condition. This work establishes no complete historical Basic conformance or complete VHT transfer conformance. + +The implementation checks above precede the separate commit, push, and pull request phase. The user authorized that phase after implementation. The original local plan changes and untracked history remain outside the publication scope. diff --git a/plan/pending/ieee80211-compressed-blockack-txop-admission.md b/plan/pending/ieee80211-compressed-blockack-txop-admission.md new file mode 100644 index 00000000000..6664e786d49 --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-txop-admission.md @@ -0,0 +1,121 @@ +# TXOP prerequisite: admit Compressed DATA before destructive preparation + +This correction is complete and removes the active Compressed timing blocker. The [replacement plan](ieee80211-compressed-blockack-replacement.md#excluded-features-and-fragmentation) excludes complete-cost TXOP admission from its original scope. On 2026-10-16, the maintainer authorized this separate correction and subsequent S5 completion. The resolved contract below passed source validation and independent review before production edits. The [S5 execution record](ieee80211-compressed-blockack-s5-execution.md#authorized-txop-correction-and-c3-acceptance) supplies the final runtime evidence and scope limits. + +## Problem and required result + +The final S5 candidate sends two later A-MSDUs beyond their TXOP bounds in a supported case. The exact parent has no such violation in that case. The [S5 execution record](ieee80211-compressed-blockack-s5-execution.md#confirmed-timing-blocker) supplies the comparison and its limits. IEEE Std 802.11-2024, Clause 10.23.2.9 does not permit the observed continuation. + +The correction must establish the complete exchange cost before DATA commitment. An aggregate must fit its applicable bound. A later ordinary MSDU must also fit. The correction must retain the permitted initial unaggregated MSDU exception. It must not strand an oversized aggregate until lifetime expiry. + +A zero TXOP limit permits one SU PPDU with one MSDU or A-MSDU, plus its required protection and response. It does not mean zero permitted airtime. With a positive limit, the initial unaggregated MSDU exception permits at most one Data or Management frame. RTS and CTS airtime counts toward the exchange cost, but those Control frames do not increase that count. A same-size retransmission has a separate permitted exception under Clause 10.23.2.9; the implementation contract must state its retry policy. + +Hypothetical example: a TXOP has 200 microseconds left. The proposed aggregate needs 400 microseconds, while its first ordinary MSDU needs 150 microseconds. The service must commit that ordinary MSDU and leave the other MSDUs queued. If neither form qualifies, the sequence must end without queue removal or sequence assignment. + +The initial implementation scope covers active Compressed agreements. Explicit Basic and Normal Ack fallback outside an agreement retain their current preparation paths. This boundary avoids an unrelated compatibility repair. It does not claim that those retained paths satisfy every TXOP constraint. + +## Confirmed cause + +The [data service](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc) removes aggregation candidates before HCF selects the actual DATA mode. The aggregation builder deletes the constituent frames. The service replaces their lifetime records and assigns a sequence number. At that point, a final HCF refusal cannot recover the original queue form. + +The [frame sequence](../../src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc) tests whether positive TXOP time remains. That test does not prove that a complete later exchange fits. HCF excludes Block Ack support from its existing complete-cost DATA guard. A guard at final submission therefore needs a preparation correction as well. + +AARF and Onoe can change state when the rate selector queries them. Separate mode queries for a preview, RTS protection, and DATA submission can produce different decisions. The cost calculation must use the actual selected mode without extra rate-control queries. + +## Proposed ownership and sequence + +| Step | Responsible component and proposed action | Expected result and verification | +| --- | --- | --- | +| 1. Prepare without commitment. | The data service obtains borrowed candidates from the aggregation policy. It constructs a preview from duplicates. | Queue ownership, deadlines, and sequence counters remain unchanged. A focused fixture records them before and after refusal. | +| 2. Select one DATA mode. | HCF selects the mode once for this preparation. The frame-sequence context owns that decision and the preparation state. | Aggregate evaluation, ordinary-MSDU evaluation, protection, and submission use one mode decision. A test rate control counts queries. | +| 3. Calculate complete cost. | HCF derives the applicable IFS, DATA duration, protection, and immediate response from their existing owners. | Admission checks the TXOP bound and the applicable TXNAV bound. PHY mode objects supply durations. | +| 4. Select a permitted form. | HCF first evaluates the aggregate. If it fails, HCF evaluates the selected ordinary MSDU with the same mode. | An oversized aggregate does not cause permanent refusal. The ordinary MSDU can fit or qualify for the initial-MSDU exception. | +| 5. Defer when necessary. | The frame sequence ends normally when neither form qualifies. | The queue remains intact. Refusal causes no DATA retry outcome, sequence assignment, or aggregate lifetime replacement. | +| 6. Commit at a safe boundary. | The data service commits only the accepted form. HCF uses its retained mode for the actual exchange. | Extraction, lifetime replacement, and sequence assignment occur once. No committed aggregate waits indefinitely because its form cannot fit. | + +BAR arbitration must precede DATA preparation, as S4 requires. The correction adds no BAR forecast. A final BAR can obtain a later TXOP through the existing arbitration path. + +The existing final false-transmit result remains a safety check. The frame-sequence handler removes a refused step and ends the sequence without a transmission-failure result. Normal admission refusal must occur before destructive source commitment. + +## Boundary requirements + +RTS creates an event boundary between preparation and DATA submission. A capability update, lifetime expiry, or RTS failure can occur before DATA starts. The implementation contract must identify the owner of each preview and original frame throughout that interval. + +The resolved contract retains reversible preparation until the first DATA submission. A failed RTS or invalidated capability discards the preview and its selected mode. Original queue frames retain their existing deadlines and sequence state. A later attempt obtains a new preparation and mode decision. The source trace established that this boundary fits the existing ownership and lifetime contracts. + +The frame-sequence API must distinguish preparation refusal from an empty queue and a failed transmission. `InProgressFrames` must not convert those outcomes into a DATA retry. The integration must preserve S4's preview and BAR-first contracts. + +Admission refusal causes no DATA retry outcome. An actual RTS failure retains its required recovery behavior while the service discards reversible preparation. The implementation contract must keep those paths separate. + +For DATA under an active Block Ack agreement, an actual RTS failure updates QSRC and the contention window. QSRC is the short retry counter for an access category. Clause 10.23.2.12.1 applies its associated-RTS increment rule to MSDUs and A-MSDUs outside a Block Ack agreement. A preview must not create a sequence-keyed counter before sequence assignment. QSRC rollover therefore does not discard queued DATA; its lifetime still bounds repeated attempts. This changes INET's former per-frame RTS discard behavior on this scoped path. Basic and Normal Ack outside an agreement retain their existing recovery. + +At the preceding exchange completion, TXNAV admission compares complete exchange cost strictly below the remaining TXNAV. That cost excludes the leading IFS. Positive-limit TXOP containment includes the leading IFS and permits equality with the bound. Clauses 10.23.2.8 and 9.2.5.2 support these separate comparisons. The correction must not move the strict TXNAV comparison to a later reference that rejects an exact single-protection reservation. + +The correction must not retain an illegal mode after peer capabilities change. It must also avoid a second adaptive-rate query merely to validate an unchanged prepared decision. The implementation contract must specify that validation separately from mode selection. + +These are engineering checks for the approved scope. They do not require the maintainer to choose a cache format or callback signature. If source analysis needs a different behavior or a wider compatibility change, the owner must update this proposal first. + +## Resolved implementation contract + +The data service owns duplicate previews through `PreparedQosData`. Each preparation records original frame IDs and its source queue. It retains no original frame pointer across events. `prepareFrame(queue, allowAggregation)` creates one form without extraction. HCF first requests the aggregate form. If that form fails admission, HCF requests the ordinary form with the same DATA mode. + +`QoSContext` owns the selected preparation and its retained modes. HCF supplies a preparation callback with three outcomes: `LEGACY`, `READY`, and `DEFERRED`. `HcfFs` calls it after BAR arbitration and before extraction. Refusal ends the repeat normally. Legacy contexts retain their existing extraction route. + +`isPreparationValid()` checks source membership, eligibility, deadlines, next sequence, and A-MSDU permission. HCF separately validates every retained mode and response input through pure queries. These checks include mode-set changes. A changed input cancels preparation without another adaptive DATA query. HCF repeats validation before RTS and before DATA submission. + +`commitPreparedFrame()` removes the accepted sources only before first DATA submission. It transfers the same preview pointer to `InProgressFrames`. Ordinary commitment transfers the original absolute deadline. Aggregate commitment starts the existing A-MSDU lifetime. Commitment assigns one sequence number and allocation index. Frame-sequence steps retain valid borrowed pointers until their disposal. + +```mermaid +flowchart TD + Queue[Original MSDUs remain queued.] --> Preview[The service creates duplicate previews.] + Preview --> Admission{Does a permitted form qualify?} + Admission -->|No| Refuse[HCF ends the sequence without commitment.] + Admission -->|Yes| Protection{Does DATA require RTS?} + Protection -->|No| Validate{Are sources and retained modes valid?} + Protection -->|Yes| RTS[HCF submits RTS with the retained decision.] + RTS -->|CTS succeeds| Validate + RTS -->|CTS fails for valid sources| Recovery[Recovery updates QSRC and CW.] + RTS -->|Sources become invalid| Discard + Recovery --> Discard[Context disposal deletes the preview.] + Validate -->|No| Discard + Validate -->|Yes| Commit[The service commits the same preview pointer.] + Commit --> Data[HCF submits DATA with its retained mode.] +``` + +Expiry during CTS wait can remove originals safely because preparation stores their IDs. An invalid preparation ends without stale DATA or a synthetic RTS failure. A valid preparation with failed CTS causes one actual RTS recovery outcome. Context disposal deletes unused previews after step disposal. + +The scoped single-protection path reserves only its current exchange. It makes no forecast for unprepared successor DATA. A later BAR can contend through the existing S4 route. Multiple protection retains its reservation formula. This choice can increase contention and change simulation trajectories for active Compressed agreements. + +Committed retransmissions use the same-size single-MPDU exception from Clause 10.23.2.9 when no earlier Data or Management frame occurred in the TXOP. This exception can include a retransmitted A-MSDU. It does not permit an oversized first-attempt A-MSDU. The supported Compressed path has no fragmentation or A-MPDU operation. Its committed MPDU retains its size across retries; Retry and Duration field changes do not change that size. Basic and outside-agreement fallback retain their existing paths. + +A bounded LLDB observation established why conservative retry containment cannot satisfy the supported exchange. AP DATA at 6 Mbit/s needs a 1.166-millisecond protected exchange within a nominal one-millisecond TXOP. Its first ordinary transmission qualifies for the initial exception. A real BAR collision makes that DATA retry eligible. Conservative containment then refuses every attempt despite the permitted same-size exception. The root validated the required policy correction before source edits. Evidence is `/tmp/inet-s5-txop-diagnostic-lldb-cost.out`. + +Internal collision has a separate preparation entry. The existing HCF branch extracts DATA before it updates collision recovery. On the scoped queued path, HCF instead commits one ordinary preview before that existing recovery action. It must not create an aggregate before admission. Other queued MSDUs retain their ownership and deadlines. An unsent ordinary MSDU still qualifies for the first-transmission exception when its conditions hold. + +Multiple protection must reserve the actual exchange when a permitted initial ordinary MSDU or same-size retransmission exceeds a positive limit. The scoped path uses the existing helper's current-exchange form for that case. It must not reduce protection to zero merely because DATA exceeds the limit. The helper's reservation formula remains unchanged. + +The root validated this contract against the current source. An independent review found no unresolved blocking gap. This contract review supplies no runtime proof. Production implementation and focused verification follow this gate. + +## Proposed verification + +The direct reproduction uses `Ieee80211CompressedBlockAckExchange_1.test`, General run 6, seed zero. It selects HT operation, 24 Mbit/s DATA, single protection, aggregation, and a one-millisecond TXOP. The candidate must eliminate both 310-microsecond continuation overruns. The raw comparison remains in `/tmp/inet-s5-exchange-parent-candidate-comparison.json`. + +Add a focused preparation fixture at the proposed path `tests/module/Ieee80211CompressedBlockAckAdmission_1.test`. That fixture does not yet exist. Its cases must establish these results: + +- An initial aggregate that cannot fit becomes an ordinary MSDU before commitment. +- A permitted initial unaggregated MSDU can exceed the TXOP limit under the applicable exception. +- A smaller ordinary MSDU can replace an aggregate when it fits the remaining interval. +- Refusal preserves queue ownership, deadlines, and sequence counters when neither form qualifies. +- RTS failure, lifetime expiry, and capability changes discard invalid preparation without stale DATA transmission. +- RTS protection, DATA duration, and admission consume one selected mode. +- Basic controls preserve their existing behavior. + +Repeat S5's complete infrastructure campaign after the correction. Retain the lowest-response cases and exact BAR/Block Ack bounds. Repeat the affected lifetime, setup, frame-sequence, and fragmentation fixtures. Repeat the nineteen selected fingerprint rows without a baseline update. + +C3 can pass only after the supported corrected exchanges meet their applicable bounds. C4 and default activation must follow that result. This proposal grants no publication or fingerprint-baseline authority. + +## Execution result + +All 24 focused admission cases pass. All sixteen infrastructure cases pass the active Compressed timing assertions. Both lowest-response cases pass. Independent review accepts C3. C4 activates the default only after that decision and repeats its required checks. + +The former run 6 violation no longer occurs on either hop. Outside-agreement admission retains its inherited limits. `StrictAllTiming` preserves their separate reproduction. Basic settings and recorded fingerprint expectations remain unchanged. The final nineteen-row fingerprint campaign passes without a baseline update. diff --git a/showcases/wireless/blockack/doc/index.rst b/showcases/wireless/blockack/doc/index.rst index 1418ec4a985..b4808ed7c33 100644 --- a/showcases/wireless/blockack/doc/index.rst +++ b/showcases/wireless/blockack/doc/index.rst @@ -50,7 +50,7 @@ Block Acknowledgment in INET In INET, the block ack mechanism can be enabled by setting the :par:`isBlockAckSupported` parameter of the MAC's :ned:`Hcf` submodule to ``true`` (it is disabled by default). -Select ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default during preparation for Compressed support. This stage rejects ``"compressed"`` because its runtime exchange path does not yet exist. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. +This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. HCF defaults to Compressed for other configurations. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Several submodules of :ned:`Hcf` are involved in the block ack mechanism. These modules have parameters controlling the block ack behavior. The following paragraphs diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg new file mode 100644 index 00000000000..3d586aad6c0 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg @@ -0,0 +1,17 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Local allocation order before fragmentation. All fragments retain this index. +// Unlike the wire sequence number, the index does not wrap at 4096. +class Ieee80211DataSequenceTag extends TagBase +{ + int64_t allocationIndex = -1; +} diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index e139deaf75d..7d1367944d4 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -43,6 +43,8 @@ const b LENGTH_ADDBAREQ = LENGTH_MGMT + B(9); // mgmt length + action body lengt const b LENGTH_ADDBARESP = LENGTH_MGMT + B(9); // mgmt length + action body length const b LENGTH_DELBA = LENGTH_MGMT + B(6); // mgmt length + action body length const b LENGTH_BASIC_BLOCKACK = B(16 + 2 + (2 + 128) + 4); // header + ba control + ba information + fcs +const b LENGTH_COMPRESSED_BLOCKACK = B(16 + 2 + 2 + 8 + 4); // header + control + sequence + bitmap + FCS +const b LENGTH_BLOCKACKREQ = B(16 + 2 + 2 + 4); // header + control + sequence + FCS const b DATAFRAME_HEADER_MINLENGTH = B(2 + 2 + 3 * 6 + 2); //bits without QoS, without Address4: 2 + 2 + 3*6(addresses) + 2 const b QOSCONTROL_PART_LENGTH = b(2 * 8); // QoS Control field length (bits) const short int MAX_NUM_FRAGMENTS = 16; @@ -356,7 +358,7 @@ class Ieee80211BlockAckReq extends Ieee80211TwoAddressHeader class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(16 + 2 + 2); // header + BAR control + BAR information + chunkLength = LENGTH_BLOCKACKREQ - B(4); int tidInfo @bit(4); // The TID_INFO subfield of the BAR Control field of the Basic BlockAckReq frame contains the TID for which a Basic BlockAck frame is requested. @@ -373,7 +375,7 @@ class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq class Ieee80211CompressedBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(16 + 2 + 2); // header + BAR control + BAR information + chunkLength = LENGTH_BLOCKACKREQ - B(4); int tidInfo @bit(4); // The TID_INFO subfield of the BAR Control field of the Compressed BlockAckReq frame contains the TID for which a BlockAck frame is requested. // The BAR Information field of the Compressed BlockAckReq frame contains the Block Ack Starting @@ -455,7 +457,7 @@ class Ieee80211BasicBlockAck extends Ieee80211BlockAck // class Ieee80211CompressedBlockAck extends Ieee80211BlockAck { - // chunkLength TODO + chunkLength = LENGTH_COMPRESSED_BLOCKACK - B(4); multiTid = 0; compressedBitmap = 1; diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index b864c39f435..3cb053c2790 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -34,6 +34,13 @@ namespace inet { namespace ieee80211 { +void Ieee80211Mac::resetPeerAgreements(const MacAddress& peer) +{ + Enter_Method("resetPeerAgreements"); + if (hcf) + hcf->resetPeer(peer); +} + using namespace inet::physicallayer; Define_Module(Ieee80211Mac); @@ -83,6 +90,7 @@ void Ieee80211Mac::initialize(int stage) if (transmitter->isHtChannelWidthSupported(channelWidth) && receiver->isHtChannelWidthSupported(channelWidth)) operationalChannelWidths.insert(channelWidth); + receiver->validateHtReceptionCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); } mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); emit(modesetChangedSignal, modeSet); @@ -202,6 +210,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/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 25d1b25fe65..0b28e4a8bc0 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -11,6 +11,7 @@ #include "inet/common/ModuleRefByPar.h" #include "inet/linklayer/base/MacProtocolBase.h" #include "inet/linklayer/ieee80211/mac/contract/IDs.h" +#include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementLifecycle.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" @@ -35,7 +36,7 @@ class Ieee80211MacHeader; * exact operation of the MAC depend on the plugged-in components (see IUpperMac, * IRx, ITx, IContention and other interface classes). */ -class INET_API Ieee80211Mac : public MacProtocolBase +class INET_API Ieee80211Mac : public MacProtocolBase, public IBlockAckAgreementLifecycle { public: static simsignal_t frameTransmissionOutcomeSignal; @@ -106,6 +107,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual void sendUpFrame(Packet *frame); virtual void sendDownFrame(Packet *frame); virtual void sendDownPendingRadioConfigMsg(); + virtual void resetPeerAgreements(const MacAddress& peer) override; virtual void processUpperFrame(Packet *packet, const Ptr& header); virtual void processLowerFrame(Packet *packet, const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 2a482cded2f..d2b1ae642fe 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -331,10 +331,13 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeMacAddress(blockAckReq->getTransmitterAddress()); bool multiTid = blockAckReq->getMultiTid(); bool compressedBitmap = blockAckReq->getCompressedBitmap(); - // BAR Control is a 16-bit little-endian field: bit 0 BAR Ack Policy, bit 1 - // Multi-TID, bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO + // IEEE Std 802.11-2024, 9.3.1.7: B1-B4 encode the complete BAR Type. + if (!multiTid && !compressedBitmap && (blockAckReq->getReserved() & 3)) + throw cRuntimeError("Basic BAR reserved fields must encode Type 0"); uint16_t barControl = (blockAckReq->getBarAckPolicy() ? 1 : 0) | ((multiTid ? 1 : 0) << 1) | ((compressedBitmap ? 1 : 0) << 2) | ((blockAckReq->getReserved() & 0x1FF) << 3); + if (!multiTid && compressedBitmap) + barControl = 2 << 1; if (!multiTid && !compressedBitmap) { auto basicBlockAckReq = CHK(dynamicPtrCast(chunk)); stream.writeUint16Le(barControl | ((basicBlockAckReq->getTidInfo() & 0xF) << 12)); @@ -347,6 +350,8 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAckReq = CHK(dynamicPtrCast(chunk)); + if (compressedBlockAckReq->getFragmentNumber() != 0) + throw cRuntimeError("Compressed BAR Fragment Number must be zero"); stream.writeUint16Le(barControl | ((compressedBlockAckReq->getTidInfo() & 0xF) << 12)); writeSequenceControl(stream, compressedBlockAckReq->getFragmentNumber(), compressedBlockAckReq->getStartingSequenceNumber().get()); if (stream.getLength() - startPos != compressedBlockAckReq->getChunkLength()) @@ -367,10 +372,13 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeMacAddress(blockAck->getTransmitterAddress()); bool multiTid = blockAck->getMultiTid(); bool compressedBitmap = blockAck->getCompressedBitmap(); - // BA Control is a 16-bit little-endian field: bit 0 Ack Policy, bit 1 Multi-TID, - // bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO + // IEEE Std 802.11-2024, 9.3.1.8: B1-B4 encode the complete BA Type. + if (!multiTid && !compressedBitmap && (blockAck->getReserved() & 3)) + throw cRuntimeError("Basic Block Ack reserved fields must encode Type 0"); uint16_t baControl = (blockAck->getBlockAckPolicy() ? 1 : 0) | ((multiTid ? 1 : 0) << 1) | ((compressedBitmap ? 1 : 0) << 2) | ((blockAck->getReserved() & 0x1FF) << 3); + if (!multiTid && compressedBitmap) + baControl = 2 << 1; if (!multiTid && !compressedBitmap) { auto basicBlockAck = CHK(dynamicPtrCast(chunk)); stream.writeUint16Le(baControl | ((basicBlockAck->getTidInfo() & 0xF) << 12)); @@ -386,6 +394,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAck = CHK(dynamicPtrCast(chunk)); + if (compressedBlockAck->getFragmentNumber() != 0) + throw cRuntimeError("Compressed Block Ack Fragment Number must be zero"); + if (compressedBlockAck->getBlockAckBitmap().getSize() != 64) + throw cRuntimeError("Compressed Block Ack bitmap must contain 64 bits"); stream.writeUint16Le(baControl | ((compressedBlockAck->getTidInfo() & 0xF) << 12)); // Block Ack Starting Sequence Control = 4-bit Fragment Number (reserved) + 12-bit sequence number writeSequenceControl(stream, compressedBlockAck->getFragmentNumber(), compressedBlockAck->getStartingSequenceNumber().get()); @@ -600,6 +612,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setReceiverAddress(stream.readMacAddress()); blockAckReq->setTransmitterAddress(stream.readMacAddress()); uint16_t barControl = stream.readUint16Le(); + auto barType = (barControl >> 1) & 0xF; bool multiTid = (barControl >> 1) & 1; bool compressedBitmap = (barControl >> 2) & 1; blockAckReq->setBarAckPolicy(barControl & 1); @@ -608,7 +621,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setReserved((barControl >> 3) & 0x1FF); int fragmentNumber = 0; SequenceNumberCyclic startingSequenceNumber; - if (!multiTid && !compressedBitmap) { + if (barType == 0) { auto basicBlockAckReq = makeShared(); copyBasicFields(basicBlockAckReq, macHeader); copyBlockAckReqFrameFields(basicBlockAckReq, blockAckReq); @@ -618,14 +631,18 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre basicBlockAckReq->setStartingSequenceNumber(startingSequenceNumber); return basicBlockAckReq; } - else if (!multiTid && compressedBitmap) { + else if (barType == 2) { auto compressedBlockAckReq = makeShared(); copyBasicFields(compressedBlockAckReq, macHeader); copyBlockAckReqFrameFields(compressedBlockAckReq, blockAckReq); + compressedBlockAckReq->setBarAckPolicy(false); + compressedBlockAckReq->setReserved(0); compressedBlockAckReq->setTidInfo((barControl >> 12) & 0xF); readSequenceControl(stream, fragmentNumber, startingSequenceNumber); compressedBlockAckReq->setFragmentNumber(fragmentNumber); compressedBlockAckReq->setStartingSequenceNumber(startingSequenceNumber); + if (fragmentNumber != 0) + compressedBlockAckReq->markIncorrect(); return compressedBlockAckReq; } else @@ -640,9 +657,8 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setDurationField(SimTime(stream.readUint16Le(), SIMTIME_US)); blockAck->setReceiverAddress(stream.readMacAddress()); blockAck->setTransmitterAddress(stream.readMacAddress()); - // BA Control is a 16-bit little-endian field: bit 0 Ack Policy, bit 1 Multi-TID, - // bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO uint16_t baControl = stream.readUint16Le(); + auto baType = (baControl >> 1) & 0xF; bool multiTid = (baControl >> 1) & 1; bool compressedBitmap = (baControl >> 2) & 1; uint8_t tidInfo = (baControl >> 12) & 0xF; @@ -650,7 +666,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setMultiTid(multiTid); blockAck->setCompressedBitmap(compressedBitmap); blockAck->setReserved((baControl >> 3) & 0x1FF); - if (!multiTid && !compressedBitmap) { + if (baType == 0) { auto basicBlockAck = makeShared(); copyBasicFields(basicBlockAck, macHeader); copyBlockAckFrameFields(basicBlockAck, blockAck); @@ -664,15 +680,16 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre std::vector bytes; bytes.push_back(stream.readByte()); bytes.push_back(stream.readByte()); - BitVector *blockAckBitmap = new BitVector(bytes); - basicBlockAck->setBlockAckBitmap(i, *blockAckBitmap); + basicBlockAck->setBlockAckBitmap(i, BitVector(bytes)); } return basicBlockAck; } - else if (!multiTid && compressedBitmap) { + else if (baType == 2) { auto compressedBlockAck = makeShared(); copyBasicFields(compressedBlockAck, macHeader); copyBlockAckFrameFields(compressedBlockAck, blockAck); + compressedBlockAck->setBlockAckPolicy(false); + compressedBlockAck->setReserved(0); compressedBlockAck->setTidInfo(tidInfo); // Block Ack Starting Sequence Control = 4-bit Fragment Number (reserved) + 12-bit @@ -684,7 +701,9 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre for (size_t i = 0; i < 8; ++i) { bytes.push_back(stream.readByte()); } - compressedBlockAck->setBlockAckBitmap(*(new BitVector(bytes))); + compressedBlockAck->setBlockAckBitmap(BitVector(bytes)); + if (fragmentNumber != 0) + compressedBlockAck->markIncorrect(); return compressedBlockAck; } else { diff --git a/src/inet/linklayer/ieee80211/mac/Rx.cc b/src/inet/linklayer/ieee80211/mac/Rx.cc index b51f7f3edf7..7f7ec187b3d 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.cc +++ b/src/inet/linklayer/ieee80211/mac/Rx.cc @@ -12,6 +12,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/IContention.h" #include "inet/linklayer/ieee80211/mac/contract/ITx.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" namespace inet { namespace ieee80211 { @@ -104,7 +105,11 @@ bool Rx::lowerFrameReceived(Packet *packet) Enter_Method("lowerFrameReceived(\"%s\")", packet->getName()); take(packet); - bool isFrameOk = isFcsOk(packet); + // 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->getHeaderSuccessful() || !aggregate->getContentComplete() || + aggregate->getAggregation())) && isFcsOk(packet); if (isFrameOk) { EV_INFO << "Received frame from PHY: " << packet << endl; const auto& header = packet->peekAtFront(); @@ -124,6 +129,72 @@ 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()) { + auto successful = [indication](int kind, B offset, B length) { + for (size_t i = 0; i < indication->getRangesArraySize(); i++) { + const auto& range = indication->getRanges(i); + if (range.kind == kind && range.offset == offset && range.length == length) + return range.valid && range.successful; + } + return false; + }; + // IEEE Std 802.11-2024, 9.7.1: a failed delimiter forces four-octet resynchronization. + auto layout = inspectMpduAggregate(packet, B(0), B(packet->getDataLength()), + [&successful](B offset, B length) { return successful(0, offset, length); }); + topology->ranges = layout.ranges; + topology->length = B(packet->getDataLength()); + bool first = true; + for (const auto& range : layout.ranges) { + if (range.kind == MpduRangeKind::DELIMITER) { + if (!range.valid) + first = false; + continue; + } + topology->members.push_back({range.offset, {}, first}); + first = false; + auto member = std::make_unique(packet->getName()); + member->insertAtBack(packet->peekDataAt(range.offset, range.length)); + member->getRegionTags().copyTags(packet->getRegionTags(), packet->getFrontOffset() + range.offset, + member->getFrontOffset(), range.length); + member->copyTags(*packet); + member->removeTagIfPresent(); + // The member outcome precedes every FCS mode, including declared-correct FCS. + member->setBitError(!successful(1, range.offset, range.length)); + bool correct = range.length >= B(4) && member->hasAtBack(B(4)) && + isFcsOk(member.get()) && member->hasAtFront(); + if (!correct) { + PacketDropDetails details; + details.setReason(INCORRECTLY_RECEIVED); + emit(packetDroppedSignal, member.get(), &details); + continue; + } + 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), range.offset}); + } + } + // 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"); diff --git a/src/inet/linklayer/ieee80211/mac/Rx.h b/src/inet/linklayer/ieee80211/mac/Rx.h index e6d6e81ca08..8f079bbe1a7 100644 --- a/src/inet/linklayer/ieee80211/mac/Rx.h +++ b/src/inet/linklayer/ieee80211/mac/Rx.h @@ -60,6 +60,7 @@ class INET_API Rx : public SimpleModule, public IRx virtual void transmissionStateChanged(physicallayer::IRadio::TransmissionState transmissionState) override; virtual void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart part) override; virtual bool lowerFrameReceived(Packet *packet) 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/Tx.cc b/src/inet/linklayer/ieee80211/mac/Tx.cc index 52f046613c0..1a3d8def863 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.cc +++ b/src/inet/linklayer/ieee80211/mac/Tx.cc @@ -47,9 +47,21 @@ 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); + if (ifs == 0) + startTransmission(); + 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,23 +84,63 @@ 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()); - } +} + +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(); @@ -109,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 7e71e6a57ea..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,7 +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; - [[nodiscard]] bool hasTransmission() const override { return txCallback != nullptr; } + 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 || aggregateCallback != nullptr; } virtual void radioTransmissionFinished() override; }; 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/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/aggregation/MpduAggregationUtils.cc b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.cc new file mode 100644 index 00000000000..b16044a11c4 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.cc @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.h" + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +MpduLayout inspectMpduAggregate(const Packet *packet, B offset, B length, + const std::function& delimiterSuccessful) +{ + MpduLayout layout; + if (offset < B(0) || length < B(0) || offset + length > packet->getDataLength()) { + layout.complete = false; + return layout; + } + B position(0); + // IEEE Std 802.11-2024, 9.7.1: delimiter boundaries are four octets apart after loss of continuity. + while (position < length) { + if (length - position < B(4)) { + layout.ranges.push_back({MpduRangeKind::DELIMITER, position, length - position, false}); + layout.complete = false; + break; + } + // Recovery can enter a typed MPDU. Interpret its candidate delimiter bytes explicitly. + const auto& delimiter = packet->peekDataAt(offset + position, B(4), + Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED | + Chunk::PF_ALLOW_REINTERPRETATION); + bool valid = delimiter->isCorrect() && delimiter->isComplete() && delimiter->isProperlyRepresented() && + delimiter->getLength() >= 0 && delimiter->getLength() <= 4095 && + B(delimiter->getLength()) <= length - position - B(4) && + (!delimiterSuccessful || delimiterSuccessful(position, B(4))); + layout.ranges.push_back({MpduRangeKind::DELIMITER, position, B(4), valid}); + position += B(4); + if (!valid) { + layout.complete = false; + continue; + } + B memberLength(delimiter->getLength()); + if (memberLength == B(0)) + continue; + layout.ranges.push_back({MpduRangeKind::MPDU, position, memberLength, true}); + position += memberLength; + if (position == length) + break; + B padding((4 - memberLength.get() % 4) % 4); + if (padding > length - position) { + layout.ranges.push_back({MpduRangeKind::DELIMITER, position, length - position, false}); + layout.complete = false; + break; + } + position += padding; + if (position == length) { + layout.ranges.push_back({MpduRangeKind::DELIMITER, position - padding, padding, false}); + layout.complete = false; // HT has no final alignment padding. + } + } + return layout; +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.h b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.h new file mode 100644 index 00000000000..7f58c37b6e2 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_MPDUAGGREGATIONUTILS_H +#define __INET_MPDUAGGREGATIONUTILS_H + +#include + +#include "inet/common/packet/Packet.h" + +namespace inet { +namespace ieee80211 { + +enum class MpduRangeKind { DELIMITER, MPDU }; + +struct INET_API MpduRange +{ + MpduRangeKind kind; + B offset; + B length; + // Delimiter validity includes header flags and member bounds. MPDU validity requires a complete range. + bool valid; +}; + +struct INET_API MpduLayout +{ + std::vector ranges; + bool complete = true; +}; + +// Offsets are relative to the PSDU, without the PHY header or PHY padding. +INET_API MpduLayout inspectMpduAggregate(const Packet *packet, B offset, B length, + const std::function& delimiterSuccessful = {}); + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc index 01729897c5d..5dfadcaace3 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.cc @@ -6,6 +6,7 @@ #include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.h" namespace inet { namespace ieee80211 { @@ -16,20 +17,19 @@ std::vector *MpduDeaggregation::deaggregateFrame(Packet *aggregatedFra { EV_DEBUG << "Deaggregating A-MPDU " << *aggregatedFrame << " into multiple packets.\n"; std::vector *frames = new std::vector(); - int paddingLength = 0; + auto layout = inspectMpduAggregate(aggregatedFrame, B(0), B(aggregatedFrame->getDataLength())); + // An incomplete layout can contain bounded recovered members. Extraction does not establish MAC acceptance. + // The caller must check delimiter validity, PHY outcomes, member FCS, and MAC conditions before acceptance. cStringTokenizer tokenizer(aggregatedFrame->getName(), "+"); - while (aggregatedFrame->getDataLength() > b(0)) { - aggregatedFrame->setFrontOffset(aggregatedFrame->getFrontOffset() + B(paddingLength == 4 ? 0 : paddingLength)); - const auto& mpduSubframeHeader = aggregatedFrame->popAtFront(); - const auto& mpdu = aggregatedFrame->peekDataAt(b(0), B(mpduSubframeHeader->getLength())); - paddingLength = 4 - (mpduSubframeHeader->getChunkLength() + mpdu->getChunkLength()).get() % 4; - aggregatedFrame->setFrontOffset(aggregatedFrame->getFrontOffset() + mpdu->getChunkLength()); + for (const auto& range : layout.ranges) { + if (range.kind != MpduRangeKind::MPDU) + continue; + const auto& mpdu = aggregatedFrame->peekDataAt(range.offset, range.length); auto frame = new Packet(); - frame->setName(tokenizer.nextToken()); + frame->setName(tokenizer.hasMoreTokens() ? tokenizer.nextToken() : aggregatedFrame->getName()); frame->insertAtBack(mpdu); - frame->getRegionTags().copyTags(aggregatedFrame->getRegionTags(), aggregatedFrame->getFrontOffset() - frame->getDataLength(), frame->getFrontOffset(), frame->getDataLength()); + frame->getRegionTags().copyTags(aggregatedFrame->getRegionTags(), aggregatedFrame->getFrontOffset() + range.offset, frame->getFrontOffset(), range.length); EV_TRACE << "Created " << *frame << " from A-MPDU.\n"; - // TODO check CRC? frames->push_back(frame); } delete aggregatedFrame; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 919bd813a6f..fa285580703 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -53,6 +53,50 @@ void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) } } +void BlockAckRecord::initializeCompressedWindow(SequenceNumberCyclic start, int size) +{ + ASSERT(start.isSpecified() && size > 0 && size <= 64); + windowStart = start; + windowSize = size; + completeReceipts.clear(); +} + +void BlockAckRecord::advanceCompressedWindow(SequenceNumberCyclic start) +{ + // IEEE Std 802.11-2024, 10.25.6.3: retain explicit receipt truth at WinStartR - 1. + if (!(windowStart < start)) + return; + windowStart = start; + for (auto it = completeReceipts.begin(); it != completeReceipts.end();) { + auto distance = (*it - windowStart.get() + 4096) % 4096; + if (distance >= windowSize && distance != 4095) + it = completeReceipts.erase(it); + else + ++it; + } +} + +void BlockAckRecord::compressedFrameReceived(const Ptr& header) +{ + ASSERT(header->getFragmentNumber() == 0 && !header->getMoreFragments()); + auto sequence = header->getSequenceNumber(); + // IEEE Std 802.11-2024, 10.25.6.3: advance before duplicate removal or reorder insertion. + if (windowStart <= sequence && !(sequence < windowStart + windowSize)) + advanceCompressedWindow(sequence - windowSize + 1); + if (windowStart <= sequence && sequence < windowStart + windowSize) + completeReceipts.insert(sequence.get()); +} + +bool BlockAckRecord::hasCompleteReceipt(SequenceNumberCyclic sequenceNumber) const +{ + return completeReceipts.count(sequenceNumber.get()) != 0; +} + +bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) const +{ + return windowStart <= sequenceNumber && sequenceNumber < windowStart + windowSize && hasCompleteReceipt(sequenceNumber); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h index 971be8a8af9..1c90bf392dc 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -25,6 +25,9 @@ class INET_API BlockAckRecord MacAddress originatorAddress = MacAddress::UNSPECIFIED_ADDRESS; Tid tid = -1; std::map acknowledgmentState; + SequenceNumberCyclic windowStart; + int windowSize = 64; // Sequence positions; the Compressed profile permits 1..64. + std::set completeReceipts; public: BlockAckRecord(MacAddress originatorAddress, Tid tid); @@ -33,6 +36,12 @@ class INET_API BlockAckRecord void blockAckPolicyFrameReceived(const Ptr& header); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); void removeAckStates(SequenceNumberCyclic sequenceNumber); + void initializeCompressedWindow(SequenceNumberCyclic start, int size); + void compressedFrameReceived(const Ptr& header); + void advanceCompressedWindow(SequenceNumberCyclic start); + bool getCompressedAckState(SequenceNumberCyclic sequenceNumber) const; + bool hasCompleteReceipt(SequenceNumberCyclic sequenceNumber) const; + SequenceNumberCyclic getWindowStart() const { return windowStart; } MacAddress getOriginatorAddress() { return originatorAddress; } Tid getTid() { return tid; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..a6363c5dded 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -8,6 +8,8 @@ #ifndef __INET_ORIGINATORBLOCKACKAGREEMENT_H #define __INET_ORIGINATORBLOCKACKAGREEMENT_H +#include + #include "inet/linklayer/ieee80211/mac/common/SequenceControlField.h" namespace inet { @@ -24,13 +26,19 @@ class INET_API OriginatorBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; bool isAMsduSupported = false; + bool responseAMsduSupported = false; + int dialogToken = -1; + int64_t highestTransmittedDataIndex = -1; bool isDelayedBlockAckPolicySupported = false; bool isAddbaResponseReceived = false; bool isAddbaRequestSent = false; 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), @@ -44,11 +52,47 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual ~OriginatorBlockAckAgreement() {} virtual int getBufferSize() const { return bufferSize; } + int getWindowSize() const { return std::min(bufferSize, 64); } + static int computeSequenceDistance(SequenceNumberCyclic start, SequenceNumberCyclic sequence) + { + return (sequence.get() - start.get() + 4096) % 4096; + } + bool containsSequence(SequenceNumberCyclic sequence) const + { + return computeSequenceDistance(startingSequenceNumber, sequence) < getWindowSize(); + } + // The Basic and Compressed bitmaps cover 64 sequence positions, independent of the accepted window size. + static bool isInBitmap(SequenceNumberCyclic sequence, SequenceNumberCyclic bitmapStart) + { + return computeSequenceDistance(bitmapStart, sequence) < 64; + } + // Preserve the earliest unresolved position, or advance to the next allocation when none remains. + void advanceWindow(SequenceNumberCyclic nextSequence, const std::vector& unresolvedSequences) + { + auto start = getStartingSequenceNumber(); + int distance = computeSequenceDistance(start, nextSequence); + for (auto sequence : unresolvedSequences) + distance = std::min(distance, computeSequenceDistance(start, sequence)); + setStartingSequenceNumber(start + distance); + } virtual SequenceNumberCyclic getStartingSequenceNumber() { return startingSequenceNumber; } virtual void setStartingSequenceNumber(SequenceNumberCyclic sequenceNumber) { startingSequenceNumber = sequenceNumber; } virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } + // IEEE Std 802.11-2024, 10.25.2 defines ADDBA negotiation; INET requires both flags before A-MSDU aggregation. + bool isAMsduPermitted() const { return isAMsduSupported && responseAMsduSupported; } + int getDialogToken() const { return dialogToken; } + void setDialogToken(int token) { dialogToken = token; } + void setResponseAMsduSupported(bool value) { responseAMsduSupported = value; } + bool getFirstDataTransmitted() const { return highestTransmittedDataIndex >= 0; } + int64_t getHighestTransmittedDataIndex() const { return highestTransmittedDataIndex; } + void recordTransmittedData(int64_t allocationIndex) + { + ASSERT(allocationIndex >= 0); + if (highestTransmittedDataIndex < allocationIndex) + highestTransmittedDataIndex = allocationIndex; + } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } virtual MacAddress getReceiverAddr() const { return receiverAddr; } virtual Tid getTid() const { return tid; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2f11dc365db..c526b7fe88c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -8,15 +8,108 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" namespace inet { namespace ieee80211 { +void OriginatorBlockAckAgreementHandler::noteEligibleDataTransmission(MacAddress peer, Tid tid) +{ + Procedure procedure; + procedure.state = ProcedureState::WAITING_FOR_SUCCESSOR; + procedures.emplace(AgreementId(peer, tid), procedure); +} + +void OriginatorBlockAckAgreementHandler::beginSetupIntent(MacAddress peer, Tid tid) +{ + auto result = procedures.emplace(AgreementId(peer, tid), Procedure()); + if (result.first->second.state == ProcedureState::WAITING_FOR_SUCCESSOR) + result.first->second.state = ProcedureState::SETUP_INTENT; +} + +void OriginatorBlockAckAgreementHandler::beginTeardown(MacAddress peer, Tid tid) +{ + Procedure procedure; + procedure.state = ProcedureState::TEARDOWN_PENDING; + procedures[{peer, tid}] = procedure; +} + +void OriginatorBlockAckAgreementHandler::failTeardown(MacAddress peer, Tid tid) +{ + Procedure procedure; + procedure.state = ProcedureState::RECOVERY_NEEDED; + procedures[{peer, tid}] = procedure; +} + +void OriginatorBlockAckAgreementHandler::finishProcedure(MacAddress peer, Tid tid) +{ + procedures.erase({peer, tid}); +} + +void OriginatorBlockAckAgreementHandler::markRequestAttempted(MacAddress peer, Tid tid) +{ + auto it = procedures.find({peer, tid}); + if (it != procedures.end() && it->second.state == ProcedureState::REQUEST_PENDING) + it->second.requestAttempted = true; +} + +void OriginatorBlockAckAgreementHandler::acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) +{ + auto it = procedures.find({peer, tid}); + if (it != procedures.end() && it->second.state == ProcedureState::REQUEST_PENDING) + it->second.responseDeadline = simTime() + timeout; +} + void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest) { OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; + blockAckAgreement->setDialogToken(addbaRequest->getDialogToken()); + procedures[agreementId].state = ProcedureState::REQUEST_PENDING; +} + +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::detachAgreement(MacAddress peer, Tid tid) +{ + auto it = blockAckAgreements.find({peer, tid}); + if (it == blockAckAgreements.end()) + return nullptr; + auto agreement = it->second; + blockAckAgreements.erase(it); + return agreement; +} + +std::vector OriginatorBlockAckAgreementHandler::detachPeer(MacAddress peer) +{ + std::vector removed; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + if (it->first.first == peer) { + removed.push_back(it->second); + it = blockAckAgreements.erase(it); + } + else + ++it; + } + for (auto it = procedures.begin(); it != procedures.end();) { + if (it->first.first == peer) + it = procedures.erase(it); + else + ++it; + } + return removed; +} + +void OriginatorBlockAckAgreementHandler::startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, + IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) +{ + auto request = buildAddbaRequest(peer, tid, sequence, policy); + request->setAMsduSupported(aMsdu && policy->isMsduSupported()); + // IEEE Std 802.11-2024, Table 9-466: ordinary setup uses a nonzero dialog token. + auto& token = dialogTokens[{peer, tid}]; + token = token % 255 + 1; + request->setDialogToken(token); + createAgreement(request); + callback->processMgmtFrame(new Packet("AddbaReq", request), request); } simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() @@ -40,15 +133,16 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + std::vector expired; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { - MacAddress receiverAddr = id.first.first; - Tid tid = id.first.second; - const auto& delba = buildDelba(receiverAddr, tid, 39); - auto delbaPacket = new Packet("Delba", delba); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + if (agreement->getIsAddbaResponseReceived() && agreement->getExpirationTime() <= now) + expired.push_back(id.first); + } + for (const auto& id : expired) { + const auto& delba = buildDelba(id.first, id.second, 39); + auto delbaPacket = new Packet("Delba", delba); + procedureCallback->processMgmtFrame(delbaPacket, delba); } scheduleInactivityTimer(agreementHandlerCallback); } @@ -76,7 +170,13 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); if (agreement) { - agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } + } + else if (auto compressed = dynamicPtrCast(blockAck)) { + auto agreement = getAgreement(compressed->getTransmitterAddress(), compressed->getTidInfo()); + if (agreement) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } @@ -87,9 +187,7 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + callback->scheduleInactivityTimer(); } OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) @@ -124,12 +222,8 @@ void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originato void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) { auto agreement = getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); - if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr) { - auto addbaReq = buildAddbaRequest(dataHeader->getReceiverAddress(), dataHeader->getTid(), dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); - createAgreement(addbaReq); - auto addbaPacket = new Packet("AddbaReq", addbaReq); - callback->processMgmtFrame(addbaPacket, addbaReq); - } + if (agreement && agreement->getIsAddbaResponseReceived()) + agreement->recordTransmittedData(packet->getTag()->getAllocationIndex()); } void OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -149,6 +243,7 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree agreement->setIsAddbaResponseReceived(true); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); + agreement->setResponseAMsduSupported(addbaResp->getAMsduSupported()); agreement->calculateExpirationTime(); } @@ -157,13 +252,11 @@ void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(const PtrgetReceiverAddress(), addbaReq->getTid()); if (agreement) agreement->setIsAddbaRequestSent(true); - else - throw cRuntimeError("Block Ack Agreement should have already been added"); } void OriginatorBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + // HCF retires the agreement before it queues DELBA. ACK completes teardown. } void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4a449d17da3..af0c99d0cb7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,6 +21,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg { protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; + std::map procedures; + std::map dialogTokens; protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); @@ -28,10 +30,22 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); - virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: + virtual simtime_t computeEarliestExpirationTime() override; + const std::map& getAgreements() const override { return blockAckAgreements; } + const std::map& getProcedures() const override { return procedures; } + virtual void noteEligibleDataTransmission(MacAddress peer, Tid tid) override; + virtual void beginSetupIntent(MacAddress peer, Tid tid) override; + virtual void beginTeardown(MacAddress peer, Tid tid) override; + virtual void failTeardown(MacAddress peer, Tid tid) override; + virtual void finishProcedure(MacAddress peer, Tid tid) override; + virtual void markRequestAttempted(MacAddress peer, Tid tid) override; + virtual void acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) override; + OriginatorBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) override; + std::vector detachPeer(MacAddress peer) override; + void startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) override; virtual ~OriginatorBlockAckAgreementHandler(); virtual void processTransmittedAddbaReq(const Ptr& addbaReq) override; virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 44b4c0874fa..1a03dd26ef2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -9,6 +9,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { @@ -24,15 +25,25 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) aMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); - // TODO addbaFailureTimeout = par("addbaFailureTimeout"); + addbaFailureTimeout = par("addbaFailureTimeout"); + if (addbaFailureTimeout <= SIMTIME_ZERO || addbaFailureTimeout == SIMTIME_MAX) + throw cRuntimeError("addbaFailureTimeout must be positive and finite"); + if (maximumAllowedBufferSize <= 0 || blockAckTimeoutValue < SIMTIME_ZERO) + throw cRuntimeError("Invalid Block Ack buffer size or inactivity timeout"); WATCH(blockAckReqThreshold); } } simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const { - // TODO ADDBAFailureTimeout -- 6.3.29.2.2 Semantics of the service primitive - throw cRuntimeError("Unimplemented"); + return addbaFailureTimeout; +} + +bool OriginatorBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const +{ + if (variant == BlockAckVariant::COMPRESSED && (!localHt || !peerHt)) + EV_DEBUG << "Compressed Block Ack setup refused: " << (!localHt ? "local HT state is absent" : "peer HT state is absent") << "; DATA uses Normal Ack.\n"; + return variant == BlockAckVariant::BASIC || (localHt && peerHt); } bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const Ptr& header) @@ -42,8 +53,16 @@ bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const P bool OriginatorBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) { - ASSERT(agreement); - return true; + // IEEE Std 802.11-2024, 10.25.2 and Table 9-466: match the pending setup and its nonzero dialog token. + // Immediate Block Ack and the 64-entry bound are INET profile limits for historical Basic compatibility. + return agreement != nullptr && !agreement->getIsAddbaResponseReceived() && + addbaResp->getTransmitterAddress() == agreement->getReceiverAddr() && + addbaResp->getTid() == agreement->getTid() && + addbaResp->getDialogToken() > 0 && addbaResp->getDialogToken() <= 255 && + addbaResp->getDialogToken() == agreement->getDialogToken() && + addbaResp->getStatusCode() == SC_SUCCESSFUL && addbaResp->getBlockAckPolicy() && + addbaResp->getBufferSize() > 0 && addbaResp->getBufferSize() <= 64 && + addbaResp->getBlockAckTimeoutValue() >= SIMTIME_ZERO; } bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index b762cc29828..b0b7e9cce24 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -32,6 +32,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi virtual void initialize(int stage) override; public: + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override; virtual bool isAddbaReqNeeded(Packet *packet, const Ptr& header) override; virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) override; virtual bool isDelbaAccepted(const Ptr& delba) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index bbb569b4b20..ab9970f883f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -21,8 +21,9 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); - int maximumAllowedBufferSize = default(64); - double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + int maximumAllowedBufferSize = default(64); // The ADDBA Request uses this advisory buffer size; the recipient response determines the accepted allocation. + double blockAckTimeoutValue @unit(s) = default(0s); // Zero disables inactivity expiration. + double addbaFailureTimeout @unit(s) = default(100ms); // Response wait after request ACK. @display("i=block/control"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc index d02e412b644..ee1960c3fb0 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc @@ -12,13 +12,11 @@ namespace ieee80211 { const Ptr OriginatorBlockAckProcedure::buildCompressedBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const { - throw cRuntimeError("Unsupported feature"); - // TODO implement - // auto blockAckReq = makeShared(); - // blockAckReq->setReceiverAddress(receiverAddress); - // blockAckReq->setStartingSequenceNumber(startingSequenceNumber); - // blockAckReq->setTidInfo(tid); - // return blockAckReq; + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(receiverAddress); + blockAckReq->setStartingSequenceNumber(startingSequenceNumber); + blockAckReq->setTidInfo(tid); + return blockAckReq; } const Ptr OriginatorBlockAckProcedure::buildBasicBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 40a4186fead..db10d2d489b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -9,6 +9,7 @@ #define __INET_RECIPIENTBLOCKACKAGREEMENT_H #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" namespace inet { namespace ieee80211 { @@ -23,6 +24,9 @@ class INET_API RecipientBlockAckAgreement : public cObject simtime_t blockAckTimeoutValue = 0; bool isAddbaResponseSent = false; simtime_t expirationTime = -1; + SequenceNumberCyclic highestReceivedSequence; + int highestReceivedFragment = 0; + BlockAckVariant variant = BlockAckVariant::BASIC; public: RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); @@ -33,6 +37,23 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual BlockAckRecord *getBlockAckRecord() const { return blockAckRecord; } virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } + BlockAckVariant getVariant() const { return variant; } + void setVariant(BlockAckVariant value) { + variant = value; + if (variant == BlockAckVariant::COMPRESSED) + blockAckRecord->initializeCompressedWindow(startingSequenceNumber, bufferSize); + } + void recordReceived(const Ptr& header) { + auto sequence = header->getSequenceNumber(); + if (!highestReceivedSequence.isSpecified() || highestReceivedSequence < sequence) { + highestReceivedSequence = sequence; + highestReceivedFragment = header->getFragmentNumber(); + } + else if (highestReceivedSequence == sequence) + highestReceivedFragment = std::max(highestReceivedFragment, int(header->getFragmentNumber())); + } + const SequenceNumberCyclic& getHighestReceivedSequence() const { return highestReceivedSequence; } + int getHighestReceivedFragment() const { return highestReceivedFragment; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } virtual void addbaResposneSent() { isAddbaResponseSent = true; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 6219ac77140..7c63aea9ec2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { @@ -24,9 +25,7 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback) { - simtime_t earliestExpirationTime = computeEarliestExpirationTime(); - if (earliestExpirationTime != SIMTIME_MAX) - callback->scheduleInactivityTimer(earliestExpirationTime); + callback->scheduleInactivityTimer(); } // The inactivity timer at a recipient is reset when MPDUs corresponding to the TID for which the Block Ack @@ -35,12 +34,26 @@ void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreeme // void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) { - if (qosHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { // TODO + Implicit Block Ack + // IEEE Std 802.11-2024, 11.5.4: matching Block Ack-policy DATA refreshes inactivity, including a duplicate. + if (qosHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { Tid tid = qosHeader->getTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); auto agreement = getAgreement(tid, originatorAddr); - if (agreement) + if (agreement) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } + } +} + +void RecipientBlockAckAgreementHandler::implicitBlockAckRequested(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); } } @@ -51,15 +64,16 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + std::vector> expired; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { - MacAddress receiverAddr = id.first.first; - Tid tid = id.first.second; - const auto& delba = buildDelba(receiverAddr, tid, 39); - auto delbaPacket = new Packet("Delba", delba); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + if (agreement->getExpirationTime() <= now) + expired.push_back(id.first); + } + for (const auto& id : expired) { + const auto& delba = buildDelba(id.first, id.second, 39); + auto delbaPacket = new Packet("Delba", delba); + procedureCallback->processMgmtFrame(delbaPacket, delba); } scheduleInactivityTimer(agreementHandlerCallback); } @@ -110,8 +124,10 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR Tid tid = addbaRequest->getTid(); addbaResponse->setTid(tid); addbaResponse->setBlockAckPolicy(!addbaRequest->getBlockAckPolicy() && blockAckAgreementPolicy->delayedBlockAckPolicySupported() ? false : true); - addbaResponse->setBufferSize(addbaRequest->getBufferSize() <= blockAckAgreementPolicy->getMaximumAllowedBufferSize() ? addbaRequest->getBufferSize() : blockAckAgreementPolicy->getMaximumAllowedBufferSize()); - addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? blockAckAgreementPolicy->getBlockAckTimeoutValue() : addbaRequest->getBlockAckTimeoutValue()); + addbaResponse->setDialogToken(addbaRequest->getDialogToken()); + // IEEE Std 802.11-2024, 10.25.2: the request allocation and timeout are advisory. + addbaResponse->setBufferSize(std::min(64, blockAckAgreementPolicy->getMaximumAllowedBufferSize())); + addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue()); addbaResponse->setAMsduSupported(blockAckAgreementPolicy->aMsduSupported()); return addbaResponse; } @@ -124,8 +140,6 @@ void RecipientBlockAckAgreementHandler::updateAgreement(const Ptrsecond; agreement->addbaResposneSent(); } - else - throw cRuntimeError("Agreement is not found"); } void RecipientBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) @@ -155,26 +169,62 @@ void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) { EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest)) { + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); + bool accepted = blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest) && + blockAckAgreements.count(id) == 0 && pendingTeardowns.count(id) == 0; + addbaResponse->setStatusCode(accepted ? SC_SUCCESSFUL : SC_UNSPECIFIED); + if (accepted) { EV_DETAIL << "Addba Request has been accepted. Creating a new Block Ack Agreement." << endl; - auto agreement = addAgreement(addbaRequest); + auto parameters = makeShared(*addbaRequest); + parameters->setBufferSize(addbaResponse->getBufferSize()); + parameters->setBlockAckTimeoutValue(addbaResponse->getBlockAckTimeoutValue()); + auto agreement = addAgreement(parameters); EV_DETAIL << "Agreement is added with the following parameters: " << *agreement << endl; EV_DETAIL << "Building Addba Response" << endl; - auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); - auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); - callback->processMgmtFrame(addbaResponsePacket, addbaResponse); } + callback->processMgmtFrame(new Packet("AddbaResponse", addbaResponse), addbaResponse); +} + +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::detachAgreement(MacAddress peer, Tid tid) +{ + auto it = blockAckAgreements.find({peer, tid}); + if (it == blockAckAgreements.end()) + return nullptr; + auto agreement = it->second; + blockAckAgreements.erase(it); + return agreement; +} + +std::vector RecipientBlockAckAgreementHandler::detachPeer(MacAddress peer) +{ + std::vector removed; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + if (it->first.first == peer) { + removed.push_back(it->second); + it = blockAckAgreements.erase(it); + } + else + ++it; + } + for (auto it = pendingTeardowns.begin(); it != pendingTeardowns.end();) { + if (it->first == peer) + it = pendingTeardowns.erase(it); + else + ++it; + } + return removed; } void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + // HCF retains teardown state until the Action reaches a terminal result. } void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + terminateAgreement(delba->getTransmitterAddress(), delba->getTid()); } RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 1e65a1557ef..21b3eabc7e5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,6 +26,7 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre { protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; + std::set> pendingTeardowns; protected: virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); @@ -33,15 +34,22 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual void updateAgreement(const Ptr& addbaResponse); virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); - virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: + virtual simtime_t computeEarliestExpirationTime() override; + const std::map, RecipientBlockAckAgreement *>& getAgreements() const override { return blockAckAgreements; } + bool isTeardownPending(MacAddress peer, Tid tid) const override { return pendingTeardowns.count({peer, tid}) != 0; } + void beginTeardown(MacAddress peer, Tid tid) override { pendingTeardowns.insert({peer, tid}); } + void finishTeardown(MacAddress peer, Tid tid) override { pendingTeardowns.erase({peer, tid}); } + RecipientBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) override; + std::vector detachPeer(MacAddress peer) override; virtual ~RecipientBlockAckAgreementHandler(); virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) override; virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; 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/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index 73813feaa5d..33360fcfc32 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -19,12 +19,29 @@ void RecipientBlockAckAgreementPolicy::initialize(int stage) isAMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); + if (maximumAllowedBufferSize <= 0 || blockAckTimeoutValue < SIMTIME_ZERO) + throw cRuntimeError("Invalid Block Ack buffer size or inactivity timeout"); } } bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaReq) { - return true; + // IEEE Std 802.11-2024, 10.25.2 and Table 9-466: validate ordinary setup; the requested buffer size is advisory. + // INET supports immediate Block Ack for historical Basic compatibility and selects at most 64 entries in the response. + return !addbaReq->getTransmitterAddress().isUnspecified() && + !addbaReq->getTransmitterAddress().isMulticast() && + addbaReq->getTid() >= 0 && addbaReq->getTid() <= 15 && + addbaReq->getDialogToken() > 0 && addbaReq->getDialogToken() <= 255 && + addbaReq->getBlockAckPolicy() && addbaReq->getBufferSize() >= 0 && + addbaReq->getBufferSize() <= 1023 && addbaReq->getStartingSequenceNumber().isSpecified() && + addbaReq->getBlockAckTimeoutValue() >= SIMTIME_ZERO; +} + +bool RecipientBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const +{ + if (variant == BlockAckVariant::COMPRESSED && (!localHt || !peerHt)) + EV_DEBUG << "Compressed Block Ack setup refused: " << (!localHt ? "local HT state is absent" : "peer HT state is absent") << "; DATA uses Normal Ack.\n"; + return variant == BlockAckVariant::BASIC || (localHt && peerHt); } bool RecipientBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h index 995fcc29fdb..d74bfd4240b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h @@ -28,6 +28,7 @@ class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IR virtual void initialize(int stage) override; public: + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override; virtual bool isAddbaReqAccepted(const Ptr& addbaReq) override; virtual bool isDelbaAccepted(const Ptr& delba) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned index 8c41285b009..6fb403594cf 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned @@ -20,7 +20,7 @@ simple RecipientBlockAckAgreementPolicy extends SimpleModule like IRecipientBloc bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); - double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + double blockAckTimeoutValue @unit(s) = default(0s); // Zero disables inactivity expiration. @display("i=block/control"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..9771a17cc3c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -31,6 +31,14 @@ void RecipientBlockAckProcedure::processReceivedBlockAckReq(Packet *blockAckPack callback->transmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, basicBlockAckReq); } } + else if (auto compressed = dynamicPtrCast(blockAckReq)) { + auto agreement = blockAckAgreementHandler->getAgreement(compressed->getTidInfo(), compressed->getTransmitterAddress()); + if (ackPolicy->isBlockAckNeeded(compressed, agreement)) { + auto blockAck = buildBlockAck(compressed, agreement); + blockAck->setDurationField(ackPolicy->computeCompressedBlockAckDurationField(blockAckPacketReq, compressed)); + callback->transmitControlResponseFrame(new Packet("CompressedBlockAck", blockAck), blockAck, blockAckPacketReq, compressed); + } + } else throw cRuntimeError("Unsupported BlockAckReq"); } @@ -65,10 +73,30 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr blockAck->setTidInfo(basicBlockAckReq->getTidInfo()); return blockAck; } + else if (auto compressed = dynamicPtrCast(blockAckReq)) { + 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/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 6cffea3c37d..c69c60c99e7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -12,11 +12,87 @@ namespace inet { namespace ieee80211 { -BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +BlockAckReordering::ReleasedFrames BlockAckReordering::detachAgreement(MacAddress peer, Tid tid, + bool deliverComplete, std::vector& discarded) +{ + auto it = receiveBuffers.find({tid, peer}); + if (it == receiveBuffers.end()) + return {}; + auto buffer = it->second; + receiveBuffers.erase(it); + ReleasedFrames released; + auto start = buffer->getNextExpectedSequenceNumber(); + for (int i = 0; i < buffer->getBufferSize(); i++) { + auto sequence = start + i; + auto position = buffer->getBuffer().find(sequence.get()); + if (position == buffer->getBuffer().end()) + continue; + auto fragments = position->second; + if (deliverComplete && isComplete(fragments)) + released.push_back({sequence.get(), fragments}); + else + discarded.insert(discarded.end(), fragments.begin(), fragments.end()); + buffer->removeFrame(sequence); + } + delete buffer; + return released; +} + +void BlockAckReordering::clearPeer(MacAddress peer) +{ + for (auto it = receiveBuffers.begin(); it != receiveBuffers.end();) { + if (it->first.second == peer) { + delete it->second; + it = receiveBuffers.erase(it); + } + else + ++it; + } +} + +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& range : aggregate->ranges) { + if (range.kind == MpduRangeKind::DELIMITER && member.offset < range.offset && range.offset < endpoint) + valid &= range.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(); auto windowStart = receiveBuffer->getNextExpectedSequenceNumber(); + bool insideWindow = windowStart <= sequenceNumber && sequenceNumber < windowStart + receiveBuffer->getBufferSize(); ReleasedFrames releasedFrames; // IEEE Std 802.11-2024, 10.25.6.6.2.1(b,c): advance for newer DATA beyond the window; discard older DATA. // The shared reorder service also retains Basic fragment reassembly as an INET compatibility choice. @@ -31,8 +107,14 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(R // This allows the recipient to delete the Block Ack if the originator does not switch // back to using Block Ack. if (receiveBuffer->insertFrame(dataPacket, dataHeader)) { - if (dataHeader->getAckPolicy() == BLOCK_ACK) + if (dataHeader->getAckPolicy() == BLOCK_ACK && agreement->getVariant() == BlockAckVariant::BASIC) agreement->blockAckPolicyFrameReceived(dataHeader); + // 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 && + isMandatoryRelease(agreement, dataHeader, interframe, context)) { + auto preceding = advanceReceiveBuffer(agreement, receiveBuffer, sequenceNumber); + releasedFrames.insert(releasedFrames.end(), preceding.begin(), preceding.end()); + } // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2): release every consecutive complete unit at the window start. auto consecutiveFrames = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); releaseReceiveBuffer(agreement, receiveBuffer, consecutiveFrames); @@ -54,6 +136,14 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedBlockAckRe return ReleasedFrames(); startingSequenceNumber = basicReq->getStartingSequenceNumber(); } + else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { + if (compressedReq->getFragmentNumber() != 0 || compressedReq->getMultiTid() || !compressedReq->getCompressedBitmap() || + compressedReq->getTidInfo() < 0 || compressedReq->getTidInfo() > 15 || !compressedReq->getStartingSequenceNumber().isSpecified() || + compressedReq->getChunkLength() != LENGTH_BLOCKACKREQ - B(4)) + return ReleasedFrames(); + startingSequenceNumber = compressedReq->getStartingSequenceNumber(); + agreement->getBlockAckRecord()->advanceCompressedWindow(startingSequenceNumber); + } else { throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); } @@ -171,7 +261,8 @@ void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, Receive receiveBuffer->setNextExpectedSequenceNumber(sequenceNumber + 1); receiveBuffer->dropFramesUntil(sequenceNumber); receiveBuffer->removeFrame(sequenceNumber); - agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); + if (agreement->getVariant() == BlockAckVariant::BASIC) + agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); } std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 42215dd9b40..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,15 +45,19 @@ 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); public: virtual ~BlockAckReordering(); + ReleasedFrames detachAgreement(MacAddress peer, Tid tid, bool deliverComplete, std::vector& discarded); + void clearPeer(MacAddress peer); void processReceivedDelba(const Ptr& delba); - ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); + 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/channelaccess/Edca.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h index b7b9e4d6bc2..4b038c0c914 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h @@ -30,11 +30,10 @@ class INET_API Edca : public SimpleModule virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - virtual AccessCategory mapTidToAc(Tid tid); - public: virtual ~Edca(); + virtual AccessCategory mapTidToAc(Tid tid); virtual AccessCategory classifyFrame(const Ptr& header); virtual Edcaf *getEdcaf(AccessCategory ac) const { return edcafs[ac]; } EdcaTransmitLifetimeHandler *getLifetimeHandler() const { return lifetimeHandler; } diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index 5dc48059089..cb0e9d6549b 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -165,6 +165,11 @@ bool Edcaf::isInternalCollision() return collisionController->isInternalCollision(this); } +bool Edcaf::hasEligibleFrames() const +{ + return frameEligibility ? frameEligibility() : !pendingQueue->isEmpty() || inProgressFrames->getLength() != 0; +} + int Edcaf::getCwMax(AccessCategory ac, int aCwMax, int aCwMin) { switch (ac) { diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index c2d716ade33..b9a77e9c051 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -49,6 +49,7 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub InProgressFrames *inProgressFrames = nullptr; bool owning = false; + std::function frameEligibility; simtime_t slotTime = -1; simtime_t sifs = -1; @@ -102,6 +103,8 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub // Edcaf virtual bool isOwning() { return owning; } virtual bool isInternalCollision(); + void setFrameEligibility(std::function predicate) { frameEligibility = predicate; } + bool hasEligibleFrames() const; virtual AccessCategory getAccessCategory() { return ac; } }; diff --git a/src/inet/linklayer/ieee80211/mac/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/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc b/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc index 33b94c93563..4b3172e039f 100644 --- a/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc +++ b/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc @@ -36,9 +36,9 @@ bool EdcaCollisionController::isInternalCollision(Edcaf *edcaf) simtime_t now = simTime(); AccessCategory accessCategory = edcaf->getAccessCategory(); auto hasFrames = [](const Edcaf *candidate) { - return !candidate->getPendingQueue()->isEmpty() || candidate->getInProgressFrames()->getLength() != 0; + return candidate->hasEligibleFrames(); }; - // An expired empty queue cannot initiate a TXOP or cause an internal collision. + // Deferred or expired DATA cannot initiate a TXOP or cause an internal collision. // Keep the winner's in-progress frame visible until its transmission completes. if (txStartTimes[accessCategory] == now && hasFrames(edcaf)) { for (int ac = accessCategory + 1; ac < 4; ac++) { diff --git a/src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h b/src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h new file mode 100644 index 00000000000..0dbe31b1f6b --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/AmpduReception.h @@ -0,0 +1,57 @@ +// 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" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.h" + +namespace inet { +namespace ieee80211 { + +// These facts describe one received PSDU. BlockAckRecord remains the receipt owner. +struct INET_API AmpduTopology +{ + struct Member { + B offset; + std::optional sequence; + bool first = false; + }; + std::vector ranges; + 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/BlockAckVariant.h b/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h new file mode 100644 index 00000000000..f02e1033036 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_BLOCKACKVARIANT_H +#define __INET_BLOCKACKVARIANT_H + +namespace inet { +namespace ieee80211 { + +enum class BlockAckVariant { BASIC, COMPRESSED }; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/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/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cc4a7b86b92..3fefe1666c0 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -18,7 +18,8 @@ class INET_API IBlockAckAgreementHandlerCallback public: virtual ~IBlockAckAgreementHandlerCallback() {} - virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + // The coordinator recomputes the earliest absolute deadline across both agreement roles. + virtual void scheduleInactivityTimer() = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementLifecycle.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementLifecycle.h new file mode 100644 index 00000000000..46e49e22386 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementLifecycle.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_IBLOCKACKAGREEMENTLIFECYCLE_H +#define __INET_IBLOCKACKAGREEMENTLIFECYCLE_H + +#include "inet/linklayer/common/MacAddress.h" + +namespace inet { +namespace ieee80211 { + +// Management supplies the old peer identity before association replacement or loss. +class INET_API IBlockAckAgreementLifecycle +{ + public: + virtual ~IBlockAckAgreementLifecycle() {} + virtual void resetPeerAgreements(const MacAddress& peer) = 0; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index ee9d4d37465..a7c54243d39 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -46,8 +46,11 @@ 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. + virtual void interruptFrameSequence() = 0; }; } // 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/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/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 4a01e89b6c3..7161304e0ad 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -22,7 +22,27 @@ namespace ieee80211 { class INET_API IOriginatorBlockAckAgreementHandler { public: + typedef std::pair AgreementId; + enum class ProcedureState { SETUP_INTENT, REQUEST_PENDING, TEARDOWN_PENDING, RECOVERY_NEEDED, WAITING_FOR_SUCCESSOR }; + struct Procedure { + ProcedureState state = ProcedureState::SETUP_INTENT; + bool requestAttempted = false; + simtime_t responseDeadline = SIMTIME_MAX; + }; virtual ~IOriginatorBlockAckAgreementHandler() {} + virtual const std::map& getAgreements() const = 0; + virtual const std::map& getProcedures() const = 0; + virtual simtime_t computeEarliestExpirationTime() = 0; + virtual OriginatorBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) = 0; + virtual std::vector detachPeer(MacAddress peer) = 0; + virtual void noteEligibleDataTransmission(MacAddress peer, Tid tid) = 0; + virtual void beginSetupIntent(MacAddress peer, Tid tid) = 0; + virtual void beginTeardown(MacAddress peer, Tid tid) = 0; + virtual void failTeardown(MacAddress peer, Tid tid) = 0; + virtual void finishProcedure(MacAddress peer, Tid tid) = 0; + virtual void markRequestAttempted(MacAddress peer, Tid tid) = 0; + virtual void acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) = 0; + virtual void startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) = 0; virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void processTransmittedAddbaReq(const Ptr& addbaReq) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 40ea82aa95e..c6e1ddd9638 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -9,6 +9,7 @@ #define __INET_IORIGINATORBLOCKACKAGREEMENTPOLICY_H #include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { @@ -21,6 +22,8 @@ class INET_API IOriginatorBlockAckAgreementPolicy public: virtual ~IOriginatorBlockAckAgreementPolicy() {} + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const = 0; + virtual bool isAddbaReqNeeded(Packet *packet, const Ptr& header) = 0; virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) = 0; virtual bool isDelbaAccepted(const Ptr& delba) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 94a583a111a..165dd477385 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -10,6 +10,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" @@ -20,12 +21,14 @@ 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; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const = 0; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const = 0; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h new file mode 100644 index 00000000000..dd6019cc3dd --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h @@ -0,0 +1,63 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IORIGINATORQOSMACDATASERVICE_H +#define __INET_IORIGINATORQOSMACDATASERVICE_H + +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h" + +namespace inet { +namespace ieee80211 { + +/** Owns a reversible DATA preview. Source packet identities refer to frames still owned by the queue. */ +struct INET_API PreparedQosData : public cObject +{ + std::unique_ptr packet; + queueing::IPacketQueue *queue = nullptr; + std::vector sourceIds; + bool aggregate = false; + + ~PreparedQosData() override; + void setPacket(Packet *value); + Packet *releasePacket(); +}; + +/** 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: + // Preview borrows the frame without extraction or sequence assignment. + virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const = 0; + virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const = 0; + virtual bool hasAMsduAggregation() const = 0; + 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; + // Success transfers the preview to the caller. Failure retains the preview and all queued sources. + virtual Packet *commitPreparedFrame(PreparedQosData& preparation) = 0; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h index 34a38ac1d9a..59f75f69e7e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h @@ -32,6 +32,10 @@ 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; virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 357745c3ddb..c2a1463709e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -23,12 +23,20 @@ class INET_API IRecipientBlockAckAgreementHandler { public: virtual ~IRecipientBlockAckAgreementHandler() {} + virtual simtime_t computeEarliestExpirationTime() = 0; + virtual const std::map, RecipientBlockAckAgreement *>& getAgreements() const = 0; + virtual RecipientBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) = 0; + virtual std::vector detachPeer(MacAddress peer) = 0; + virtual bool isTeardownPending(MacAddress peer, Tid tid) const = 0; + virtual void beginTeardown(MacAddress peer, Tid tid) = 0; + virtual void finishTeardown(MacAddress peer, Tid tid) = 0; virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; 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/IRecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h index 48009a67161..2f429ef7169 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h @@ -9,6 +9,7 @@ #define __INET_IRECIPIENTBLOCKACKAGREEMENTPOLICY_H #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" namespace inet { namespace ieee80211 { @@ -18,6 +19,8 @@ class INET_API IRecipientBlockAckAgreementPolicy public: virtual ~IRecipientBlockAckAgreementPolicy() {} + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const = 0; + virtual bool isAddbaReqAccepted(const Ptr& addbaReq) = 0; virtual bool isDelbaAccepted(const Ptr& delba) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/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/contract/IRecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h index ccf47cc5610..42aaecc9db8 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h @@ -26,6 +26,8 @@ class INET_API IRecipientQosAckPolicy virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const = 0; virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const = 0; + // The request selects the response variant and its actual airtime. + virtual simtime_t computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 8a3ff4c0b7c..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 { @@ -23,8 +24,11 @@ class INET_API IRecipientQosMacDataService public: virtual ~IRecipientQosMacDataService() {} + virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) = 0; + virtual void clearPeer(MacAddress peer) = 0; - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; + // 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, 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 abfdc4dacb1..9c1a4eae650 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" @@ -52,6 +53,8 @@ class INET_API IRx virtual void transmissionStateChanged(physicallayer::IRadio::TransmissionState state) = 0; virtual void receivedSignalPartChanged(physicallayer::IRadioSignal::SignalPart part) = 0; virtual bool lowerFrameReceived(Packet *packet) = 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/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index 63e16367533..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,10 @@ 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, * including any wait before transmission and the transmission itself. diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 9e27f154969..b0b7f5edf00 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -264,6 +264,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 a05c4f1d269..f2b22de216f 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -10,16 +10,27 @@ #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #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" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" namespace inet { namespace ieee80211 { @@ -43,12 +54,12 @@ void Hcf::initialize(int stage) blockAckVariant = BlockAckVariant::COMPRESSED; else throw cRuntimeError("Unknown blockAckVariant \"%s\": expected \"basic\" or \"compressed\"", configuredVariant); - if (blockAckVariant == BlockAckVariant::COMPRESSED) - throw cRuntimeError("Compressed Block Ack exchanges are not implemented"); mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); + mib = check_and_cast(mac->getModuleByPath(mac->par("mibModule"))); startRxTimer = new cMessage("startRxTimeout"); inactivityTimer = new cMessage("blockAckInactivityTimer"); dataLifetimeTimer = new cMessage("dataLifetimeTimer"); + addbaResponseTimer = new cMessage("addbaResponseTimer"); edca = check_and_cast(getSubmodule("edca")); hcca = check_and_cast(getSubmodule("hcca")); tx = check_and_cast(getModuleByPath(par("txModule"))); @@ -59,9 +70,16 @@ void Hcf::initialize(int stage) rateSelection = check_and_cast(getSubmodule("rateSelection")); frameSequenceHandler = new FrameSequenceHandler(); WATCH_EXPR("frameSequenceInfo", getFrameSequenceInfo()); + WATCH_EXPR("numPendingInterframeReceptions", pendingReceptions.size()); originatorDataService = check_and_cast(getSubmodule(("originatorMacDataService"))); recipientDataService = check_and_cast(getSubmodule("recipientMacDataService")); originatorAckPolicy = check_and_cast(getSubmodule("originatorAckPolicy")); + 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(); @@ -77,16 +95,49 @@ 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 (blockAckVariant == BlockAckVariant::COMPRESSED && par("isBlockAckSupported").boolValue()) { + interframeRadio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto radioModule = check_and_cast(interframeRadio.get()); + for (auto signal : {receptionStartedSignal, receptionEndedSignal, transmissionStartedSignal, transmissionEndedSignal, IRadio::receptionStateChangedSignal}) + radioModule->subscribe(signal, this); + } + if (par("isBlockAckSupported").boolValue()) { + EV_INFO << "Block Ack profile: " << (blockAckVariant == BlockAckVariant::BASIC ? "basic" : "compressed") << endl; + if (blockAckVariant == BlockAckVariant::COMPRESSED) { + if (originatorBlockAckAgreementPolicy && originatorBlockAckAgreementPolicy->isDelayedAckPolicySupported()) + throw cRuntimeError("Compressed Block Ack does not support delayedAckPolicySupported in %s.originatorBlockAckAgreementPolicy", getFullPath().c_str()); + if (recipientBlockAckAgreementPolicy && recipientBlockAckAgreementPolicy->delayedBlockAckPolicySupported()) + throw cRuntimeError("Compressed Block Ack does not support delayedAckPolicySupported in %s.recipientBlockAckAgreementPolicy", getFullPath().c_str()); + if (!mib->isHtOperationSupported()) + EV_WARN << "Compressed Block Ack has no usable local HT support; DATA uses Normal Ack. Set blockAckVariant = basic for Basic compatibility.\n"; + } + } // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. auto lifetimeHandler = edca->getLifetimeHandler(); lifetimeHandler->setDeadlineChangedCallback([this]() { scheduleDataLifetimeTimer(); }); check_and_cast(originatorDataService)->setLifetimeHandler( lifetimeHandler, [this]() { expireDataFrames(); }); + check_and_cast(originatorDataService)->setEligibility( + [this](Packet *packet) { return isFrameEligible(packet, false); }, + [this](Packet *packet) { return isAMsduPermitted(packet); }); + check_and_cast(originatorDataService)->setFragmentationPermission([this](Packet *packet) { + auto data = dynamicPtrCast(packet->peekAtFront()); + if (blockAckVariant != BlockAckVariant::COMPRESSED || !data || data->getType() != ST_DATA_WITH_QOS || !originatorBlockAckAgreementHandler) + return true; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + return !agreement || !agreement->getIsAddbaResponseReceived(); + }); + mib->subscribe(Ieee80211Mib::rateStateChangedSignal, this); for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + edca->getEdcaf(static_cast(ac))->setFrameEligibility( + [this, ac]() { return hasEligibleFrameToTransmit(static_cast(ac)); }); check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(packetDroppedSignal, this); auto frames = edca->getEdcaf(static_cast(ac))->getInProgressFrames(); frames->setLifetimeHandler(lifetimeHandler); + frames->setTransmissionEligibility([this](Packet *packet) { return isFrameEligible(packet, true); }); frames->subscribe(InProgressFrames::packetDequeuedSignal, this); } } @@ -127,7 +178,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()); @@ -179,6 +235,8 @@ void Hcf::expireDataFrames(bool preserveSequence) PacketDropDetails details; details.setReason(LIFETIME_EXPIRED); emit(packetDroppedSignal, packet, &details); + if (inProgress) + firstDataDiscarded(header); if (!inProgress) delete packet; } @@ -189,6 +247,7 @@ void Hcf::expireDataFrames(bool preserveSequence) frames->clearDroppedFrames(); } scheduleDataLifetimeTimer(); + advanceOriginatorWindows(); } void Hcf::forEachChild(cVisitor *v) @@ -200,8 +259,24 @@ void Hcf::forEachChild(cVisitor *v) void Hcf::handleMessage(cMessage *msg) { - if (msg == dataLifetimeTimer) + if (msg == addbaResponseTimer) { + std::vector> expired; + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) { + if (entry.second.responseDeadline <= simTime()) + expired.push_back(entry.first); + } + for (const auto& id : expired) + beginTeardown(id.first, id.second, true, 39); + scheduleAddbaResponseTimer(); + updateContention(); + } + else if (msg == dataLifetimeTimer) { expireDataFrames(); + if (!frameSequenceHandler->isSequenceRunning()) + completeTransfers(); + updateAgreementProcedures(); + updateContention(); + } else if (msg == startRxTimer) { if (!isReceptionInProgress()) { frameSequenceHandler->handleStartRxTimeout(); @@ -209,8 +284,19 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == inactivityTimer) { if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) { - originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); - recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + std::vector> expired; + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + if (entry.second->getIsAddbaResponseReceived() && entry.second->getExpirationTime() <= simTime()) + expired.push_back({entry.first.first, entry.first.second, true}); + } + for (const auto& entry : recipientBlockAckAgreementHandler->getAgreements()) { + if (entry.second->getExpirationTime() <= simTime()) + expired.push_back({entry.first.first, entry.first.second, false}); + } + for (const auto& entry : expired) + beginTeardown(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry), 39); + scheduleInactivityTimer(); + updateContention(); } else throw cRuntimeError("Unknown event"); @@ -255,7 +341,8 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetType() == ST_DATA_WITH_QOS) edca->getLifetimeHandler()->registerFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + updateAgreementProcedures(); + if (!pendingQueue->isEmpty() && !handlingAgreementAction && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -266,7 +353,93 @@ void Hcf::processUpperFrame(Packet *packet, const Ptr(interframeRadio.get()) && signalID == receptionStartedSignal) { + auto reception = check_and_cast(obj); + auto transmission = reception->getTransmission(); + std::set active; + for (auto ongoing : interframeRadio->getReceptionsInProgress()) + active.insert(ongoing->getId()); + for (auto it = pendingReceptions.begin(); it != pendingReceptions.end();) { + if (!it->second.completed && !active.count(it->first)) + it = pendingReceptions.erase(it); + else + ++it; + } + PendingReception pending; + for (auto& entry : pendingReceptions) { + if (!entry.second.completed) { + pending.overlapped = true; + entry.second.overlapped = true; + entry.second.previousAggregate.reset(); + } + } + auto medium = interframeRadio->getMedium(); + pending.timing = medium->getTransmissionTimingSnapshot(transmission->getId()); + auto wifi = dynamic_cast(transmission); + pending.ht = wifi && wifi->getMode()->getModulationClass() == IIeee80211Mode::ModulationClass::HT; + if (wifi) + pending.extension = wifi->getSignalExtensionDuration(); + auto gap = medium->computePrecedingSignalGap(transmission->getId(), interframeHistory.timing); + if (!pending.overlapped && gap && (interframeHistory.mpdu || interframeHistory.ack || + interframeHistory.aggregate || interframeHistory.blockAck)) { + // IEEE Std 802.11-2024, 10.3.2.3.3: this model uses coverage class zero. + pending.sifs = std::abs((*gap - modeSet->getSifsTime()).raw()) <= modeSet->getSlotTime().raw() / 10; + pending.precedingAggregate = bool(interframeHistory.aggregate); + pending.precedingIndividual = interframeHistory.mpdu || interframeHistory.ack; + pending.previousAggregate = interframeHistory.aggregate; + if ((interframeHistory.mpdu || interframeHistory.aggregate) && interframeHistory.ht && pending.ht && interframeHistory.noSifsResponse && + interframeHistory.timing.transmitterRadioId == pending.timing.transmitterRadioId && interframeHistory.extension) { + auto rifs = wifi->getMode()->getRifsTime(); + // IEEE Std 802.11-2024, 10.3.2.3.2 and 10.3.8: RIFS excludes the actual preceding extension. + pending.rifs = std::abs((*gap + *interframeHistory.extension - rifs).raw()) <= rifs.raw() / 10; + pending.precedingReceiver = interframeHistory.receiver; + } + } + interframeHistory = {}; + pendingReceptions.insert_or_assign(transmission->getId(), std::move(pending)); + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == receptionEndedSignal) { + auto reception = check_and_cast(obj); + auto pending = pendingReceptions.find(reception->getTransmission()->getId()); + if (pending != pendingReceptions.end()) + pending->second.completed = true; + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == transmissionStartedSignal) { + auto transmission = check_and_cast(obj); + auto topology = interframeHistory.aggregate; + bool compatible = false; + auto packet = transmission->getPacket(); + if (topology && packet && packet->hasAtFront()) { + auto phy = packet->peekAtFront(); + auto ht = dynamicPtrCast(phy); + if (!ht || !ht->getAggregation()) { + auto header = packet->peekDataAt(phy->getChunkLength()); + if (auto blockAck = dynamicPtrCast(header)) + compatible = blockAck->getReceiverAddress() == topology->transmitter && blockAck->getTidInfo() == topology->tid; + } + } + clearInterframeHistory(); + if (compatible) + transmittedBlockAckAggregate = std::move(topology); + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == transmissionEndedSignal) { + auto topology = transmittedBlockAckAggregate; + clearInterframeHistory(); + auto transmission = check_and_cast(obj); + if (completedTransmissionWasAck) { + interframeHistory.timing = interframeRadio->getMedium()->getTransmissionTimingSnapshot(transmission->getId()); + interframeHistory.ack = true; + } + else if (completedTransmissionWasBlockAck && topology) { + interframeHistory.timing = interframeRadio->getMedium()->getTransmissionTimingSnapshot(transmission->getId()); + interframeHistory.blockAck = true; + interframeHistory.aggregate = std::move(topology); + } + completedTransmissionWasAck = false; + completedTransmissionWasBlockAck = false; + } + else if (signalID == InProgressFrames::packetDequeuedSignal) { Enter_Method("packetDequeued"); edca->getLifetimeHandler()->forgetFrame(check_and_cast(obj)->getId()); } @@ -274,6 +447,7 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, Enter_Method("%s", cComponent::getSignalName(signalID)); auto packet = check_and_cast(obj); edca->getLifetimeHandler()->forgetFrame(packet->getId()); + actionTerminated(packet, false); if (packet->findTag() != nullptr) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); @@ -284,24 +458,732 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, ModeSetListener::receiveSignal(source, signalID, obj, details); } +void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) +{ + Enter_Method_Silent(); + if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == IRadio::receptionStateChangedSignal && + value != IRadio::RECEPTION_STATE_RECEIVING) { + bool incomplete = false; + for (auto it = pendingReceptions.begin(); it != pendingReceptions.end();) { + if (!it->second.completed) { + incomplete = true; + it = pendingReceptions.erase(it); + } + else + ++it; + } + // Tx owns a prepared response during the switch to transmitter mode. Its start identifies the actual frame. + if (incomplete || value == IRadio::RECEPTION_STATE_BUSY || + (value == IRadio::RECEPTION_STATE_UNDEFINED && !tx->hasTransmission())) + interframeHistory = {}; + } +} + +void Hcf::clearInterframeHistory() +{ + interframeHistory = {}; + pendingReceptions.clear(); + transmittedBlockAckAggregate.reset(); +} + +bool Hcf::receiveInterframe(Packet *packet, const Ptr& header, BlockAckReceiveContext *context) +{ + auto time = packet->findTag(); + if (!time) + return false; + auto entry = pendingReceptions.find(time->getTransmissionId()); + if (entry == pendingReceptions.end() || !entry->second.completed) + return false; + auto pending = std::move(entry->second); + pendingReceptions.erase(entry); + if (pending.overlapped) + return false; + // IEEE Std 802.11-2024, 10.27.3.1, 10.27.3.3, and 11.38.1: the BSS controls RIFS permission. + bool rifsPermitted = pending.rifs && modeSet->getPhyType() != Ieee80211ModeSet::PhyType::VHT && + (mib->mode == Ieee80211Mib::INDEPENDENT || mib->mode == Ieee80211Mib::MESH || mib->getHtOperation().rifsMode); + bool related = pending.sifs || (pending.rifs && rifsPermitted && pending.precedingReceiver == header->getReceiverAddress()); + if (context) { + context->followsAggregate = related && pending.precedingAggregate; + context->followsIndividual = related && pending.precedingIndividual; + context->previousAggregate = std::move(pending.previousAggregate); + } + interframeHistory.timing = std::move(pending.timing); + interframeHistory.mpdu = true; + interframeHistory.ht = pending.ht; + interframeHistory.receiver = header->getReceiverAddress(); + interframeHistory.extension = pending.extension; + auto dataOrMgmt = dynamicPtrCast(header); + interframeHistory.noSifsResponse = dataOrMgmt ? !recipientAckPolicy->isAckNeeded(dataOrMgmt) : + !dynamicPtrCast(header) && !dynamicPtrCast(header); + return related; +} + void Hcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); scheduleAfter(timeout, startRxTimer); } -void Hcf::scheduleInactivityTimer(simtime_t timeout) +void Hcf::scheduleInactivityTimer() { Enter_Method("scheduleInactivityTimer"); - rescheduleAfter(timeout, inactivityTimer); + // IEEE Std 802.11-2024, 11.5.4 requires inactivity expiration. HCF reads each role's absolute deadline. + simtime_t deadline = SIMTIME_MAX; + if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) + deadline = std::min(originatorBlockAckAgreementHandler->computeEarliestExpirationTime(), + recipientBlockAckAgreementHandler->computeEarliestExpirationTime()); + cancelEvent(inactivityTimer); + if (deadline != SIMTIME_MAX) + scheduleAt(std::max(simTime(), deadline), inactivityTimer); +} + +void Hcf::scheduleAddbaResponseTimer() +{ + simtime_t deadline = SIMTIME_MAX; + if (originatorBlockAckAgreementHandler) { + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) + deadline = std::min(deadline, entry.second.responseDeadline); + } + cancelEvent(addbaResponseTimer); + if (deadline != SIMTIME_MAX) + scheduleAt(std::max(simTime(), deadline), addbaResponseTimer); +} + +bool Hcf::isProfileAllowed(MacAddress peer, bool originator) const +{ + auto state = mib->findPeerHtState(peer); + bool peerHt = state && state->valid && (originator ? state->negotiatedCapabilities.localTxPeerRx.valid : + state->negotiatedCapabilities.localRxPeerTx.valid); + return originator ? originatorBlockAckAgreementPolicy->isProfileAllowed(blockAckVariant, mib->isHtOperationSupported(), peerHt) : + recipientBlockAckAgreementPolicy->isProfileAllowed(blockAckVariant, mib->isHtOperationSupported(), peerHt); +} + +bool Hcf::hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const +{ + if (finishingSequence || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + for (int i = 0; i < context->getNumSteps(); i++) { + auto step = dynamic_cast(context->getStep(i)); + if (!step) + continue; + auto packet = step->getFrameToTransmit(); + if (!obsoleteActions.count(packet->getId())) + continue; + auto header = packet->peekAtFront(); + if (header->getReceiverAddress() != peer) + continue; + if (auto request = dynamicPtrCast(header)) { + if (originator && request->getTid() == tid) + return true; + } + else if (auto response = dynamicPtrCast(header)) { + if (!originator && response->getTid() == tid) + return true; + } + else if (auto delba = dynamicPtrCast(header)) { + if (delba->getInitiator() == originator && delba->getTid() == tid) + return true; + } + } + return false; +} + +bool Hcf::isFrameEligible(Packet *packet, bool numbered) const +{ + if (obsoleteActions.count(packet->getId())) + return false; + auto data = dynamicPtrCast(packet->peekAtFront()); + if (!data || data->getType() != ST_DATA_WITH_QOS || !originatorBlockAckAgreementHandler) + return true; + auto peer = data->getReceiverAddress(); + auto tid = data->getTid(); + if (hasObsoleteActionInFlight(peer, tid, true)) + return false; + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + auto it = procedures.find({peer, tid}); + if (it == procedures.end()) + return true; + if (it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::WAITING_FOR_SUCCESSOR) + return true; + return numbered && it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::SETUP_INTENT; +} + +bool Hcf::isAMsduPermitted(Packet *packet) const +{ + auto data = dynamicPtrCast(packet->peekAtFront()); + if (!data || !originatorBlockAckAgreementHandler) + return true; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + return !agreement || !agreement->getIsAddbaResponseReceived() || agreement->isAMsduPermitted(); +} + +bool Hcf::hasQueuedData(MacAddress peer, Tid tid) const +{ + auto matches = [this, peer, tid](Packet *packet) { + auto data = dynamicPtrCast(packet->peekAtFront()); + return data && data->getType() == ST_DATA_WITH_QOS && data->getReceiverAddress() == peer && data->getTid() == tid && + !edca->getLifetimeHandler()->isExpired(packet); + }; + // Check the TID's category first. Other categories retain support for custom classification. + int first = edca->mapTidToAc(tid); + for (int offset = 0; offset < AC_NUMCATEGORIES; offset++) { + int ac = (first + offset) % AC_NUMCATEGORIES; + auto queue = edca->getEdcaf(static_cast(ac))->getPendingQueue(); + if (auto selected = dynamic_cast(queue)) { + try { + if (selected->peekEligiblePacket(matches)) + return true; + continue; + } + catch (const queueing::UnsupportedPacketSelection&) { + // Existence checks also support queues without optional selected extraction. + } + } + for (int i = 0; i < queue->getNumPackets(); i++) { + if (matches(queue->getPacket(i))) + return true; + } + } + return false; +} + +bool Hcf::hasNumberedData(MacAddress peer, Tid tid) const +{ + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto frames = edca->getEdcaf(static_cast(ac))->getInProgressFrames(); + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + if (data && data->getReceiverAddress() == peer && data->getTid() == tid) + return true; + } + } + return false; +} + +void Hcf::updateAgreementProcedures() +{ + if (!originatorBlockAckAgreementHandler || updatingProcedures) + return; + updatingProcedures = true; + auto procedures = originatorBlockAckAgreementHandler->getProcedures(); + for (const auto& entry : procedures) { + auto peer = entry.first.first; + auto tid = entry.first.second; + auto state = entry.second.state; + using State = IOriginatorBlockAckAgreementHandler::ProcedureState; + bool queued = hasQueuedData(peer, tid); + // INET setup policy waits for a successor after the first eligible DATA transmission. + if (state == State::WAITING_FOR_SUCCESSOR) { + if (!queued) + continue; + originatorBlockAckAgreementHandler->beginSetupIntent(peer, tid); + state = State::SETUP_INTENT; + } + if (state == State::SETUP_INTENT) { + if (!queued) + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + else if (!hasNumberedData(peer, tid) && !hasObsoleteActionInFlight(peer, tid, true)) { + auto service = check_and_cast(originatorDataService); + originatorBlockAckAgreementHandler->startSetup(peer, tid, service->getNextSequenceNumber(peer, tid), + originatorBlockAckAgreementPolicy, this, service->hasAMsduAggregation()); + } + } + else if (state == State::REQUEST_PENDING && !queued) { + if (entry.second.requestAttempted) + beginTeardown(peer, tid, true, 37); + else { + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + retireAgreement(peer, tid, true); + } + } + else if (state == State::RECOVERY_NEEDED && queued) + beginTeardown(peer, tid, true, 37); + } + updatingProcedures = false; + scheduleAddbaResponseTimer(); +} + +void Hcf::updateContention() +{ + if (handlingAgreementAction) + return; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto edcaf = edca->getEdcaf(static_cast(ac)); + if (edcaf->isOwning()) + continue; + if (hasFrameToTransmit(static_cast(ac))) + edcaf->requestChannel(this); + } +} + +void Hcf::advanceOriginatorWindows() +{ + if (!originatorBlockAckAgreementHandler) + return; + auto service = check_and_cast(originatorDataService); + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + auto agreement = entry.second; + if (!agreement->getIsAddbaResponseReceived()) + continue; + auto next = service->getNextSequenceNumber(entry.first.first, entry.first.second); + std::vector unresolvedSequences; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto frames = edca->getEdcaf(static_cast(ac))->getInProgressFrames(); + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + if (data && data->getType() == ST_DATA_WITH_QOS && + data->getReceiverAddress() == entry.first.first && data->getTid() == entry.first.second) + unresolvedSequences.push_back(data->getSequenceNumber()); + } + } + // IEEE Std 802.11-2024, 10.25.6.7.1: resolved positions permit transmit-window progress. + agreement->advanceWindow(next, unresolvedSequences); + } +} + +void Hcf::completeTransfers() +{ + if (!originatorBlockAckAgreementHandler) + return; + std::vector> completed; + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + auto agreement = entry.second; + if (agreement->getIsAddbaResponseReceived() && agreement->getFirstDataTransmitted() && + !hasNumberedData(entry.first.first, entry.first.second) && !hasQueuedData(entry.first.first, entry.first.second)) + completed.push_back(entry.first); + } + // IEEE Std 802.11-2024, 10.25.4; INET uses the accepted queue predicate at an exchange boundary. + for (const auto& id : completed) + beginTeardown(id.first, id.second, true, 37); +} + +void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) +{ + 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) { + prepared->cancelled = true; + cancelledAggregate = prepared->batch && tx->cancelPendingAggregate(this); + } + } + } + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto edcaf = edca->getEdcaf(static_cast(ac)); + auto frames = edcaf->getInProgressFrames(); + std::vector discard; + for (int i = 0; i < frames->getLength(); i++) { + auto packet = frames->getFrames(i); + auto data = dynamicPtrCast(packet->peekAtFront()); + bool transmitted = data && edcaf->getAckHandler()->getQoSDataAckStatus(data) != QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED; + if (data && frameSequenceHandler->isSequenceRunning()) { + auto context = frameSequenceHandler->getContext(); + for (int j = 0; j < context->getNumSteps(); j++) { + if (auto step = dynamic_cast(context->getStep(j))) + transmitted |= step->getFrameToTransmit() == packet; + } + } + // IEEE Std 802.11-2024, 11.5.3.4: the next MPDU must follow the highest transmitted sequence. + // Include unsent fragments even if ACK processing already removed an earlier fragment. + // Local allocation indices preserve order across gaps and wire sequence wraparound. + if (data && data->getReceiverAddress() == peer && data->getTid() == tid && + (all || transmitted || (highestTransmittedDataIndex >= 0 && + packet->getTag()->getAllocationIndex() <= highestTransmittedDataIndex))) + discard.push_back(packet); + } + for (auto packet : discard) { + auto data = packet->peekAtFront(); + edcaf->getAckHandler()->dropFrame(data); + edcaf->getRecoveryProcedure()->dropFrame(data); + frames->dropFrame(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + } + if (all) { + auto queue = edcaf->getPendingQueue(); + discard.clear(); + for (int i = 0; i < queue->getNumPackets(); i++) { + auto packet = queue->getPacket(i); + auto data = dynamicPtrCast(packet->peekAtFront()); + if (data && data->getReceiverAddress() == peer && data->getTid() == tid) + discard.push_back(packet); + } + for (auto packet : discard) { + queue->removePacket(packet); + take(packet); + edca->getLifetimeHandler()->forgetFrame(packet->getId()); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) + frames->clearDroppedFrames(); + } + if (cancelledAggregate) + aggregateTransmissionCancelled(); +} + +void Hcf::firstDataDiscarded(const Ptr& header) +{ + if (!originatorBlockAckAgreementHandler) + return; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived() && !agreement->getFirstDataTransmitted() && + agreement->getStartingSequenceNumber() == header->getSequenceNumber()) + beginTeardown(header->getReceiverAddress(), header->getTid(), true, 37); +} + +void Hcf::invalidateActions(MacAddress peer, Tid tid, bool originator) +{ + std::set invalid; + std::set attempted; + std::set rtsRecoveryOwned; + for (auto it = ownedActions.begin(); it != ownedActions.end();) { + if (it->second.peer == peer && it->second.tid == tid && it->second.originator == originator) { + invalid.insert(it->first); + if (it->second.attempted) + attempted.insert(it->first); + if (it->second.rtsRecoveryOwned) + rtsRecoveryOwned.insert(it->first); + obsoleteActions.insert(it->first); + it = ownedActions.erase(it); + } + else + ++it; + } + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto edcaf = edca->getEdcaf(static_cast(ac)); + auto queue = edcaf->getPendingQueue(); + std::vector removed; + for (int i = 0; i < queue->getNumPackets(); i++) { + if (invalid.count(queue->getPacket(i)->getId())) + removed.push_back(queue->getPacket(i)); + } + for (auto packet : removed) { + queue->removePacket(packet); + take(packet); + obsoleteActions.erase(packet->getId()); + delete packet; + } + auto frames = edcaf->getInProgressFrames(); + removed.clear(); + for (int i = 0; i < frames->getLength(); i++) { + if (invalid.count(frames->getFrames(i)->getId())) + removed.push_back(frames->getFrames(i)); + } + for (auto packet : removed) { + auto header = packet->peekAtFront(); + edcaf->getAckHandler()->dropFrame(header); + if (attempted.count(packet->getId())) + edcaf->getMgmtAndNonQoSRecoveryProcedure()->dropFrame(packet, header); + if (rtsRecoveryOwned.count(packet->getId())) + edcaf->getMgmtAndNonQoSRecoveryProcedure()->dropRtsProtectedFrame(header); + frames->dropFrame(packet); + if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) + obsoleteActions.erase(packet->getId()); + } + if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) + frames->clearDroppedFrames(); + } +} + +void Hcf::retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset) +{ + if (!originator) + clearInterframeHistory(); + if (originator) { + auto agreement = originatorBlockAckAgreementHandler->detachAgreement(peer, tid); + invalidateActions(peer, tid, originator); + discardAgreementData(peer, tid, associationReset, agreement ? agreement->getHighestTransmittedDataIndex() : -1); + if (agreement) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement); + delete agreement; + } + } + else { + auto agreement = recipientBlockAckAgreementHandler->detachAgreement(peer, tid); + invalidateActions(peer, tid, originator); + if (agreement) { + sendUp(recipientDataService->retireAgreement(agreement, associationReset)); + emit(blockAckAgreementDeletedSignal, agreement); + delete agreement; + } + } + scheduleInactivityTimer(); +} + +void Hcf::beginTeardown(MacAddress peer, Tid tid, bool originator, int reasonCode) +{ + if (!originatorBlockAckAgreementHandler || !recipientBlockAckAgreementHandler) + return; + if (originator) { + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + auto it = procedures.find({peer, tid}); + if (it != procedures.end() && it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING) + return; + originatorBlockAckAgreementHandler->beginTeardown(peer, tid); + } + else { + if (recipientBlockAckAgreementHandler->isTeardownPending(peer, tid)) + return; + recipientBlockAckAgreementHandler->beginTeardown(peer, tid); + } + retireAgreement(peer, tid, originator); + auto delba = makeShared(); + delba->setReceiverAddress(peer); + delba->setTid(tid); + delba->setInitiator(originator); + delba->setReasonCode(reasonCode); + processMgmtFrame(new Packet("Delba", delba), delba); + scheduleAddbaResponseTimer(); +} + +void Hcf::actionTerminated(Packet *packet, bool acknowledged) +{ + auto it = ownedActions.find(packet->getId()); + if (it == ownedActions.end()) + return; + auto action = it->second; + ownedActions.erase(it); + if (action.rtsRecoveryOwned) + edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->dropRtsProtectedFrame(packet->peekAtFront()); + if (action.kind == OwnedAction::REQUEST) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(action.peer, action.tid); + if (!agreement || agreement->getIsAddbaResponseReceived()) + return; + if (acknowledged) + originatorBlockAckAgreementHandler->acknowledgeRequest(action.peer, action.tid, + originatorBlockAckAgreementPolicy->computeAddbaFailureTimeout()); + else if (action.attempted) + // INET recovery policy confirms peer teardown before another setup. + beginTeardown(action.peer, action.tid, true, 37); + else { + originatorBlockAckAgreementHandler->finishProcedure(action.peer, action.tid); + retireAgreement(action.peer, action.tid, true); + } + } + else if (action.kind == OwnedAction::DELBA) { + if (action.originator) { + if (acknowledged) + originatorBlockAckAgreementHandler->finishProcedure(action.peer, action.tid); + else { + // INET recovery policy discards this batch after failed DELBA delivery. + originatorBlockAckAgreementHandler->failTeardown(action.peer, action.tid); + discardAgreementData(action.peer, action.tid, true); + } + } + else + recipientBlockAckAgreementHandler->finishTeardown(action.peer, action.tid); + } + scheduleAddbaResponseTimer(); + if (action.kind == OwnedAction::DELBA || (action.kind == OwnedAction::REQUEST && !acknowledged)) { + updateAgreementProcedures(); + updateContention(); + } +} + +void Hcf::resetPeer(MacAddress peer) +{ + clearInterframeHistory(); + Enter_Method("resetPeer"); + if (!originatorBlockAckAgreementHandler) + return; + bool previous = handlingAgreementAction; + handlingAgreementAction = true; + auto originators = originatorBlockAckAgreementHandler->detachPeer(peer); + auto recipients = recipientBlockAckAgreementHandler->detachPeer(peer); + for (int tid = 0; tid < 16; tid++) { + invalidateActions(peer, tid, true); + invalidateActions(peer, tid, false); + discardAgreementData(peer, tid, true); + } + for (auto agreement : recipients) + sendUp(recipientDataService->retireAgreement(agreement, true)); + recipientDataService->clearPeer(peer); + for (auto agreement : originators) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement); + delete agreement; + } + for (auto agreement : recipients) { + emit(blockAckAgreementDeletedSignal, agreement); + delete agreement; + } + handlingAgreementAction = previous; + scheduleInactivityTimer(); + scheduleAddbaResponseTimer(); +} + +void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details) +{ + if (signalID != Ieee80211Mib::rateStateChangedSignal) + return; + Enter_Method("rateStateChanged"); + // This cleanup prepares Compressed support. Basic agreements do not depend on HT admission state. + if (blockAckVariant == BlockAckVariant::BASIC || !originatorBlockAckAgreementHandler) + return; + std::vector> retired; + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + if (!isProfileAllowed(entry.first.first, true)) + retired.push_back({entry.first.first, entry.first.second, true}); + } + for (const auto& entry : recipientBlockAckAgreementHandler->getAgreements()) { + if (!isProfileAllowed(entry.first.first, false)) + retired.push_back({entry.first.first, entry.first.second, false}); + } + for (const auto& entry : retired) + beginTeardown(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry), 37); + updateContention(); +} + +void Hcf::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) { + clearInterframeHistory(); + 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()))) { + clearInterframeHistory(); + return; + } + common = header; + } + if (!isForUs(common) || !recipientBlockAckAgreementHandler) { + clearInterframeHistory(); + 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)) { + clearInterframeHistory(); + return; + } + auto topology = std::make_shared(*reception.topology); + topology->transmitter = common->getTransmitterAddress(); + topology->receiver = common->getReceiverAddress(); + topology->tid = common->getTid(); + BlockAckReceiveContext context; + if (interframeRadio) + receiveInterframe(packet, common, &context); + context.aggregate = topology; + // The PPDU consumes physical history once. Members never consume receiveInterframe separately. + if (interframeHistory.mpdu) { + interframeHistory.mpdu = false; + interframeHistory.aggregate = topology; + interframeHistory.noSifsResponse = false; + } + 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()); take(packet); + if (auto bar = dynamicPtrCast(header)) { + if (!validateBlockAckRequest(packet, bar)) { + interframeHistory = {}; + if (auto time = packet->findTag()) + pendingReceptions.erase(time->getTransmissionId()); + delete packet; + return; + } + } + BlockAckReceiveContext receiveContext; + bool interframe = interframeRadio && receiveInterframe(packet, header, &receiveContext); EV_INFO << "Processing lower frame: " << packet->getName() << endl; auto edcaf = edca->getChannelOwner(); + auto action = dynamicPtrCast(header); + bool blockAckAction = dynamicPtrCast(header) || + dynamicPtrCast(header) || dynamicPtrCast(header); + if (action && blockAckAction && isForUs(header) && edcaf && frameSequenceHandler->isSequenceRunning() && + dynamic_cast(frameSequenceHandler->getContext()->getLastStep())) { + handlingAgreementAction = true; + emit(packetReceivedFromPeerSignal, packet); + std::unique_ptr responseSource(packet->dup()); + auto accepted = recipientDataService->managementFrameReceived(packet, action); + for (auto frame : accepted) { + take(frame); + dispatchAgreementAction(frame->peekAtFront()); + } + frameSequenceHandler->interruptFrameSequence(); + recipientAckProcedure->processReceivedFrame(responseSource.get(), action, check_and_cast(recipientAckPolicy), this); + for (auto frame : accepted) + delete frame; + handlingAgreementAction = false; + updateAgreementProcedures(); + updateContention(); + return; + } if (edcaf && frameSequenceHandler->isSequenceRunning()) { // TODO always call processResponse? if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) { @@ -323,7 +1205,7 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& else if (hcca->isOwning()) throw cRuntimeError("Hcca is unimplemented!"); else if (isForUs(header)) - recipientProcessReceivedFrame(packet, header); + recipientProcessReceivedFrame(packet, header, interframe, &receiveContext); else { EV_INFO << "This frame is not for us" << std::endl; PacketDropDetails details; @@ -333,6 +1215,33 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& } } +bool Hcf::validateBlockAckRequest(Packet *packet, const Ptr& bar) const +{ + // IEEE Std 802.11-2024, 9.3.1.7.2: the supported Compressed BAR has a zero Fragment Number. + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed ? compressed->getTidInfo() : -1; + bool valid = basic || compressed; + if (basic) + valid &= basic->getFragmentNumber() == 0 && !basic->getMultiTid() && !basic->getCompressedBitmap() && + basic->getStartingSequenceNumber().isSpecified() && (basic->getReserved() & 3) == 0; + if (compressed) + valid &= compressed->getFragmentNumber() == 0 && !compressed->getMultiTid() && compressed->getCompressedBitmap() && + compressed->getStartingSequenceNumber().isSpecified(); + if (!valid || tid < 0 || tid > 15 || bar->getChunkLength() != LENGTH_BLOCKACKREQ - B(4) || + packet->getDataLength() != LENGTH_BLOCKACKREQ || bar->getTransmitterAddress().isUnspecified() || + bar->getTransmitterAddress().isMulticast() || bar->getReceiverAddress().isUnspecified() || bar->getReceiverAddress().isMulticast()) + return false; + auto agreement = recipientBlockAckAgreementHandler ? + recipientBlockAckAgreementHandler->getAgreement(tid, bar->getTransmitterAddress()) : nullptr; + // ADDBA has no variant field. INET stops before any protocol update for an active conflicting profile. + if (bar->getReceiverAddress() == mac->getAddress() && agreement && bool(compressed) != (blockAckVariant == BlockAckVariant::COMPRESSED)) + throw cRuntimeError("Block Ack profile conflict at %s: peer=%s TID=%d local=%s received=%s; ADDBA does not negotiate the profile. Configure matching blockAckVariant values and start a new run", + getFullPath().c_str(), bar->getTransmitterAddress().str().c_str(), tid, + blockAckVariant == BlockAckVariant::COMPRESSED ? "compressed" : "basic", compressed ? "compressed" : "basic"); + return true; +} + void Hcf::channelGranted(IChannelAccess *channelAccess) { Enter_Method("channelGranted"); @@ -365,7 +1274,8 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) FrameSequenceContext *Hcf::buildContext(AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - auto qosContext = new QoSContext(originatorAckPolicy, originatorBlockAckProcedure, originatorBlockAckAgreementHandler, edcaf->getTxopProcedure()); + auto qosContext = new QoSContext(originatorAckPolicy, originatorBlockAckProcedure, originatorBlockAckAgreementHandler, edcaf->getTxopProcedure(), blockAckVariant); + qosContext->prepareData = [this](FrameSequenceContext *context) { return prepareDataTransmission(context); }; return new FrameSequenceContext(mac->getAddress(), modeSet, edcaf->getInProgressFrames(), rtsProcedure, rtsPolicy, nullptr, qosContext); } @@ -385,7 +1295,35 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) for (auto edcaf : internallyCollidedEdcafs) { AccessCategory ac = edcaf->getAccessCategory(); auto dataRecoveryProcedure = edcaf->getRecoveryProcedure(); - Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); + if (originatorAckPolicy->isBlockAckReqNeeded(edcaf->getInProgressFrames(), edcaf->getTxopProcedure(), originatorBlockAckAgreementHandler)) { + dataRecoveryProcedure->controlFrameTransmissionFailed(); + edcaf->requestChannel(this); + continue; + } + auto frames = edcaf->getInProgressFrames(); + Packet *internallyCollidedFrame = nullptr; + bool scopedQueuedData = false; + if (blockAckVariant == BlockAckVariant::COMPRESSED && originatorBlockAckAgreementHandler) { + auto candidate = frames->peekFrameToTransmit(); + auto data = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + auto agreement = data ? originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()) : nullptr; + if (data && data->getType() == ST_DATA_WITH_QOS && !data->getReceiverAddress().isMulticast() && + agreement && agreement->getIsAddbaResponseReceived() && !frames->containsFrame(candidate)) { + scopedQueuedData = true; + // Collision recovery needs a numbered frame, but it must not form an unadmitted aggregate. + auto service = check_and_cast(originatorDataService); + auto prepared = service->prepareFrame(edcaf->getPendingQueue(), false); + if (prepared) { + auto selected = prepared->packet->peekAtFront(); + if (selected->getReceiverAddress() == data->getReceiverAddress() && selected->getTid() == data->getTid()) + internallyCollidedFrame = service->commitPreparedFrame(*prepared); + } + if (internallyCollidedFrame) + frames->adoptPreparedFrame(internallyCollidedFrame); + } + } + if (!internallyCollidedFrame && !scopedQueuedData) + internallyCollidedFrame = frames->getFrameToTransmit(); if (internallyCollidedFrame == nullptr) continue; auto internallyCollidedHeader = internallyCollidedFrame->peekAtFront(); @@ -420,6 +1358,8 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, internallyCollidedFrame, &transmissionDetails); } + else if (auto dataHeader = dynamicPtrCast(internallyCollidedHeader)) + firstDataDiscarded(dataHeader); if (hasFrameToTransmit(ac)) edcaf->requestChannel(this); } @@ -437,12 +1377,17 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) void Hcf::frameSequenceFinished() { Enter_Method("frameSequenceFinished"); + cancelEvent(startRxTimer); + finishingSequence = true; emit(IFrameSequenceHandler::frameSequenceFinishedSignal, frameSequenceHandler->getContext()); auto edcaf = edca->getChannelOwner(); if (edcaf) { // The context still owns references. dropFrame defers deletion until context disposal. expireDataFrames(false); - bool startContention = hasFrameToTransmit(); // TODO outstanding frame + advanceOriginatorWindows(); + completeTransfers(); + updateAgreementProcedures(); + bool startContention = !handlingAgreementAction && hasFrameToTransmit(); edcaf->releaseChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review edcaf->getTxopProcedure()->endTxop(); @@ -457,22 +1402,85 @@ void Hcf::frameSequenceFinished() } else throw cRuntimeError("Frame sequence finished but channel owner not found!"); + obsoleteActions.clear(); + finishingSequence = false; } -void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) +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); + if (recipientBlockAckAgreementHandler) { + // IEEE Std 802.11-2024, 11.5.4: valid Block Ack traffic without an agreement requires DELBA. + // Reject malformed DATA and unsupported BAR fields before this recovery path. + if (auto data = dynamicPtrCast(header)) { + auto agreement = data->getType() == ST_DATA_WITH_QOS ? + recipientBlockAckAgreementHandler->getAgreement(data->getTid(), data->getTransmitterAddress()) : nullptr; + if (agreement && (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified())) { + delete packet; + return; + } + if (agreement && blockAckVariant == BlockAckVariant::COMPRESSED && + (data->getFragmentNumber() != 0 || data->getMoreFragments())) { + delete packet; + return; + } + if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK && + (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified() || + data->getTransmitterAddress().isUnspecified() || data->getTransmitterAddress().isMulticast())) { + delete packet; + return; + } + if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK && + !recipientBlockAckAgreementHandler->getAgreement(data->getTid(), data->getTransmitterAddress())) { + beginTeardown(data->getTransmitterAddress(), data->getTid(), false, 38); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + return; + } + } + else if (auto bar = dynamicPtrCast(header)) { + if (!validateBlockAckRequest(packet, bar)) { + delete packet; + return; + } + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = recipientBlockAckAgreementHandler->getAgreement(tid, bar->getTransmitterAddress()); + if (!agreement) { + beginTeardown(bar->getTransmitterAddress(), tid, false, 38); + delete packet; + return; + } + agreement->calculateExpirationTime(); + scheduleInactivityTimer(); + } + } if (auto dataOrMgmtHeader = dynamicPtrCast(header)) recipientAckProcedure->processReceivedFrame(packet, dataOrMgmtHeader, check_and_cast(recipientAckPolicy), this); if (auto dataHeader = dynamicPtrCast(header)) { if (dataHeader->getType() == ST_DATA_WITH_QOS && recipientBlockAckAgreementHandler) recipientBlockAckAgreementHandler->qosFrameReceived(dataHeader, this); - sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); + sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler, interframe, context)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { - sendUp(recipientDataService->managementFrameReceived(packet, mgmtHeader)); - recipientProcessReceivedManagementFrame(mgmtHeader); + auto accepted = recipientDataService->managementFrameReceived(packet, mgmtHeader); + for (auto frame : accepted) { + take(frame); + auto completeHeader = frame->peekAtFront(); + if (dynamicPtrCast(completeHeader)) { + recipientProcessReceivedManagementFrame(completeHeader); + delete frame; + } + else + sendUp({frame}); + } + updateAgreementProcedures(); + updateContention(); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -485,7 +1493,7 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); - else if (auto blockAckRequest = dynamicPtrCast(header)) { + else if (auto blockAckRequest = dynamicPtrCast(header)) { if (recipientBlockAckProcedure) recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } @@ -496,40 +1504,97 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header) +{ + dispatchAgreementAction(header); +} + +bool Hcf::dispatchAgreementAction(const Ptr& header) { if (recipientBlockAckAgreementHandler && originatorBlockAckAgreementHandler) { if (auto addbaRequest = dynamicPtrCast(header)) { + auto peer = addbaRequest->getTransmitterAddress(); + auto tid = addbaRequest->getTid(); + auto previous = recipientBlockAckAgreementHandler->getAgreement(tid, peer); + if (!isProfileAllowed(peer, false) || hasObsoleteActionInFlight(peer, tid, false)) { + auto response = makeShared(); + response->setReceiverAddress(peer); + response->setTid(tid); + response->setDialogToken(addbaRequest->getDialogToken()); + response->setStatusCode(SC_UNSPECIFIED); + processMgmtFrame(new Packet("AddbaResponse", response), response); + return false; + } recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); - emit(blockAckAgreementAddedSignal, agreement); + if (!previous && agreement) + agreement->setVariant(blockAckVariant); + if (!previous && agreement) + clearInterframeHistory(); + if (!previous && agreement) + emit(blockAckAgreementAddedSignal, agreement); + scheduleInactivityTimer(); } else if (auto addbaResp = dynamicPtrCast(header)) { + auto peer = addbaResp->getTransmitterAddress(); + auto tid = addbaResp->getTid(); + auto pending = originatorBlockAckAgreementHandler->getAgreement(peer, tid); + if (!pending || pending->getIsAddbaResponseReceived() || pending->getDialogToken() != addbaResp->getDialogToken()) + return false; + if (addbaResp->getStatusCode() != SC_SUCCESSFUL) { + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + retireAgreement(peer, tid, true); + scheduleAddbaResponseTimer(); + return true; + } + if (!isProfileAllowed(peer, true) || !originatorBlockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, pending)) + return false; originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); auto agreement = originatorBlockAckAgreementHandler->getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + invalidateActions(peer, tid, true); emit(blockAckAgreementAddedSignal, agreement); + scheduleAddbaResponseTimer(); + if (!hasQueuedData(peer, tid) && !hasNumberedData(peer, tid)) + beginTeardown(peer, tid, true, 37); + return true; } else if (auto delba = dynamicPtrCast(header)) { - if (delba->getInitiator()) { - auto agreement = recipientBlockAckAgreementHandler->getAgreement(delba->getTid(), delba->getReceiverAddress()); - emit(blockAckAgreementDeletedSignal, agreement); - recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); - } - else { - auto agreement = originatorBlockAckAgreementHandler->getAgreement(delba->getReceiverAddress(), delba->getTid()); - emit(blockAckAgreementDeletedSignal, agreement); - originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy); - } + auto peer = delba->getTransmitterAddress(); + auto tid = delba->getTid(); + bool originator = !delba->getInitiator(); + if (tid < 0 || tid > 15 || peer.isMulticast() || peer.isUnspecified()) + return false; + if (originator) + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + else + recipientBlockAckAgreementHandler->finishTeardown(peer, tid); + retireAgreement(peer, tid, originator); + scheduleAddbaResponseTimer(); + return true; } else ; // Beacon, etc } else ; // Optional modules + return false; } void Hcf::transmissionComplete(Packet *packet, const Ptr& header) { Enter_Method("transmissionComplete"); + if (!header) { + completedTransmissionWasAck = completedTransmissionWasBlockAck = false; + 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; + } + completedTransmissionWasAck = header->getType() == ST_ACK; + completedTransmissionWasBlockAck = bool(dynamicPtrCast(header)); auto edcaf = edca->getChannelOwner(); if (edcaf) { auto step = check_and_cast(frameSequenceHandler->getContext()->getLastStep()); @@ -550,6 +1615,17 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) Enter_Method("originatorProcessRtsProtectionFailed"); auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + if (prepared && prepared->frame == packet && (prepared->preparation || 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())) + edcaf->getRecoveryProcedure()->controlFrameTransmissionFailed(); + return; + } + if (obsoleteActions.count(packet->getId()) || !edcaf->getInProgressFrames()->containsFrame(packet)) + return; if (edcaf) { EV_INFO << "RTS frame transmission failed\n"; bool retryLimitReached = false; @@ -559,6 +1635,9 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) { edcaf->getMgmtAndNonQoSRecoveryProcedure()->rtsFrameTransmissionFailed(mgmtHeader, edcaf->getStationRetryCounters()); + auto action = ownedActions.find(packet->getId()); + if (action != ownedActions.end()) + action->second.rtsRecoveryOwned = true; retryLimitReached = edcaf->getMgmtAndNonQoSRecoveryProcedure()->isRtsFrameRetryLimitReached(packet, mgmtHeader); } else @@ -577,7 +1656,10 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) details.setLimit(-1); // TODO emit(packetDroppedSignal, packet, &details); emit(linkBrokenSignal, packet); + if (auto data = dynamicPtrCast(protectedHeader)) + firstDataDiscarded(data); if (dynamicPtrCast(protectedHeader)) { + actionTerminated(packet, false); FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -591,6 +1673,25 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) 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()) || + (dynamicPtrCast(packet->peekAtFront()) && + !edca->getChannelOwner()->getInProgressFrames()->containsFrame(packet))) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; auto transmittedHeader = packet->peekAtFront(); if (auto dataOrMgmt = dynamicPtrCast(transmittedHeader)) { @@ -599,9 +1700,7 @@ void Hcf::originatorProcessTransmittedFrame(Packet *packet) rx->successfulFrameTransmitted(step->getPpduEnd(), transmittedHeader->getDurationField()); } } - auto edcaf = edca->getChannelOwner(); if (edcaf) { - edcaf->emit(packetSentToPeerSignal, packet); AccessCategory ac = edcaf->getAccessCategory(); if (transmittedHeader->getReceiverAddress().isMulticast()) { if (dynamicPtrCast(transmittedHeader)) @@ -630,6 +1729,14 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); + if (originatorBlockAckAgreementHandler && dataHeader->getType() == ST_DATA_WITH_QOS && + !dataHeader->getReceiverAddress().isMulticast() && isProfileAllowed(dataHeader->getReceiverAddress(), true) && + originatorBlockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && + !originatorBlockAckAgreementHandler->getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid())) { + originatorBlockAckAgreementHandler->noteEligibleDataTransmission(dataHeader->getReceiverAddress(), dataHeader->getTid()); + if (hasQueuedData(dataHeader->getReceiverAddress(), dataHeader->getTid())) + originatorBlockAckAgreementHandler->beginSetupIntent(dataHeader->getReceiverAddress(), dataHeader->getTid()); + } if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } @@ -657,8 +1764,15 @@ void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& controlHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) - edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq); + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + auto basic = dynamicPtrCast(blockAckReq); + auto compressed = dynamicPtrCast(blockAckReq); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), tid) : nullptr; + if (agreement) + edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq, agreement); + } else if (auto rtsFrame = dynamicPtrCast(controlHeader)) rtsProcedure->processTransmittedRts(rtsFrame); else @@ -668,8 +1782,16 @@ 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()) || + (dynamicPtrCast(failedHeader) && !edcaf->getInProgressFrames()->containsFrame(failedPacket))) + return; if (edcaf) { bool retryLimitReached = false; if (auto dataHeader = dynamicPtrCast(failedHeader)) { @@ -695,6 +1817,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else if (auto blockAckReq = dynamicPtrCast(failedHeader)) { edcaf->getAckHandler()->processFailedBlockAckReq(blockAckReq); + edcaf->getRecoveryProcedure()->controlFrameTransmissionFailed(); return; } else @@ -713,6 +1836,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) emit(packetDroppedSignal, failedPacket, &details); emit(linkBrokenSignal, failedPacket); if (dynamicPtrCast(failedHeader)) { + actionTerminated(failedPacket, false); FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); @@ -732,8 +1856,17 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) void Hcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTransmittedPacket) { Enter_Method("originatorProcessReceivedFrame"); - EV_INFO << "Processing received frame " << receivedPacket->getName() << " as originator in frame sequence.\n"; emit(packetReceivedFromPeerSignal, receivedPacket); + 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))) + return; + EV_INFO << "Processing received frame " << receivedPacket->getName() << " as originator in frame sequence.\n"; auto receivedHeader = receivedPacket->peekAtFront(); auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); auto step = check_and_cast(frameSequenceHandler->getContext()->getStepBeforeLast()); @@ -784,24 +1917,42 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); + advanceOriginatorWindows(); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { + actionTerminated(lastTransmittedPacket, true); FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, lastTransmittedPacket, &transmissionDetails); } + completeTransfers(); } - else if (auto blockAck = dynamicPtrCast(header)) { - EV_INFO << "BasicBlockAck has arrived" << std::endl; + else if (auto blockAck = dynamicPtrCast(header)) { + auto basic = dynamicPtrCast(blockAck); + auto compressed = dynamicPtrCast(blockAck); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), tid) : nullptr; + if (!agreement) + return; + EV_INFO << "BlockAck has arrived" << std::endl; edcaf->getRecoveryProcedure()->blockAckFrameReceived(); - auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); + auto ackedSeqAndFragNums = compressed ? edcaf->getAckHandler()->processReceivedCompressedBlockAck(compressed, agreement) : + edcaf->getAckHandler()->processReceivedBlockAck(blockAck); if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); EV_TRACE << "It has acknowledged the following frames:" << std::endl; for (auto it : ackedSeqAndFragNums) EV_TRACE << " sequenceNumber = " << it.second.second.getSequenceNumber() << ", fragmentNumber = " << (int)it.second.second.getFragmentNumber() << std::endl; + for (int i = 0; i < edcaf->getInProgressFrames()->getLength(); i++) { + auto data = dynamicPtrCast(edcaf->getInProgressFrames()->getFrames(i)->peekAtFront()); + if (data && edcaf->getAckHandler()->getQoSDataAckStatus(data) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED) + edcaf->getRecoveryProcedure()->dropFrame(data); + } edcaf->getInProgressFrames()->dropFrames(ackedSeqAndFragNums); + advanceOriginatorWindows(); edcaf->getAckHandler()->dropFrames(ackedSeqAndFragNums); + completeTransfers(); } else if (dynamicPtrCast(header)) ; // void @@ -819,7 +1970,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetType() == ST_DATA_WITH_QOS) ; // void - else if (dynamicPtrCast(header)) + else if (dynamicPtrCast(header)) ; // void else throw cRuntimeError("Unknown control frame"); @@ -833,9 +1984,17 @@ void Hcf::originatorProcessReceivedDataFrame(const PtrgetEdcaf(ac); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return (originatorBlockAckAgreementHandler && + originatorAckPolicy->isBlockAckReqNeeded(edcaf->getInProgressFrames(), edcaf->getTxopProcedure(), originatorBlockAckAgreementHandler)) || + edcaf->getInProgressFrames()->hasEligibleInProgressFrames() || + check_and_cast(originatorDataService)->peekEligibleFrame(edcaf->getPendingQueue()) != nullptr; else throw cRuntimeError("Hcca is unimplemented"); } @@ -845,7 +2004,7 @@ bool Hcf::hasFrameToTransmit() expireDataFrames(); auto edcaf = edca->getChannelOwner(); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return hasFrameToTransmit(edcaf->getAccessCategory()); else throw cRuntimeError("Hcca is unimplemented"); } @@ -879,7 +2038,8 @@ simtime_t Hcf::computeMultipleProtectionDuration(simtime_t limit, simtime_t rema duration = limit - ppduDuration; else { if (reservation - ppduDuration > remaining - ppduDuration) - throw cRuntimeError("Multiple protection duration has an empty raw interval"); + throw cRuntimeError("Multiple protection duration has an empty raw interval: reservation=%s, remaining=%s, PPDU=%s", + reservation.str().c_str(), remaining.str().c_str(), ppduDuration.str().c_str()); duration = remaining - ppduDuration; } return encodeDuration(duration, true); @@ -912,8 +2072,9 @@ simtime_t Hcf::estimateZeroLimitExchange(Packet *packet, const Ptr(header)) - return ppduDuration + sifs + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + if (auto bar = dynamicPtrCast(header)) + return ppduDuration + sifs + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration( + dynamicPtrCast(bar) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); if (auto dataOrMgmt = dynamicPtrCast(header)) { if (recipientAckPolicy->isAckNeeded(dataOrMgmt)) return ppduDuration + sifs + rateSelection->computeResponseAckFrameMode(packet, dataOrMgmt, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK); @@ -928,6 +2089,429 @@ simtime_t Hcf::computeImmediateResponseDuration(Packet *packet, const PtrcomputeResponseAckFrameMode(packet, header, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK) : SIMTIME_ZERO; } +bool Hcf::isInitialDataException(const FrameSequenceContext *context, const PreparedDataTransmission& prepared) const +{ + if (prepared.batch && (prepared.batch->members.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 && (dynamic_cast(step) || + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront()))) + return false; + } + return true; +} + +simtime_t Hcf::computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const +{ + auto sifs = modeSet->getSifsTime(); + auto cost = prepared.dataMode->getDuration(prepared.batch ? prepared.envelope->getDataLength() : prepared.frame->getDataLength()); + if (prepared.ackMode) + 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; + return cost; +} + +bool Hcf::canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, + 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 && (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. + if (txop->getLimit() == 0) { + if (previousData != 0) + 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) { + auto last = dynamic_cast(context->getLastStep()); + later = !last || last->getPpduEnd() >= SIMTIME_ZERO || context->getNumSteps() > 1; + } + // 10.23.2.8 samples TXNAV at exchange completion, before the leading IFS. + if (later && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining() + 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) + return false; + return true; +} + +bool Hcf::validatePreparedData(const PreparedDataTransmission& prepared, simtime_t start) const +{ + if (prepared.cancelled || prepared.modeSet != modeSet) + return false; + auto header = prepared.frame->peekAtFront(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()) : nullptr; + if (agreement != prepared.agreement || !agreement || !agreement->getIsAddbaResponseReceived() || + !isProfileAllowed(header->getReceiverAddress(), true)) + return false; + auto service = check_and_cast(originatorDataService); + if (prepared.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; + } + else if (!edca->getChannelOwner()->getInProgressFrames()->containsFrame(prepared.frame) || + !isFrameEligible(prepared.frame, true) || edca->getLifetimeHandler()->isExpired(prepared.frame, start)) + return false; + auto peer = mib->findPeerHtState(header->getReceiverAddress()); + if (!isPeerCompatibleMode(modeSet, peer, prepared.dataMode) || + (prepared.rtsMode && !isPeerCompatibleMode(modeSet, peer, prepared.rtsMode))) + return false; + if (originatorAckPolicy->computeAckPolicy(prepared.frame, header, agreement) != header->getAckPolicy()) + return false; + if (prepared.ackMode && (!modeSet->containsMode(prepared.ackMode) || + rateSelection->computeResponseAckFrameMode(prepared.frame, header, ResponseQueryRole::PREDICT) != prepared.ackMode)) + return false; + if (prepared.ctsMode && (!modeSet->containsMode(prepared.ctsMode) || + rateSelection->computeResponseCtsFrameMode(prepared.rtsPrediction.get(), + prepared.rtsPrediction->peekAtFront(), ResponseQueryRole::PREDICT) != prepared.ctsMode)) + return false; + return true; +} + +DataPreparationResult Hcf::prepareDataTransmission(FrameSequenceContext *context) +{ + Enter_Method("prepareDataTransmission"); + if (blockAckVariant != BlockAckVariant::COMPRESSED || !originatorBlockAckAgreementHandler) + return DataPreparationResult::LEGACY; + auto frames = context->getInProgressFrames(); + auto candidate = frames->peekFrameToTransmit(); + auto header = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + if (!header || header->getType() != ST_DATA_WITH_QOS || header->getReceiverAddress().isMulticast()) + return DataPreparationResult::LEGACY; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()); + if (!agreement || !agreement->getIsAddbaResponseReceived()) + return DataPreparationResult::LEGACY; + auto prepared = std::make_unique(); + prepared->modeSet = modeSet; + prepared->agreement = agreement; + auto service = check_and_cast(originatorDataService); + if (frames->containsFrame(candidate)) + prepared->frame = candidate; + else { + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), !service->hasAMpduAggregation()); + if (!prepared->preparation) + return DataPreparationResult::DEFERRED; + prepared->frame = prepared->preparation->packet.get(); + } + auto selected = prepared->frame->peekAtFront(); + if (selected->getReceiverAddress() != header->getReceiverAddress() || selected->getTid() != header->getTid()) + return DataPreparationResult::DEFERRED; + auto txop = context->getQoSContext()->txopProcedure; + prepared->dataMode = rateSelection->computeMode(prepared->frame, + prepared->frame->peekAtFront(), txop); + if (service->hasAMpduAggregation() && prepareAggregateData(context, *prepared)) + return DataPreparationResult::READY; + const IIeee80211Mode *selectedRtsMode = nullptr; + for (;;) { + auto packet = prepared->frame; + auto policy = originatorAckPolicy->computeAckPolicy(packet, packet->peekAtFront(), agreement); + auto updated = packet->removeAtFront(); + bool retry = false; + if (!prepared->preparation) { + auto status = edca->getChannelOwner()->getAckHandler()->getQoSDataAckStatus(updated); + retry = status == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED || + status == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED || status == QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED; + } + // Forwarded MSDUs can carry the preceding hop's Retry bit. Local ACK state owns this hop's retry decision. + updated->setRetry(retry); + updated->setAckPolicy(policy); + packet->insertAtFront(updated); + setFrameMode(packet, updated, prepared->dataMode); + prepared->ackMode = recipientAckPolicy->isAckNeeded(updated) ? + rateSelection->computeResponseAckFrameMode(packet, updated, ResponseQueryRole::PREDICT) : nullptr; + bool protect = rtsPolicy->isRtsNeeded(packet, updated); + prepared->setRtsPrediction(nullptr); + prepared->rtsMode = nullptr; + prepared->ctsMode = nullptr; + if (protect) { + prepared->setRtsPrediction(new Packet("prepared-RTS", rtsProcedure->buildRtsFrame(updated))); + prepared->rtsPrediction->insertAtBack(makeShared()); + auto rts = prepared->rtsPrediction->peekAtFront(); + if (!selectedRtsMode) + selectedRtsMode = rateSelection->computeMode(prepared->rtsPrediction.get(), rts, txop); + prepared->rtsMode = selectedRtsMode; + setFrameMode(prepared->rtsPrediction.get(), rts, selectedRtsMode); + prepared->ctsMode = rateSelection->computeResponseCtsFrameMode(prepared->rtsPrediction.get(), rts, ResponseQueryRole::PREDICT); + } + if (validatePreparedData(*prepared, simTime() + context->getIfs()) && + canAdmitPreparedData(context, *prepared, context->getIfs(), protect)) { + context->getQoSContext()->preparedData = std::move(prepared); + return DataPreparationResult::READY; + } + if (!prepared->preparation || !prepared->preparation->aggregate) + return DataPreparationResult::DEFERRED; + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), false); + if (!prepared->preparation) + return DataPreparationResult::DEFERRED; + prepared->frame = prepared->preparation->packet.get(); + } +} + +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) { @@ -974,6 +2558,39 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) Enter_Method("transmitFrame"); auto channelOwner = edca->getChannelOwner(); if (channelOwner) { + 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)); + if (preparedStep && (!validatePreparedData(*prepared, simTime() + ifs) || + !canAdmitPreparedData(context, *prepared, ifs, rtsStep != nullptr))) + return false; + if (dynamicPtrCast(header) && + !(preparedStep && (prepared->preparation || prepared->batch)) && + !channelOwner->getInProgressFrames()->containsFrame(packet)) + return false; + if (!isFrameEligible(packet, true)) + return false; + if (auto dataHeader = dynamicPtrCast(header)) { + auto status = channelOwner->getAckHandler()->getQoSDataAckStatus(dataHeader); + if (dataHeader->getType() == ST_DATA_WITH_QOS && + (status == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED || status == QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED)) { + // IEEE Std 802.11-2024, 9.2.4.1.6: DATA after a failed Block Ack exchange is a retransmission. + auto updated = packet->removeAtFront(); + updated->setRetry(true); + packet->insertAtFront(updated); + header = updated; + } + } auto lifetimeHandler = edca->getLifetimeHandler(); auto step = frameSequenceHandler->getContext()->getLastStep(); auto protectedPacket = dynamic_cast(step); @@ -981,9 +2598,7 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) // IEEE Std 802.11-2024, 10.23.2.12.1: a delayed start cannot exceed DATA lifetime. if (lifetimeHandler->isExpired(data, simTime() + ifs)) return false; - auto header = packet->peekAtFront(); auto txop = channelOwner->getTxopProcedure(); - auto context = frameSequenceHandler->getContext(); bool scopedSingle = txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && !par("isBlockAckSupported").boolValue(); Packet *retainedPacket = packet; auto retainedModeTag = scopedSingle ? retainedPacket->findTag() : nullptr; @@ -996,9 +2611,55 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && receiveStep->getReceivedFrame()->peekAtFront()->getType() == ST_CTS; } - auto mode = dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); + auto compressedBar = dynamicPtrCast(header); + auto mode = preparedStep ? (rtsStep ? prepared->rtsMode : prepared->dataMode) : + compressedBar ? rateSelection->computeCompressedBlockAckReqMode(packet, compressedBar, context->getNumSteps() == 1) : + dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); setFrameMode(packet, header, mode); - if (scopedSingle) { + if (auto bar = dynamicPtrCast(header)) { + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), tid) : nullptr; + if (!agreement || !agreement->getIsAddbaResponseReceived()) + return false; + auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration( + compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + response; + // IEEE Std 802.11-2024, 10.23.2.8 and 10.23.2.9: a later BAR must fit both bounds. + if (context->getNumSteps() > 1 && + ((txop->getLimit() > 0 && ifs + cost > txop->getRemaining()) || + (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()))) + return false; + } + if (preparedStep) { + auto response = rtsStep ? prepared->ctsMode->getDuration(LENGTH_CTS) : + prepared->ackMode ? prepared->ackMode->getDuration(LENGTH_ACK) : SIMTIME_ZERO; + simtime_t duration; + if (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION) { + // Reserve this exchange only. A later BAR obtains contention through the existing arbitration path. + duration = encodeDuration(singleProtectionMechanism->computeDurationField(header, response, + rtsStep ? computePreparedDataCost(*prepared, false) : SIMTIME_ZERO, true), true); + } + else { + auto ppduDuration = mode->getDuration(packet->getDataLength()); + auto projectedStart = simTime() + ifs; + auto reservation = std::max(SIMTIME_ZERO, txop->getReservationEnd() - projectedStart); + auto remaining = std::max(SIMTIME_ZERO, txop->getStart() + txop->getLimit() - projectedStart); + auto cost = computePreparedDataCost(*prepared, rtsStep != nullptr); + bool singleDataOverrun = txop->getLimit() > 0 && ifs + cost > txop->getRemaining() && + isInitialDataException(context, *prepared); + // An allowed single-DATA overrun protects its actual exchange beyond the nominal limit. + duration = singleDataOverrun ? computeMultipleProtectionDuration(0, 0, 0, ppduDuration, cost, false) : + computeMultipleProtectionDuration(txop->getLimit(), remaining, reservation, ppduDuration, + txop->getLimit() == 0 ? cost : SIMTIME_ZERO, txop->getReservationEnd() != 0); + } + auto updated = packet->removeAtFront(); + updated->setDurationField(duration); + packet->insertAtFront(updated); + } + else if (scopedSingle) { auto sifs = modeSet->getSifsTime(); auto cost = mode->getDuration(packet->getDataLength()); simtime_t responseDuration = SIMTIME_ZERO; @@ -1050,7 +2711,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) packet->insertAtFront(updated); } else if (txop->getProtectionMechanism() == TxopProcedure::ProtectionMechanism::SINGLE_PROTECTION) { - auto pendingPacket = channelOwner->getInProgressFrames()->getPendingFrameFor(packet); + auto pendingPacket = dynamicPtrCast(header) ? nullptr : + channelOwner->getInProgressFrames()->getPendingFrameFor(packet, false); + if (dynamicPtrCast(header) && txop->getLimit() == 0) { + // The zero-limit frame sequence ends after this exchange; it has no successor reservation. + pendingPacket = nullptr; + } + else if (pendingPacket && !channelOwner->getInProgressFrames()->containsFrame(pendingPacket)) { + // HcfFs ends this TXOP before extraction can change the successor's size or setup state. + pendingPacket = nullptr; + } const auto& pendingHeader = pendingPacket == nullptr ? nullptr : pendingPacket->peekAtFront(); auto duration = singleProtectionMechanism->computeDurationField(packet, header, pendingPacket, pendingHeader, txop, recipientAckPolicy); auto header = packet->removeAtFront(); @@ -1074,9 +2744,32 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) } else throw cRuntimeError("Undefined protection mechanism"); + if (preparedStep && packet == prepared->frame && prepared->preparation) { + auto service = check_and_cast(originatorDataService); + auto committed = service->commitPreparedFrame(*prepared->preparation); + if (!committed) + return false; + ASSERT(committed == packet); + prepared->preparation.reset(); + channelOwner->getInProgressFrames()->adoptPreparedFrame(committed); + header = committed->peekAtFront(); + } RateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; tx->transmitFrame(packet, packet->peekAtFront(), ifs, false, this); + if (auto dataHeader = dynamicPtrCast(header)) { + if (originatorBlockAckAgreementHandler) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived()) + agreement->recordTransmittedData(packet->getTag()->getAllocationIndex()); + } + } + auto action = ownedActions.find(packet->getId()); + if (action != ownedActions.end()) { + action->second.attempted = true; + if (action->second.kind == OwnedAction::REQUEST) + originatorBlockAckAgreementHandler->markRequestAttempted(action->second.peer, action->second.tid); + } return true; } else @@ -1090,7 +2783,7 @@ void Hcf::transmitControlResponseFrame(Packet *responsePacket, const Ptr(receivedHeader)) responseMode = rateSelection->computeResponseCtsFrameMode(receivedPacket, rtsFrame, ResponseQueryRole::RESPOND); - else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) + else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseBlockAckFrameMode(receivedPacket, blockAckReq, ResponseQueryRole::RESPOND); else if (auto dataOrMgmtHeader = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseAckFrameMode(receivedPacket, dataOrMgmtHeader, ResponseQueryRole::RESPOND); @@ -1124,6 +2817,37 @@ void Hcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const void Hcf::processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) { Enter_Method("processMgmtFrame"); + OwnedAction action; + action.peer = mgmtHeader->getReceiverAddress(); + bool owned = true; + if (auto request = dynamicPtrCast(mgmtHeader)) { + action.tid = request->getTid(); + action.originator = true; + action.kind = OwnedAction::REQUEST; + } + else if (auto response = dynamicPtrCast(mgmtHeader)) { + action.tid = response->getTid(); + action.originator = false; + action.kind = OwnedAction::RESPONSE; + } + else if (auto delba = dynamicPtrCast(mgmtHeader)) { + action.tid = delba->getTid(); + action.originator = delba->getInitiator(); + action.kind = OwnedAction::DELBA; + bool pending = action.originator ? + originatorBlockAckAgreementHandler->getProcedures().count({action.peer, action.tid}) != 0 && + originatorBlockAckAgreementHandler->getProcedures().at({action.peer, action.tid}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING : + recipientBlockAckAgreementHandler->isTeardownPending(action.peer, action.tid); + if (!pending) { + beginTeardown(action.peer, action.tid, action.originator, delba->getReasonCode()); + delete mgmtPacket; + return; + } + } + else + owned = false; + if (owned) + ownedActions.emplace(mgmtPacket->getId(), action); mgmtPacket->insertAtBack(makeShared()); processUpperFrame(mgmtPacket, mgmtHeader); } @@ -1158,6 +2882,7 @@ bool Hcf::isSentByUs(const Ptr& header) const void Hcf::corruptedFrameReceived() { Enter_Method("corruptedFrameReceived"); + clearInterframeHistory(); if (frameSequenceHandler->isSequenceRunning() && !startRxTimer->isScheduled()) { frameSequenceHandler->handleStartRxTimeout(); } @@ -1167,6 +2892,14 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + if (tx) + tx->cancelPendingAggregate(this); + if (interframeRadio) { + auto radioModule = check_and_cast(interframeRadio.get()); + for (auto signal : {receptionStartedSignal, receptionEndedSignal, transmissionStartedSignal, transmissionEndedSignal, IRadio::receptionStateChangedSignal}) + radioModule->unsubscribe(signal, this); + } + cancelAndDelete(addbaResponseTimer); cancelAndDelete(dataLifetimeTimer); cancelAndDelete(startRxTimer); cancelAndDelete(inactivityTimer); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 4a0537880f8..7c2dba912f5 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,10 +8,13 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" #include "inet/linklayer/ieee80211/mac/contract/ICtsPolicy.h" @@ -36,33 +39,88 @@ #include "inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/CtsProcedure.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h" +#include "inet/linklayer/ieee80211/mac/contract/AmpduReception.h" namespace inet { namespace ieee80211 { class Ieee80211Mac; +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: - enum class BlockAckVariant { BASIC, COMPRESSED }; BlockAckVariant blockAckVariant = BlockAckVariant::BASIC; + bool validateBlockAckRequest(Packet *packet, const Ptr& bar) const; + struct InterframeHistory { + physicallayer::TransmissionTimingSnapshot timing; + bool mpdu = false; + bool ack = false; + bool blockAck = false; + bool ht = false; + bool noSifsResponse = false; + MacAddress receiver; + std::optional extension; + std::shared_ptr aggregate; + }; + struct PendingReception { + bool completed = false; + bool sifs = false; + bool rifs = false; + bool overlapped = false; + bool precedingIndividual = false; + bool precedingAggregate = false; + std::shared_ptr previousAggregate; + MacAddress precedingReceiver; + physicallayer::TransmissionTimingSnapshot timing; + bool ht = false; + std::optional extension; + }; + opp_component_ptr interframeRadio; + InterframeHistory interframeHistory; + std::map pendingReceptions; + bool completedTransmissionWasAck = false; + bool completedTransmissionWasBlockAck = false; + std::shared_ptr transmittedBlockAckAggregate; + void clearInterframeHistory(); + bool receiveInterframe(Packet *packet, const Ptr& header, + BlockAckReceiveContext *context = nullptr); public: + Packet *prepareAggregateTransmission() override; + void aggregateTransmissionCancelled() override; + void frameTransmissionStarted(Packet *packet) override; static simsignal_t edcaCollisionDetectedSignal; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; protected: Ieee80211Mac *mac = nullptr; + Ieee80211Mib *mib = nullptr; IRateControl *dataAndMgmtRateControl = nullptr; cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; cMessage *dataLifetimeTimer = nullptr; + cMessage *addbaResponseTimer = nullptr; + struct OwnedAction { + MacAddress peer; + Tid tid = -1; + bool originator = false; + enum Kind { REQUEST, RESPONSE, DELBA } kind = REQUEST; + bool attempted = false; + bool rtsRecoveryOwned = false; + }; + std::map ownedActions; + std::set obsoleteActions; + bool handlingAgreementAction = false; + bool finishingSequence = false; + bool updatingProcedures = false; // Transmission and Reception IRx *rx = nullptr; @@ -113,6 +171,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -123,6 +183,14 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: FrameSequenceContext *buildContext(AccessCategory ac); static simtime_t encodeDuration(simtime_t duration, bool holder); simtime_t computeImmediateResponseDuration(Packet *packet, const Ptr& header); + DataPreparationResult prepareDataTransmission(FrameSequenceContext *context); + bool 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, 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); static simtime_t computeMultipleProtectionDuration(simtime_t limit, simtime_t remaining, @@ -131,13 +199,35 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: TxopProcedure *txop, simtime_t ppduDuration); virtual bool hasFrameToTransmit(); virtual bool hasFrameToTransmit(AccessCategory ac); + bool hasEligibleFrameToTransmit(AccessCategory ac) const; virtual bool isReceptionInProgress(); void scheduleDataLifetimeTimer(); void expireDataFrames(bool preserveSequence = true); std::set getReferencedDataFrames() const; + bool isFrameEligible(Packet *packet, bool numbered) const; + bool isAMsduPermitted(Packet *packet) const; + bool hasQueuedData(MacAddress peer, Tid tid) const; + bool hasNumberedData(MacAddress peer, Tid tid) const; + bool hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const; + void updateAgreementProcedures(); + void advanceOriginatorWindows(); + void completeTransfers(); + void updateContention(); + void scheduleAddbaResponseTimer(); + void actionTerminated(Packet *packet, bool acknowledged); + void invalidateActions(MacAddress peer, Tid tid, bool originator); + void retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset = false); + void beginTeardown(MacAddress peer, Tid tid, bool originator, int reasonCode); + void discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex = -1); + void firstDataDiscarded(const Ptr& header); + bool dispatchAgreementAction(const Ptr& header); + bool isProfileAllowed(MacAddress peer, bool originator) const; // Recipient - virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); + 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); @@ -176,16 +266,18 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; // IProcedureCallback - virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleInactivityTimer() override; std::string getFrameSequenceInfo() const; public: virtual ~Hcf(); + void resetPeer(MacAddress peer); // 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/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 2036bbbe680..19462280f07 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -29,7 +29,7 @@ module Hcf extends Module like IHcf { parameters: bool isBlockAckSupported = default(false); - string blockAckVariant = default("basic"); // "basic" selects historical compatibility. "compressed" is reserved until its exchange path exists. + string blockAckVariant = default("compressed"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. string rxModule; string txModule; diff --git a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc index 88395deef09..098aad01b1c 100644 --- a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc +++ b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc @@ -12,6 +12,24 @@ namespace inet { namespace ieee80211 { +void QoSDuplicateRemoval::setLastReceived(MacAddress peer, Tid tid, const SequenceNumberCyclic& sequence, int fragment) +{ + if (sequence.isSpecified()) + lastSeenSeqNumCache.insert_or_assign(Key(peer, tid), SequenceControlField(sequence.get(), fragment)); +} + +void QoSDuplicateRemoval::clearPeer(MacAddress peer) +{ + for (auto it = lastSeenSeqNumCache.begin(); it != lastSeenSeqNumCache.end();) { + if (it->first.first == peer) + it = lastSeenSeqNumCache.erase(it); + else + ++it; + } + lastSeenSharedSeqNumCache.erase(peer); + lastSeenTimePriorityManagementSeqNumCache.erase(peer); +} + bool QoSDuplicateRemoval::isDuplicate(const Ptr& header) { SequenceControlField seqVal(header->getSequenceNumber().get(), header->getFragmentNumber()); diff --git a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h index d1075215fbc..4a947d4a9f5 100644 --- a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h +++ b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h @@ -28,6 +28,8 @@ class INET_API QoSDuplicateRemoval : public IDuplicateRemoval Mac2SeqValMap lastSeenTimePriorityManagementSeqNumCache; public: + void setLastReceived(MacAddress peer, Tid tid, const SequenceNumberCyclic& sequence, int fragment); + void clearPeer(MacAddress peer); virtual bool isDuplicate(const Ptr& header) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 2f32f0207b9..ce397baca9e 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -26,8 +26,10 @@ Packet *BasicReassembly::addFragment(Packet *packet) // Frame is not fragmented if (!header->getMoreFragments() && header->getFragmentNumber() == 0) return packet; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) + // IEEE Std 802.11-2024, 10.4: dispatch Block Ack Actions only after complete MMPDU reassembly. + bool blockAckAction = dynamicPtrCast(header) || + dynamicPtrCast(header) || dynamicPtrCast(header); + if (dynamicPtrCast(header) && !blockAckAction) return packet; // find entry for this frame Key key; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc index d5a8ac4d2c3..7ab65c01b5f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc @@ -6,12 +6,49 @@ #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) +{ + if (rtsPrediction) + drop(rtsPrediction.get()); + rtsPrediction.reset(packet); + if (rtsPrediction) + take(rtsPrediction.get()); +} + FrameSequenceContext::FrameSequenceContext(MacAddress address, Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQoSContext, QoSContext *qosContext) : address(address), modeSet(modeSet), @@ -31,6 +68,11 @@ void FrameSequenceContext::removeLastStep() delete step; } +Packet *FrameSequenceContext::getDataFrameToTransmit() const +{ + return qosContext && qosContext->preparedData ? qosContext->preparedData->frame : inProgressFrames->getFrameToTransmit(); +} + simtime_t FrameSequenceContext::getIfs() const { return getNumSteps() == 0 ? 0 : modeSet->getSifsTime(); // TODO pifs diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h index ba9a3695257..b5026782257 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h @@ -8,13 +8,18 @@ #ifndef __INET_FRAMESEQUENCECONTEXT_H #define __INET_FRAMESEQUENCECONTEXT_H +#include +#include + #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" #include "inet/linklayer/ieee80211/mac/contract/IFrameSequence.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorAckPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/contract/IRtsPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/RtsProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" @@ -24,20 +29,52 @@ namespace inet { namespace ieee80211 { +class FrameSequenceContext; + +enum class DataPreparationResult { LEGACY, READY, DEFERRED }; + +/** Retains one actual mode decision until DATA submission or cancellation. */ +struct INET_API PreparedDataTransmission : public cObject +{ + std::unique_ptr preparation; + 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; + const physicallayer::IIeee80211Mode *rtsMode = nullptr; + const physicallayer::IIeee80211Mode *ackMode = nullptr; + const physicallayer::IIeee80211Mode *ctsMode = nullptr; + std::unique_ptr rtsPrediction; + OriginatorBlockAckAgreement *agreement = nullptr; // Identity only; validation obtains current state from its owner. + bool cancelled = false; + + ~PreparedDataTransmission() override; + void setRtsPrediction(Packet *packet); + void setEnvelope(Packet *packet); +}; + class INET_API QoSContext { public: - QoSContext(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler, TxopProcedure *txopProcedure) : + QoSContext(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler, TxopProcedure *txopProcedure, BlockAckVariant variant = BlockAckVariant::BASIC) : ackPolicy(ackPolicy), blockAckProcedure(blockAckProcedure), blockAckAgreementHandler(blockAckAgreementHandler), - txopProcedure(txopProcedure) + txopProcedure(txopProcedure), + blockAckVariant(variant) {} IOriginatorQoSAckPolicy *ackPolicy = nullptr; IOriginatorBlockAckProcedure *blockAckProcedure = nullptr; IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler = nullptr; TxopProcedure *txopProcedure = nullptr; + const BlockAckVariant blockAckVariant; + std::function prepareData; + std::shared_ptr preparedData; }; class INET_API NonQoSContext @@ -79,6 +116,8 @@ class INET_API FrameSequenceContext : public cObject virtual IFrameSequenceStep *getStepBeforeLast() const { return steps.size() > 1 ? steps[steps.size() - 2] : nullptr; } virtual InProgressFrames *getInProgressFrames() const { return inProgressFrames; } + // A reversible preview replaces destructive extraction only for the scoped QoS path. + Packet *getDataFrameToTransmit() const; virtual IRtsProcedure *getRtsProcedure() const { return rtsProcedure; } virtual IRtsPolicy *getRtsPolicy() const { return rtsPolicy; } diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index 66699fadeb0..84d3d9ac4b2 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -14,6 +14,25 @@ namespace inet { namespace ieee80211 { +void FrameSequenceHandler::interruptFrameSequence() +{ + ASSERT(isSequenceRunning()); + ASSERT(dynamic_cast(context->getLastStep())); + context->getLastStep()->setCompletion(IFrameSequenceStep::Completion::REJECTED); + 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. + 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 + abortFrameSequence(); +} + void FrameSequenceHandler::handleStartRxTimeout() { auto lastStep = context->getLastStep(); @@ -58,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"; @@ -157,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 de514c54c9d..d18fceb8528 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -29,12 +29,14 @@ 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; virtual void processResponse(Packet *frame) override; virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; + virtual void interruptFrameSequence() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } virtual ~FrameSequenceHandler(); 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/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index b12ce4d54f4..7bcd7d3be5c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -34,8 +34,12 @@ HcfFs::HcfFs() : int HcfFs::selectHcfSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) { - auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - return frameToTransmit->peekAtFront()->getReceiverAddress().isMulticast() ? 0 : 1; + auto qos = context->getQoSContext(); + auto frameToTransmit = qos->blockAckAgreementHandler ? context->getInProgressFrames()->peekFrameToTransmit() : + context->getInProgressFrames()->getFrameToTransmit(); + if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) + return 1; + return frameToTransmit && frameToTransmit->peekAtFront()->getReceiverAddress().isMulticast() ? 0 : 1; } int HcfFs::selectDataOrManagementSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) @@ -57,15 +61,29 @@ bool HcfFs::isSelfCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *con bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *context) { - bool hasFrameToTransmit = context->getInProgressFrames()->hasInProgressFrames(); + auto frames = context->getInProgressFrames(); + auto qos = context->getQoSContext(); + bool bar = qos->ackPolicy->isBlockAckReqNeeded(frames, qos->txopProcedure, qos->blockAckAgreementHandler); + qos->preparedData.reset(); + if (!bar && qos->prepareData) { + auto preparation = qos->prepareData(context); + if (preparation == DataPreparationResult::READY) + return true; + if (preparation == DataPreparationResult::DEFERRED) + return false; + } + bool hasFrameToTransmit = qos->blockAckAgreementHandler ? + bar || frames->peekFrameToTransmit() != nullptr : frames->hasInProgressFrames(); if (hasFrameToTransmit) { - auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& nextHeader = nextFrameToTransmit->peekAtFront(); + auto nextFrameToTransmit = qos->blockAckAgreementHandler ? frames->peekFrameToTransmit() : frames->getFrameToTransmit(); if (frameSequence->getCount() == 0) return true; - auto qos = context->getQoSContext(); auto txop = qos->txopProcedure; - if (nextHeader->getReceiverAddress().isMulticast() || txop->getRemaining() <= 0) + // Basic single protection reserves only prepared MPDUs. Unextracted DATA starts a new TXOP. + if (qos->blockAckAgreementHandler && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && + !bar && !frames->containsFrame(nextFrameToTransmit)) + return false; + if ((!bar && nextFrameToTransmit->peekAtFront()->getReceiverAddress().isMulticast()) || txop->getRemaining() <= 0) return false; if (txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION) { auto deadline = txop->getStart() + txop->getLimit(); @@ -77,8 +95,7 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte if (projectedStart >= deadline) return false; // BAR precedes DATA in TxOpFs and does not use RTS. - if (!qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), txop) && - context->getRtsPolicy()->isRtsNeeded(nextFrameToTransmit, nextHeader)) + if (!bar && context->getRtsPolicy()->isRtsNeeded(nextFrameToTransmit, nextFrameToTransmit->peekAtFront())) { // Integer legacy RTS airtime requires its start on the deadline's microsecond grid. if ((projectedStart - deadline).raw() % SimTime(1, SIMTIME_US).raw() != 0) @@ -92,7 +109,7 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte bool HcfFs::hasMoreTxOpsAndMulticast(RepeatingFs *frameSequence, FrameSequenceContext *context) { - return hasMoreTxOps(frameSequence, context) && context->getInProgressFrames()->getFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); + return hasMoreTxOps(frameSequence, context) && context->getDataFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 60c875bb16f..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) @@ -40,7 +70,7 @@ IFrameSequenceStep *RtsFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto dataOrMgmtPacket = context->getInProgressFrames()->getFrameToTransmit(); + auto dataOrMgmtPacket = context->getDataFrameToTransmit(); auto rtsFrame = context->getRtsProcedure()->buildRtsFrame(dataOrMgmtPacket->peekAtFront()); auto rtsPacket = new Packet("RTS"); rtsPacket->insertAtBack(rtsFrame); @@ -119,7 +149,7 @@ IFrameSequenceStep *DataFs::prepareStep(FrameSequenceContext *context) // RTS/CTS retains its selected DATA even if its lifetime expires during protection. // HCF rejects the next submission before Tx accepts that expired DATA. if (packet == nullptr) - packet = context->getInProgressFrames()->getFrameToTransmit(); + packet = context->getDataFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: @@ -150,7 +180,13 @@ IFrameSequenceStep *ManagementAckFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + Packet *packet = nullptr; + if (context->getQoSContext() && dynamic_cast(context->getLastStep())) { + if (auto rts = dynamic_cast(context->getStepBeforeLast())) + packet = const_cast(rts->getProtectedFrame()); + } + if (packet == nullptr) + packet = context->getInProgressFrames()->getFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: { @@ -195,7 +231,13 @@ IFrameSequenceStep *ManagementFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + Packet *packet = nullptr; + if (context->getQoSContext() && dynamic_cast(context->getLastStep())) { + if (auto rts = dynamic_cast(context->getStepBeforeLast())) + packet = const_cast(rts->getProtectedFrame()); + } + if (packet == nullptr) + packet = context->getInProgressFrames()->getFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: @@ -263,7 +305,7 @@ IFrameSequenceStep *RtsCtsFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + auto packet = context->getDataFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); auto rtsFrame = context->getRtsProcedure()->buildRtsFrame(dataOrMgmtHeader); auto rtsPacket = new Packet("RTS"); @@ -400,12 +442,15 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con { switch (step) { case 0: { - auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure); + auto qos = context->getQoSContext(); + auto blockAckReqParams = qos->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler); auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); - auto blockAckReq = context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); - auto blockAckPacket = new Packet("BasicBlockAckReq", blockAckReq); + bool compressed = qos->blockAckVariant == BlockAckVariant::COMPRESSED; + auto blockAckReq = compressed ? qos->blockAckProcedure->buildCompressedBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber) : + qos->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); + auto blockAckPacket = new Packet(compressed ? "CompressedBlockAckReq" : "BasicBlockAckReq", blockAckReq); blockAckPacket->insertAtBack(makeShared()); return new TransmitStep(blockAckPacket, context->getIfs(), true); } @@ -433,7 +478,27 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK; + auto requestStep = check_and_cast(context->getStep(firstStep)); + auto request = requestStep->getFrameToTransmit()->peekAtFront(); + if (auto basic = dynamicPtrCast(request)) { + auto response = dynamicPtrCast(receivedHeader); + bool valid = response && !response->getMultiTid() && !response->getCompressedBitmap() && + response->getFragmentNumber() == 0 && response->getStartingSequenceNumber().isSpecified() && + (response->getReserved() & 3) == 0 && response->getChunkLength() == LENGTH_BASIC_BLOCKACK - B(4) && + receivedPacket->getDataLength() == LENGTH_BASIC_BLOCKACK && response->getBlockAckBitmapArraySize() == 64; + if (valid) + for (int i = 0; i < 64; i++) + valid &= response->getBlockAckBitmap(i).getSize() == 16; + return valid && context->isForUs(receivedHeader) && + response->getReceiverAddress() == basic->getTransmitterAddress() && + response->getTransmitterAddress() == basic->getReceiverAddress() && + response->getTidInfo() == basic->getTidInfo(); + } + auto compressed = dynamicPtrCast(request); + auto response = dynamicPtrCast(receivedHeader); + // IEEE Std 802.11-2024, 9.3.1.8.2: this profile supports the 64-bit bitmap without dynamic fragmentation. + return 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 02420634437..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)) { } @@ -44,10 +46,13 @@ int TxOpFs::selectMgmtOrDataQap(AlternativesFs *frameSequence, FrameSequenceCont int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) { - auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& macHeader = frameToTransmit->peekAtFront(); - if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure)) + 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)) return 3; else { @@ -70,7 +75,10 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte bool TxOpFs::isRtsCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *context) { - auto protectedFrame = context->getInProgressFrames()->getFrameToTransmit(); + 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/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 4074ff0004c..2b77ca61c30 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -133,20 +133,25 @@ void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header) { EV_WARN << "Retry limit reached for " << *packet << ".\n"; - auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); - if (packet->getByteLength() >= rtsThreshold) { - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { - auto it = shortRetryCounter.find(id); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + dropFrame(packet, header); emit(retryLimitReachedSignal, packet); } +void NonQosRecoveryProcedure::dropFrame(Packet *packet, const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + if (packet->getByteLength() >= rtsThreshold) + longRetryCounter.erase(id); + else + shortRetryCounter.erase(id); +} + +void NonQosRecoveryProcedure::dropRtsProtectedFrame(const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + shortRetryCounter.erase(id); +} + // // The SRC for an MPDU of type Data or MMPDU and the SSRC shall be incremented every // time transmission of a MAC frame of length less than or equal to dot11RTSThreshold diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h index f738e56dde6..83c04085b1b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h @@ -65,6 +65,8 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); virtual void retryLimitReached(Packet *packet, const Ptr& header); + void dropFrame(Packet *packet, const Ptr& header); + void dropRtsProtectedFrame(const Ptr& header); virtual int getLongRetryLimit() { return longRetryLimit; } virtual int getShortRetryLimit() { return shortRetryLimit; } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 484f239cb09..b1aa01db57d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -31,73 +31,70 @@ bool OriginatorQosAckPolicy::isAckNeeded(const Ptr& h return !header->getReceiverAddress().isMulticast(); } -std::map> OriginatorQosAckPolicy::getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const +bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { - auto outstandingFrames = inProgressFrames->getOutstandingFrames(); - std::map> outstandingFramesPerReceiver; - for (auto frame : outstandingFrames) - outstandingFramesPerReceiver[frame->peekAtFront()->getReceiverAddress()].push_back(frame); - return outstandingFramesPerReceiver; + return std::get<2>(computeBlockAckReqParameters(frames, txop, agreements)) != -1; } -SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const std::vector& outstandingFrames) const +std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { - ASSERT(outstandingFrames.size() > 0); - auto startingSequenceNumber = outstandingFrames[0]->peekAtFront()->getSequenceNumber(); - for (size_t i = 1; i < outstandingFrames.size(); i++) { - auto seqNum = outstandingFrames[i]->peekAtFront()->getSequenceNumber(); - if (seqNum < startingSequenceNumber) - startingSequenceNumber = seqNum; + using Id = std::pair; + 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); + } } - return startingSequenceNumber; -} - -bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const -{ - // The Compressed Bitmap subfield of the BA Control field or BAR Control field shall be set to 1 in all - // BlockAck and BlockAckReq frames sent from one HT STA to another HT STA and shall be set to 0 otherwise. - return false; // non-HT STA -// for (auto frame : outstandingFrames) -// if (frame->getSequenceNumber() >= startingSequenceNumber && frame->getFragmentNumber() > 0) -// return false; -// return true; -} - -// FIXME -bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const -{ - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) - return true; + std::map> groups; + for (auto frame : frames->getOutstandingFrames()) { + auto header = frame->peekAtFront(); + auto agreement = agreements ? agreements->getAgreement(header->getReceiverAddress(), header->getTid()) : nullptr; + if (agreement && agreement->getIsAddbaResponseReceived() && agreement->containsSequence(header->getSequenceNumber())) + groups[{header->getReceiverAddress(), header->getTid()}].push_back(frame); } - return false; -} - -// FIXME -std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const -{ - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) { - auto largestOutstandingFrames = outstandingFramesPerReceiver.begin(); - for (auto it = outstandingFramesPerReceiver.begin(); it != outstandingFramesPerReceiver.end(); it++) { - if (it->second.size() > largestOutstandingFrames->second.size()) - largestOutstandingFrames = it; + if (groups.empty()) + return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(0), Tid(-1)); + auto candidate = frames->peekFrameToTransmit(); + auto data = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + auto result = std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(0), Tid(-1)); + simtime_t oldestArrival = SIMTIME_MAX; + for (const auto& entry : groups) { + const auto& id = entry.first; + auto agreement = agreements->getAgreement(id.first, id.second); + // INET BAR policy defers a short group only for DATA within that group's window. + if ((int)entry.second.size() < blockAckReqThreshold && data && + data->getReceiverAddress() == id.first && data->getTid() == id.second && + agreement->containsSequence(frames->getCandidateSequenceNumber(candidate))) + continue; + auto start = agreement->getStartingSequenceNumber(); + int oldestOffset = -1; + simtime_t arrival = SIMTIME_MAX; + for (auto frame : entry.second) { + auto sequence = frame->peekAtFront()->getSequenceNumber(); + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(start, sequence); + if (oldestOffset == -1 || offset < oldestOffset) { + oldestOffset = offset; + arrival = frame->getArrivalTime(); } - MacAddress receiverAddress = largestOutstandingFrames->first; - SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); - Tid tid = largestOutstandingFrames->second.at(0)->peekAtFront()->getTid(); - return std::make_tuple(receiverAddress, startingSequenceNumber, tid); + else if (offset == oldestOffset) + arrival = std::min(arrival, frame->getArrivalTime()); + } + // Map order supplies the peer/TID tie break. + if (std::get<2>(result) == -1 || arrival < oldestArrival) { + oldestArrival = arrival; + result = std::make_tuple(id.first, start + oldestOffset, id.second); } } - return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(), -1); + return result; } AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const { if (agreement == nullptr) return AckPolicy::NORMAL_ACK; + if (agreement->getIsAddbaResponseReceived() && header->getAMsduPresent() && !agreement->isAMsduPermitted()) + throw cRuntimeError("A-MSDU is not permitted by both ADDBA flags"); if (agreement->getIsAddbaResponseReceived() && isBlockAckPolicyEligibleFrame(packet, header)) { if (checkAgreementPolicy(header, agreement)) return AckPolicy::BLOCK_ACK; @@ -115,10 +112,9 @@ bool OriginatorQosAckPolicy::isBlockAckPolicyEligibleFrame(Packet *packet, const bool OriginatorQosAckPolicy::checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const { - bool bufferFull = agreement->getBufferSize() == agreement->getNumSentBaPolicyFrames(); - bool aMsduOk = agreement->getIsAMsduSupported() || !header->getAMsduPresent(); + bool aMsduOk = agreement->isAMsduPermitted() || !header->getAMsduPresent(); // TODO bool baPolicy = agreement->getIsDelayedBlockAckPolicySupported() || !frame->getAckPolicy(); - return !bufferFull && aMsduOk && (header->getSequenceNumber() >= agreement->getStartingSequenceNumber()); // TODO && baPolicy + return aMsduOk && agreement->containsSequence(header->getSequenceNumber()); } // @@ -138,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 bcd9f1b2ae7..405d0269169 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -25,22 +25,22 @@ 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; } virtual void initialize(int stage) override; virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; - virtual std::map> getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const; - virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; - virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; public: + 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; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const override; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index ef82c225c78..4d4404e20e1 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -9,17 +9,41 @@ #include +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" #include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/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" +#include "inet/queueing/contract/ISelectivePacketCollection.h" namespace inet { namespace ieee80211 { Define_Module(OriginatorQosMacDataService); +PreparedQosData::~PreparedQosData() +{ + if (packet) + drop(packet.get()); +} + +void PreparedQosData::setPacket(Packet *value) +{ + if (packet) + drop(packet.get()); + packet.reset(value); + if (packet) + take(packet.get()); +} + +Packet *PreparedQosData::releasePacket() +{ + drop(packet.get()); + return packet.release(); +} + void OriginatorQosMacDataService::initialize() { aMsduAggregationPolicy = dynamic_cast(getSubmodule("msduAggregationPolicy")); @@ -33,9 +57,255 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } -Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue) +std::unique_ptr OriginatorQosMacDataService::prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) +{ + Enter_Method("prepareFrame"); + if (expireFrames) + expireFrames(); + auto selected = peekEligibleFrame(queue); + if (!selected) + return nullptr; + auto selectedHeader = dynamicPtrCast(selected->peekAtFront()); + if (!selectedHeader || selectedHeader->getType() != ST_DATA_WITH_QOS) + return nullptr; + auto result = std::make_unique(); + result->queue = queue; + std::vector sources{selected}; + auto module = dynamic_cast(queue); + auto head = queue->canPullPacket(module ? module->gate("out") : nullptr); + if (allowAggregation && selected == head && aMsduAggregationPolicy && (!aMsduPermitted || aMsduPermitted(selected))) { + std::unique_ptr> proposed(aMsduAggregationPolicy->computeAggregateFrames(queue)); + bool valid = proposed && !proposed->empty() && proposed->front() == selected; + std::set unique; + if (proposed) { + for (auto frame : *proposed) { + bool present = false; + for (int i = 0; i < queue->getNumPackets(); i++) + present |= queue->getPacket(i) == frame; + valid &= present && unique.insert(frame).second && + (!extractionEligible || extractionEligible(frame)) && (!aMsduPermitted || aMsduPermitted(frame)); + } + } + if (valid) { + sources = *proposed; + result->aggregate = true; + } + } + for (auto source : sources) + result->sourceIds.push_back(source->getId()); + if (result->aggregate) { + std::vector duplicates; + for (auto source : sources) + duplicates.push_back(source->dup()); + result->setPacket(aMsduAggregation->aggregateFrames(&duplicates)); + } + else + result->setPacket(selected->dup()); + // The preview carries the next value without consuming the sequence counter. + auto header = result->packet->removeAtFront(); + header->setSequenceNumber(getNextSequenceNumber(header->getReceiverAddress(), header->getTid())); + result->packet->insertAtFront(header); + return result; +} + +bool OriginatorQosMacDataService::isPreparationValid(const PreparedQosData& preparation, simtime_t start) const +{ + if (!preparation.packet) + 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() != sequence) + return false; + for (auto id : preparation.sourceIds) { + Packet *source = nullptr; + for (int i = 0; i < preparation.queue->getNumPackets(); i++) { + auto candidate = preparation.queue->getPacket(i); + if (candidate->getId() == id) { + source = candidate; + break; + } + } + if (!source || (lifetimeHandler && lifetimeHandler->isExpired(source, start)) || + (extractionEligible && !extractionEligible(source)) || + (preparation.aggregate && aMsduPermitted && !aMsduPermitted(source))) + return false; + } + return true; +} + +Packet *OriginatorQosMacDataService::commitPreparedFrame(PreparedQosData& preparation) +{ + Enter_Method("commitPreparedFrame"); + if (!isPreparationValid(preparation, simTime())) + return nullptr; + return commitValidatedFrame(preparation); +} + +Packet *OriginatorQosMacDataService::commitValidatedFrame(PreparedQosData& preparation) +{ + std::vector sources; + std::vector trackedIds; + simtime_t deadline = SIMTIME_MAX; + for (auto id : preparation.sourceIds) { + for (int i = 0; i < preparation.queue->getNumPackets(); i++) { + auto source = preparation.queue->getPacket(i); + if (source->getId() == id) { + sources.push_back(source); + if (lifetimeHandler && lifetimeHandler->getDeadline(source) != SIMTIME_MAX) { + trackedIds.push_back(id); + deadline = lifetimeHandler->getDeadline(source); + } + break; + } + } + } + for (auto source : sources) { + if (preparation.aggregate) + preparation.queue->removePacket(source); + else { + auto module = dynamic_cast(preparation.queue); + auto next = preparation.queue->canPullPacket(module ? module->gate("out") : nullptr); + auto extracted = source == next ? preparation.queue->dequeuePacket() : + check_and_cast(preparation.queue)->extractPacket(source); + ASSERT(extracted == source); + // The preview predates extraction. Preserve the queue's completed service metadata. + preparation.packet->copyRegionTags(*source, source->getFrontOffset(), + preparation.packet->getFrontOffset(), source->getDataLength()); + } + take(source); + } + if (lifetimeHandler) + lifetimeHandler->prepareFrameReplacement(trackedIds); + for (auto source : sources) + delete source; + auto packet = preparation.releasePacket(); + take(packet); + if (lifetimeHandler) { + if (preparation.aggregate) { + auto edca = check_and_cast(getParentModule()->getSubmodule("edca")); + lifetimeHandler->registerFrame(packet, edca->classifyFrame(packet->peekAtFront())); + } + else if (deadline != SIMTIME_MAX) + lifetimeHandler->registerFragments({packet}, deadline); + } + auto header = packet->removeAtFront(); + assignSequenceNumber(header); + packet->insertAtFront(header); + packet->addTagIfAbsent()->setAllocationIndex(nextDataAllocationIndex++); + if (preparation.aggregate) + emit(packetAggregatedSignal, packet); + return packet; +} + +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); + if (subframes) { + bool valid = !subframes->empty() && subframes->front() == selected; + std::set unique; + for (auto frame : *subframes) { + bool present = false; + for (int i = 0; i < pendingQueue->getNumPackets(); i++) { + if (pendingQueue->getPacket(i) == frame) { + present = true; + break; + } + } + valid &= present && unique.insert(frame).second && + (!extractionEligible || extractionEligible(frame)) && + (!aMsduPermitted || aMsduPermitted(frame)); + } + if (!valid) { + delete subframes; + return nullptr; + } + } if (subframes) { std::vector sourceIds; AccessCategory ac = AC_BE; @@ -105,16 +375,20 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu Enter_Method("extractFramesToTransmit"); if (expireFrames) expireFrames(); - if (pendingQueue->isEmpty()) + auto selected = peekEligibleFrame(pendingQueue); + if (selected == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); Packet *packet = nullptr; - if (aMsduAggregationPolicy) - packet = aMsduAggregateIfNeeded(pendingQueue); + auto module = dynamic_cast(pendingQueue); + auto next = pendingQueue->canPullPacket(module ? module->gate("out") : nullptr); + if (selected == next && aMsduAggregationPolicy && (!aMsduPermitted || aMsduPermitted(selected))) + packet = aMsduAggregateIfNeeded(pendingQueue, selected); if (!packet) { - packet = pendingQueue->dequeuePacket(); + packet = selected == next ? pendingQueue->dequeuePacket() : + check_and_cast(pendingQueue)->extractPacket(selected); take(packet); } // PS Defer Queueing @@ -123,10 +397,12 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu assignSequenceNumber(header); packet->insertAtFront(header); } + if (dynamicPtrCast(packet->peekAtFront())) + packet->addTagIfAbsent()->setAllocationIndex(nextDataAllocationIndex++); // if (msduIntegrityAndProtection) // frame = protectMsduIfNeeded(frame); std::vector *fragments = nullptr; - if (fragmentationPolicy) + if (fragmentationPolicy && (!fragmentationPermitted || fragmentationPermitted(packet))) fragments = fragmentIfNeeded(packet); if (!fragments) fragments = new std::vector({ packet }); @@ -143,6 +419,35 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu } } +Packet *OriginatorQosMacDataService::peekEligibleFrame(queueing::IPacketQueue *queue) const +{ + if (queue->isEmpty()) + return nullptr; + auto predicate = [this](Packet *packet) { return !extractionEligible || extractionEligible(packet); }; + auto module = dynamic_cast(queue); + auto packet = queue->canPullPacket(module ? module->gate("out") : nullptr); + if (packet && predicate(packet)) + return packet; + if (auto selected = dynamic_cast(queue)) { + try { + return selected->peekEligiblePacket(predicate); + } + catch (const queueing::UnsupportedPacketSelection&) { + // Ordinary service remains valid when this composition cannot bypass a deferred head. + } + } + for (int i = 0; i < queue->getNumPackets(); i++) { + if (predicate(queue->getPacket(i))) + throw cRuntimeError("Deferred DATA hides an eligible frame: queue requires ISelectivePacketCollection"); + } + return nullptr; +} + +SequenceNumberCyclic OriginatorQosMacDataService::getNextSequenceNumber(MacAddress peer, Tid tid) const +{ + return check_and_cast(sequenceNumberAssignment)->getNextSequenceNumber(peer, tid); +} + OriginatorQosMacDataService::~OriginatorQosMacDataService() { delete aMsduAggregation; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 9e377d36427..4b6941521ee 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -16,14 +16,14 @@ #include "inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IMsduAggregation.h" #include "inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h" -#include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/contract/ISequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mac/lifetime/EdcaTransmitLifetimeHandler.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, public SimpleModule +class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService, public SimpleModule { protected: // Figure 5-1—MAC data plane architecture @@ -41,16 +41,39 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p IMpduAggregation *aMpduAggregation = nullptr; EdcaTransmitLifetimeHandler *lifetimeHandler = nullptr; // Borrowed from EDCA. std::function expireFrames; + std::function extractionEligible; + std::function aMsduPermitted; + std::function fragmentationPermitted; + int64_t nextDataAllocationIndex = 0; protected: virtual void initialize() override; virtual void assignSequenceNumber(const Ptr& header); virtual std::vector *fragmentIfNeeded(Packet *frame); - virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue); + virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected); virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); + bool isPreparationValidForSequence(const PreparedQosData& preparation, simtime_t start, SequenceNumberCyclic sequence) const; + Packet *commitValidatedFrame(PreparedQosData& preparation); public: + void setFragmentationPermission(std::function permission) { fragmentationPermitted = permission; } + void setEligibility(std::function eligible, std::function permission) { + extractionEligible = eligible; + aMsduPermitted = permission; + } + 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; virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 2fa02655743..f01e08414ca 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -109,6 +109,33 @@ void QosAckHandler::dropFrames(std::set>> QosAckHandler::processReceivedCompressedBlockAck( + const Ptr& blockAck, const OriginatorBlockAckAgreement *agreement) +{ + std::set acked; + if (!agreement) + return acked; + auto start = blockAck->getStartingSequenceNumber(); + const auto& bitmap = blockAck->getBlockAckBitmap(); + // IEEE Std 802.11-2024, 10.25.6.8: positive bits cover transmitted frames throughout the active window. + for (auto& entry : ackStatuses) { + if (entry.first.first != blockAck->getTransmitterAddress() || entry.first.second.first != blockAck->getTidInfo()) + continue; + auto sequence = SequenceNumberCyclic(entry.first.second.second.getSequenceNumber()); + auto& status = entry.second; + bool transmitted = status != Status::FRAME_NOT_YET_TRANSMITTED && status != Status::NO_ACK_REQUIRED; + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(start, sequence); + if (transmitted && agreement->containsSequence(sequence) && entry.first.second.second.getFragmentNumber() == 0 && + offset < 64 && bitmap.getBit(offset)) { + status = Status::BLOCK_ACK_ARRIVED_ACKED; + acked.insert(entry.first); + } + else if (status == Status::WAITING_FOR_BLOCK_ACK) + status = Status::BLOCK_ACK_ARRIVED_UNACKED; + } + return acked; +} + std::set>> QosAckHandler::processReceivedBlockAck(const Ptr& blockAck) { MacAddress receiverAddr = blockAck->getTransmitterAddress(); @@ -147,6 +174,13 @@ std::set>> QosAckHand else { throw cRuntimeError("Multi-TID BlockReq is unimplemented"); } + auto basic = dynamicPtrCast(blockAck); + auto compressed = dynamicPtrCast(blockAck); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + for (auto& entry : ackStatuses) { + if (entry.first.first == receiverAddr && entry.first.second.first == tid && entry.second == Status::WAITING_FOR_BLOCK_ACK) + entry.second = Status::BLOCK_ACK_ARRIVED_UNACKED; + } return ackedFrames; } @@ -195,23 +229,48 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const PtrgetReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()))] = Status::WAITING_FOR_NORMAL_ACK; } -void QosAckHandler::processTransmittedBlockAckReq(const Ptr& blockAckReq) +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) { + if (ackStatus.first.first != blockAckReq->getReceiverAddress()) + continue; auto tid = ackStatus.first.second.first; auto seqCtrlField = ackStatus.first.second.second; + auto sequence = SequenceNumberCyclic(seqCtrlField.getSequenceNumber()); + if (agreement && !agreement->containsSequence(sequence)) + continue; auto& status = ackStatus.second; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { if (basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && OriginatorBlockAckAgreement::isInBitmap(sequence, startingSeqNum)) status = Status::WAITING_FOR_BLOCK_ACK; } } else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { if (compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum && seqCtrlField.getFragmentNumber() == 0) // TODO ASSERT(seqCtrlField.second == 0)? + 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 7c1e0bce61c..fb5c7d29e7e 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -13,6 +13,7 @@ #include "inet/linklayer/ieee80211/mac/common/SequenceControlField.h" #include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -56,11 +57,15 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void processReceivedAck(const Ptr& ack, const Ptr& ackedHeader); virtual std::set>> processReceivedBlockAck(const Ptr& blockAck); + std::set>> processReceivedCompressedBlockAck( + const Ptr& blockAck, const OriginatorBlockAckAgreement *agreement); virtual void processFailedBlockAckReq(const Ptr& blockAckReq); virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); - virtual void processTransmittedBlockAckReq(const Ptr& blockAckReq); + 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); virtual void dropFrames(std::set>> seqAndFragNums); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc index e0b177a94b2..75bc71ca2a1 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc @@ -110,6 +110,25 @@ void QosRecoveryProcedure::ctsFrameReceived() void QosRecoveryProcedure::blockAckFrameReceived() { resetStationSrc(); + resetContentionWindow(); +} + +void QosRecoveryProcedure::controlFrameTransmissionFailed() +{ + // IEEE Std 802.11-2024, 10.23.2.2: control failures update EDCA backoff without DATA retry feedback. + if (stationShortRetryCounter < shortRetryLimit) { + stationShortRetryCounter++; + incrementContentionWindow(); + } + else { + resetStationSrc(); + resetContentionWindow(); + } +} + +void QosRecoveryProcedure::implicitBlockAckMemberFailed(Packet *packet, const Ptr& header) +{ + incrementCounter(header, packet->getByteLength() >= rtsThreshold ? longRetryCounter : shortRetryCounter); } // diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h index 69435073efc..62c9b663066 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h @@ -61,11 +61,13 @@ 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(); virtual void ackFrameReceived(Packet *packet, const Ptr& ackedHeader); virtual void blockAckFrameReceived(); + virtual void controlFrameTransmissionFailed(); virtual void rtsFrameTransmissionFailed(const Ptr& protectedHeader); virtual void dataFrameTransmissionFailed(Packet *packet, const Ptr& failedHeader); diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc index fbd4606601a..e2f2b63c464 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc @@ -67,8 +67,9 @@ simtime_t SingleProtectionMechanism::computeCtsDurationField(const Ptr& blockAckReq) { // TODO ACK or BlockAck frame, as applicable - if (dynamicPtrCast(blockAckReq)) { - simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + if (dynamicPtrCast(blockAckReq) || dynamicPtrCast(blockAckReq)) { + simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getDuration( + dynamicPtrCast(blockAckReq) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); simtime_t blockAckReqDurationPerId = blockAckFrameDuration + modeSet->getSifsTime(); return blockAckReqDurationPerId; } diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 76838835cce..dd63aa884cb 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" + namespace inet { namespace ieee80211 { @@ -63,9 +65,19 @@ bool InProgressFrames::containsFrame(const Packet *packet) const bool InProgressFrames::isEligibleToTransmit(Packet *packet) { return (!lifetimeHandler || !lifetimeHandler->isExpired(packet)) && + (!transmissionEligible || transmissionEligible(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()) { @@ -84,6 +96,16 @@ void InProgressFrames::ensureHasFrameToTransmit() } } +void InProgressFrames::adoptPreparedFrame(Packet *packet) +{ + Enter_Method("adoptPreparedFrame"); + take(packet); + ackHandler->frameGotInProgress(packet->peekAtFront()); + inProgressFrames.push_back(packet); + packet->setArrivalTime(simTime()); + emit(packetEnqueuedSignal, packet); +} + Packet *InProgressFrames::getFrameToTransmit() { ensureHasFrameToTransmit(); @@ -94,9 +116,26 @@ Packet *InProgressFrames::getFrameToTransmit() return nullptr; } -Packet *InProgressFrames::getPendingFrameFor(Packet *frame) +Packet *InProgressFrames::peekFrameToTransmit() +{ + for (auto frame : inProgressFrames) { + if (isEligibleToTransmit(frame)) + return frame; + } + return check_and_cast(dataService)->peekEligibleFrame(pendingQueue); +} + +SequenceNumberCyclic InProgressFrames::getCandidateSequenceNumber(Packet *packet) const +{ + auto header = packet->peekAtFront(); + if (containsFrame(packet)) + return header->getSequenceNumber(); + return check_and_cast(dataService)->getNextSequenceNumber(header->getReceiverAddress(), header->getTid()); +} + +Packet *InProgressFrames::getPendingFrameFor(Packet *frame, bool extract) { - auto frameToTransmit = getFrameToTransmit(); + auto frameToTransmit = extract ? getFrameToTransmit() : peekFrameToTransmit(); if (dynamicPtrCast(frame->peekAtFront())) return frameToTransmit; else { @@ -104,6 +143,8 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) if (isEligibleToTransmit(frame) && frameToTransmit != frame) return frame; } + if (!extract) + return check_and_cast(dataService)->peekEligibleFrame(pendingQueue); auto frames = dataService->extractFramesToTransmit(pendingQueue); if (frames) { auto firstFrame = (*frames)[0]; @@ -157,7 +198,8 @@ std::vector InProgressFrames::getOutstandingFrames() { std::vector outstandingFrames; for (auto frame : inProgressFrames) { - if (ackHandler->isOutstandingFrame(frame->peekAtFront())) + if ((!lifetimeHandler || !lifetimeHandler->isExpired(frame)) && + ackHandler->isOutstandingFrame(frame->peekAtFront())) outstandingFrames.push_back(frame); } return outstandingFrames; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index ca7175ee2d5..bd7264fffd3 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -32,6 +32,7 @@ class INET_API InProgressFrames : public SimpleModule EdcaTransmitLifetimeHandler *lifetimeHandler = nullptr; // Borrowed from EDCA; null for DCF. std::vector inProgressFrames; std::vector droppedFrames; + std::function transmissionEligible; protected: virtual void initialize(int stage) override; @@ -43,19 +44,26 @@ class INET_API InProgressFrames : public SimpleModule public: virtual ~InProgressFrames(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler) { lifetimeHandler = handler; } + void setTransmissionEligibility(std::function predicate) { transmissionEligible = predicate; } + bool hasEligibleInProgressFrames() { return hasEligibleFrameToTransmit(); } virtual std::string str() const override; virtual void forEachChild(cVisitor *v) override; virtual int getLength() const { return inProgressFrames.size(); } virtual Packet *getFrames(int i) const { return inProgressFrames[i]; } bool containsFrame(const Packet *packet) const; + // Takes ownership after the data service commits a reversible preparation. + void adoptPreparedFrame(Packet *packet); virtual Packet *getFrameToTransmit(); - virtual Packet *getPendingFrameFor(Packet *frame); + virtual Packet *peekFrameToTransmit(); + SequenceNumberCyclic getCandidateSequenceNumber(Packet *packet) const; + virtual Packet *getPendingFrameFor(Packet *frame, bool extract = true); virtual void dropFrame(Packet *packet); virtual void dropFrames(std::set>> seqAndFragNums); virtual bool hasInProgressFrames() { ensureHasFrameToTransmit(); return hasEligibleFrameToTransmit(); } virtual std::vector getOutstandingFrames(); + std::vector getEligibleFrames(); virtual void clearDroppedFrames(); }; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 6076a48172d..bd93bd3c52d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -131,6 +131,17 @@ static const IIeee80211Mode *selectCompatibleMode(const Ieee80211ModeSet *modeSe } // namespace +bool isPeerCompatibleMode(const Ieee80211ModeSet *modeSet, + const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode) +{ + if (!modeSet || !mode || !modeSet->containsMode(mode)) + return false; + if (mode->getHtMcsIndex() < 0) + return true; + return peerHtState && peerHtState->valid && + isCompatibleHtMode(mode, peerHtState->negotiatedCapabilities.localTxPeerRx, peerHtState->negotiatedCapabilities.operation); +} + const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode, const MacAddress& peerAddress) { diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index 5197aa38891..e512fbbdb28 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -15,6 +15,10 @@ namespace inet { namespace ieee80211 { +/** Checks the retained transmit mode without a new rate-control decision. */ +INET_API bool isPeerCompatibleMode(const physicallayer::Ieee80211ModeSet *modeSet, + const Ieee80211Mib::PeerHtState *peerHtState, const physicallayer::IIeee80211Mode *mode); + /** * Selects a mode compatible with local transmission to the peer. * Non-HT modes remain unchanged. Absent peer state selects a legacy mode diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 82db4bbd06c..283bbe42d6b 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -144,10 +144,94 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet auto mode = responseBlockAckFrameMode ? responseBlockAckFrameMode : getMode(packet, blockAckReq); return getPeerCompatibleResponseMode(blockAckReq, mode, role); } + else if (dynamicPtrCast(blockAckReq)) { + 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) +{ + const Ieee80211RateSet *local = mib ? &mib->getLocalRateSet().operational : nullptr; + const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; + auto peerState = mib ? mib->findPeerRateSet(blockAckReq->getReceiverAddress()) : nullptr; + const Ieee80211RateSet *peer = peerState ? &peerState->operational : nullptr; + auto eligible = [&](const IIeee80211Mode *candidate) { + auto modulation = candidate->getModulationClass(); + bool legacy = modulation == IIeee80211Mode::ModulationClass::DSSS_HRDSSS || + modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM; + auto rate = candidate->getDataMode()->getNetBitrate(); + if (!legacy || (local && local->known && !local->legacyRates.count(rate))) + return false; + // IEEE Std 802.11-2024, 10.6.6.2 and 10.6.6.4: initial BAR and later BAR use different sets. + if (!startsTxop && peer && peer->known) + return peer->legacyRates.count(rate) != 0; + if (basic && basic->known && !basic->legacyRates.empty()) + return basic->legacyRates.count(rate) != 0; + // INET uses mandatory rates when BSS rate state is unknown. + return modeSet->getIsMandatory(candidate); + }; + if (controlFrameMode) { + if (!eligible(controlFrameMode)) + throw cRuntimeError("controlFrameBitrate is not a legal non-HT Compressed BAR rate for this TXOP position"); + return controlFrameMode; + } + const IIeee80211Mode *selected = nullptr; + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto candidate = modeSet->getMode(i); + if (eligible(candidate) && (!selected || candidate->getDataMode()->getNetBitrate() > selected->getDataMode()->getNetBitrate())) + selected = candidate; + } + if (!selected) + throw cRuntimeError("Compressed BAR has no legal local non-HT mode"); + return selected; +} + const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 5bc4647a89e..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 @@ -84,6 +85,8 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame, ResponseQueryRole role) override; virtual const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& dataOrMgmtHeader, ResponseQueryRole role) override; virtual const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq, ResponseQueryRole role) override; + virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) override; virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index cb9b112bd42..4712d009d0c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -61,7 +61,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { - if (dynamicPtrCast(blockAckReq)) { + if (dynamicPtrCast(blockAckReq) || dynamicPtrCast(blockAckReq)) { return agreement != nullptr; // TODO The Basic BlockAckReq frame shall be discarded if all MSDUs referenced by this // frame have been discarded from the transmit buffer due to expiry of their lifetime limit. @@ -94,6 +94,13 @@ simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packe return basicBlockAckReq->getDurationField() - modeSet->getSifsTime() - computeBasicBlockAckDuration(packet, basicBlockAckReq); } +simtime_t RecipientQosAckPolicy::computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const +{ + // IEEE Std 802.11-2024, 9.2.5.6: subtract the actual response airtime and SIFS. + auto airtime = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::RESPOND)->getDuration(LENGTH_COMPRESSED_BLOCKACK); + return std::max(SIMTIME_ZERO, blockAckReq->getDurationField() - modeSet->getSifsTime() - airtime); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 9f4ce456784..0012ef77b55 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -34,6 +34,7 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const override; virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const override; + virtual simtime_t computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 95fac972e3a..9d3d2edfe56 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -53,10 +53,18 @@ Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) return nullptr; } -std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) +std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe, const BlockAckReceiveContext *context) { Enter_Method("dataFrameReceived"); take(dataPacket); + if (blockAckAgreementHandler) { + auto agreement = blockAckAgreementHandler->getAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); + if (agreement) { + agreement->recordReceived(dataHeader); + if (agreement->getVariant() == BlockAckVariant::COMPRESSED) + agreement->getBlockAckRecord()->compressedFrameReceived(dataHeader); + } + } // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; @@ -72,7 +80,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader, interframe, context); } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -116,33 +124,35 @@ std::vector RecipientQosMacDataService::managementFrameReceived(Packet Enter_Method("managementFrameReceived"); take(mgmtPacket); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption - if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) + if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { + delete mgmtPacket; return std::vector(); + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting - if (dynamicPtrCast(mgmtHeader)) { - delete mgmtPacket; + if (mgmtPacket == nullptr) return std::vector(); - } - else - return std::vector({ mgmtPacket }); + return std::vector({ mgmtPacket }); } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReleasedFrames frames; if (blockAckReordering) { - Tid tid = blockAckReq->getTidInfo(); + auto basic = dynamicPtrCast(blockAckReq); + auto compressed = dynamicPtrCast(blockAckReq); + if (!basic && !compressed) + return {}; + Tid tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) + if (agreement) { frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); + } else return std::vector(); } @@ -193,6 +203,49 @@ RecipientQosMacDataService::~RecipientQosMacDataService() delete blockAckReordering; } +std::vector RecipientQosMacDataService::retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) +{ + Enter_Method("retireAgreement"); + auto peer = agreement->getBlockAckRecord()->getOriginatorAddress(); + auto tid = agreement->getBlockAckRecord()->getTid(); + auto duplicate = check_and_cast(duplicateRemoval); + // IEEE Std 802.11-2024, 11.5.3.5: ordinary teardown retains the highest received sequence number. + if (!associationReset) + duplicate->setLastReceived(peer, tid, agreement->getHighestReceivedSequence(), agreement->getHighestReceivedFragment()); + std::vector discarded; + auto released = blockAckReordering->detachAgreement(peer, tid, !associationReset, discarded); + for (auto packet : discarded) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + std::vector complete; + for (const auto& unit : released) { + auto frame = defragment(unit.second); + if (!frame) + continue; + if (frame->peekAtFront()->getAMsduPresent()) { + emit(packetDeaggregatedSignal, frame); + auto subframes = aMsduDeaggregation->deaggregateFrame(frame); + complete.insert(complete.end(), subframes->begin(), subframes->end()); + delete subframes; + } + else + complete.push_back(frame); + } + return complete; +} + +void RecipientQosMacDataService::clearPeer(MacAddress peer) +{ + Enter_Method("clearPeer"); + blockAckReordering->clearPeer(peer); + check_and_cast(duplicateRemoval)->clearPeer(peer); + for (int tid = -1; tid < 16; tid++) + basicReassembly->purge(peer, tid, 0, 4095); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index e4b8ee28865..4b07f244a07 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -46,7 +46,9 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, virtual Packet *defragment(Packet *mgmtFragment); public: - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; + 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, 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/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc index c0d2085cf3d..8f5f1340436 100644 --- a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc +++ b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc @@ -10,6 +10,12 @@ namespace inet { namespace ieee80211 { +SequenceNumberCyclic QoSSequenceNumberAssignment::getNextSequenceNumber(MacAddress peer, Tid tid) const +{ + auto it = lastSentSeqNums.find({peer, tid}); + return it == lastSentSeqNums.end() ? SequenceNumberCyclic(0) : it->second + 1; +} + QoSSequenceNumberAssignment::CacheType QoSSequenceNumberAssignment::getCacheType(const Ptr& header, bool incoming) { bool isTimePriorityFrame = false; // TODO diff --git a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h index 07928c860b5..81cb584ace8 100644 --- a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h +++ b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h @@ -36,6 +36,7 @@ class INET_API QoSSequenceNumberAssignment : public ISequenceNumberAssignment virtual CacheType getCacheType(const Ptr& header, bool incoming); public: + SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; virtual void assignSequenceNumber(const Ptr& header) override; }; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h index 6a851b507e4..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; @@ -101,6 +105,7 @@ inline Ieee80211HtOperationElement makeHtOperationElement(const physicallayer::I element.secondaryChannelOffset = operation.secondaryChannelOffset; element.staChannelWidth40Mhz = operation.operatingChannelWidth == MHz(40); element.protectionMode = static_cast(operation.protectionMode); + element.rifsMode = operation.rifsMode; for (int i = 0; i < 77; i++) element.basicMcsSupported[i] = operation.basicMcsSupported[i]; return element; @@ -119,6 +124,7 @@ inline Ieee80211HtOperation makeHtOperation(const physicallayer::IIeee80211Band operation.secondaryChannelOffset = element.secondaryChannelOffset; operation.operatingChannelWidth = element.staChannelWidth40Mhz ? MHz(40) : MHz(20); operation.protectionMode = static_cast(element.protectionMode); + operation.rifsMode = element.rifsMode; for (int i = 0; i < 77; i++) operation.basicMcsSupported[i] = element.basicMcsSupported[i]; return operation; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index b4439163dc0..3442658d920 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -130,6 +130,8 @@ void Ieee80211MgmtAp::frameTransmissionFinished(const Packet *responseFrame, Fra if (status == FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED) { if (sta->second.pendingAssociationSuccessful) { bool wasAssociated = mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED; + if (wasAssociated) + resetPeerAgreements(address); mib->commitAssociationId(address); mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; auto peerRateSet = sta->second.pendingPeerRateSet; @@ -155,6 +157,7 @@ void Ieee80211MgmtAp::frameTransmissionFinished(const Packet *responseFrame, Fra sendAssocNotification(address); } else if (mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) { + resetPeerAgreements(address); // This model does not implement negotiated management-frame protection. // IEEE Std 802.11-2024, 11.3.5.3(p) for association and 11.3.5.5(n) // for same-AP reassociation therefore require the existing association @@ -254,6 +257,8 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + if (wasAssociated) + resetPeerAgreements(sta->address); mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; if (wasAssociated) @@ -294,6 +299,8 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + if (wasAssociated) + resetPeerAgreements(sta->address); mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; // TODO only when ACK of this frame arrives if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -320,6 +327,8 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; // mark STA as not authenticated; alternatively, it could also be removed from staList + if (wasAssociated) + resetPeerAgreements(sta->address); mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; if (wasAssociated) @@ -523,6 +532,8 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; + if (wasAssociated) + resetPeerAgreements(sta->address); mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; if (wasAssociated) mib->releaseAssociationId(sta->address); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 3276906bbaa..5e61e81d759 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -17,6 +17,7 @@ #include "inet/common/lifecycle/NodeStatus.h" #include "inet/linklayer/common/InterfaceTag_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementLifecycle.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -26,6 +27,12 @@ namespace ieee80211 { using namespace inet::physicallayer; +void Ieee80211MgmtBase::resetPeerAgreements(const MacAddress& peer) +{ + auto mac = getModuleFromPar(par("macModule"), this); + check_and_cast(mac)->resetPeerAgreements(peer); +} + void Ieee80211MgmtBase::initialize(int stage) { OperationalBase::initialize(stage); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index e05f29ace6c..10baef736c4 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -62,6 +62,7 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener /** Utility method to dispose of an unhandled frame */ virtual void dropManagementFrame(Packet *frame); + void resetPeerAgreements(const MacAddress& peer); // Convert the modeled legacy rate elements to the existing MIB representation. static Ieee80211RateSetState makeRateSet(const Ieee80211SupportedRatesElement& rates, diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 5a2c15ca8b3..1bf033e08b0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -622,6 +622,7 @@ void Ieee80211MgmtSta::clearCurrentAssociation() { ASSERT(mib->bssStationData.isAssociated); auto address = assocAP.address; + resetPeerAgreements(address); mib->bssStationData.isAssociated = false; cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index b2b02ad5a4d..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; } }; @@ -70,12 +72,13 @@ struct Ieee80211HtOperation int primaryChannel = 0; int secondaryChannelOffset = 0; Ieee80211HtProtectionMode protectionMode = Ieee80211HtProtectionMode::NO_PROTECTION; + bool rifsMode = false; std::array basicMcsSupported = {}; bool operator==(const Ieee80211HtOperation& other) const { return operatingChannelWidth == other.operatingChannelWidth && primaryChannel == other.primaryChannel && secondaryChannelOffset == other.secondaryChannelOffset && protectionMode == other.protectionMode && - basicMcsSupported == other.basicMcsSupported; + rifsMode == other.rifsMode && basicMcsSupported == other.basicMcsSupported; } }; @@ -89,6 +92,7 @@ struct Ieee80211HtDirectionalCapabilities bool receiverShortGi20 = false; bool receiverShortGi40 = false; int receiverMaxAmpduLengthExponent = 0; + int receiverMinimumMpduStartSpacing = 0; // Encoded receiver limit, 0..7. }; struct Ieee80211NegotiatedHtCapabilities @@ -139,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 9c4db7b2026..e25d07a2ece 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -162,8 +162,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS modeSet->isHtShortGuardIntervalSupported(MHz(20)); localHtCapabilities.shortGi40 = localHtCapabilities.supportedChannelWidths.count(MHz(40)) != 0 && modeSet->isHtShortGuardIntervalSupported(MHz(40)); + bool vhtSupported = false; for (int index = 0; index < modeSet->getNumModes(); index++) { const auto *mode = modeSet->getMode(index); + vhtSupported |= mode->getModulationClass() == physicallayer::IIeee80211Mode::ModulationClass::VHT; int mcs = mode->getHtMcsIndex(); if (mcs >= 0 && mcs < 77 && operationalChannelWidths.count(mode->getDataMode()->getBandwidth()) != 0 && mode->getDataMode()->getNumberOfSpatialStreams() <= operationalHtSpatialStreamLimit) @@ -186,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) @@ -199,6 +204,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS if (protectionMode < 0 || protectionMode > 3) throw cRuntimeError("htProtectionMode must be between 0 and 3"); htOperation.protectionMode = static_cast(protectionMode); + // IEEE Std 802.11-2024, 10.27.3.3: the protected modes permit the conservative zero policy. + // IEEE Std 802.11-2024, 11.38.1: a VHT AP advertises zero. + htOperation.rifsMode = !vhtSupported && (htOperation.protectionMode == Ieee80211HtProtectionMode::NO_PROTECTION || + htOperation.protectionMode == Ieee80211HtProtectionMode::TWENTY_MHZ_PROTECTION); bool peerStateChanged = false; for (auto& entry : peerHtStates) { if (entry.second.valid) { diff --git a/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/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc b/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc index a3c828e2457..e1aceee6450 100644 --- a/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc +++ b/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc @@ -102,6 +102,7 @@ const IReceptionResult *ReceiverBase::computeReceptionResult(const IListening *l auto signalTimeInd = packet->addTagIfAbsent(); signalTimeInd->setStartTime(reception->getStartTime()); signalTimeInd->setEndTime(reception->getEndTime()); + signalTimeInd->setTransmissionId(reception->getTransmission()->getId()); if (snir->getMax() == 0) { auto errorRateInd = packet->addTagIfAbsent(); errorRateInd->setSymbolErrorRate(1); diff --git a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc index b5632d21994..c5032d10a4d 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc @@ -14,6 +14,16 @@ namespace physicallayer { Define_Module(MapCommunicationCache); +const IArrival *MapCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + auto entry = transmissionCache.find(transmission->getId()); + if (entry == transmissionCache.end() || !entry->second.receptionCacheEntries) + return nullptr; + const auto& receptions = *entry->second.receptionCacheEntries; + auto reception = receptions.find(receiver->getId()); + return reception == receptions.end() ? nullptr : reception->second.arrival; +} + MapCommunicationCache::~MapCommunicationCache() { for (auto& it : transmissionCache) diff --git a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h index e9d9c8fa0e5..8ea9821446c 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h @@ -47,6 +47,7 @@ class INET_API MapCommunicationCache : public CommunicationCacheBase public: virtual ~MapCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "MapCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc index b735ca369e3..2fa4300abc4 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc @@ -14,6 +14,16 @@ namespace physicallayer { Define_Module(ReferenceCommunicationCache); +const IArrival *ReferenceCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + int transmissionIndex = transmission->getId(); + int radioIndex = receiver->getId(); + if (transmissionIndex < 0 || transmissionIndex >= int(transmissionCache.size()) || radioIndex < 0) + return nullptr; + auto receptions = transmissionCache[transmissionIndex].receptionCacheEntries; + return receptions && radioIndex < int(receptions->size()) ? receptions->at(radioIndex).arrival : nullptr; +} + ReferenceCommunicationCache::~ReferenceCommunicationCache() { for (auto& transmissionCacheEntry : transmissionCache) diff --git a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h index 8c27a6322ad..b4de8fa4887 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h @@ -42,6 +42,7 @@ class INET_API ReferenceCommunicationCache : public CommunicationCacheBase public: virtual ~ReferenceCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "ReferenceCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc index 3351dc55b87..5e2f96da7b2 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc @@ -14,6 +14,17 @@ namespace physicallayer { Define_Module(VectorCommunicationCache); +const IArrival *VectorCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + int transmissionIndex = transmission->getId() - baseTransmissionId; + int radioIndex = receiver->getId() - baseRadioId; + if (baseTransmissionId < 0 || baseRadioId < 0 || transmissionIndex < 0 || + transmissionIndex >= int(transmissionCache.size()) || radioIndex < 0) + return nullptr; + auto receptions = transmissionCache[transmissionIndex].receptionCacheEntries; + return receptions && radioIndex < int(receptions->size()) ? receptions->at(radioIndex).arrival : nullptr; +} + VectorCommunicationCache::~VectorCommunicationCache() { for (auto& transmissionCacheEntry : transmissionCache) @@ -141,7 +152,8 @@ void VectorCommunicationCache::removeTransmission(const ITransmission *transmiss const ITransmission *VectorCommunicationCache::getTransmission(int id) const { - ASSERT(baseTransmissionId != -1); + if (baseTransmissionId == -1) + return nullptr; int transmissionIndex = id - baseTransmissionId; if (0 <= transmissionIndex && transmissionIndex < (int)transmissionCache.size()) return transmissionCache[transmissionIndex].transmission; diff --git a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h index 3b40aee60bf..3f8e0dbeea0 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h @@ -60,6 +60,7 @@ class INET_API VectorCommunicationCache : public CommunicationCacheBase public: virtual ~VectorCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "VectorCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h index 8688c8d06e0..55f2385206c 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h @@ -65,6 +65,8 @@ class INET_API ICommunicationCache : public virtual IPrintableObject /** @name Reception Cache */ //@{ virtual const IArrival *getCachedArrival(const IRadio *receiver, const ITransmission *transmission) = 0; + // This lookup returns nullptr for absent state. It allocates no cache entry. + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const = 0; virtual void setCachedArrival(const IRadio *receiver, const ITransmission *transmission, const IArrival *arrival) = 0; virtual void removeCachedArrival(const IRadio *receiver, const ITransmission *transmission) = 0; diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h index a56ffb5d8af..0a70986456a 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h @@ -8,6 +8,8 @@ #ifndef __INET_IRADIOMEDIUM_H #define __INET_IRADIOMEDIUM_H +#include + #include "inet/physicallayer/wireless/common/contract/packetlevel/IWirelessSignal.h" #include "inet/common/packet/Packet.h" #include "inet/environment/contract/IMaterial.h" @@ -25,6 +27,7 @@ #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IReceptionDecision.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h" namespace inet { namespace physicallayer { @@ -171,6 +174,24 @@ class INET_API IRadioMedium : public virtual IPrintableObject */ virtual const ITransmission *getTransmission(int id) const = 0; + /** + * Copies the physical endpoints of a live transmission without new propagation calculations. + * An absent transmission returns a snapshot with transmissionId equal to -1. + * An absent radio entry means that the medium has no cached arrival for that radio. + * The caller owns the returned value. Later cache removal does not change it. + */ + virtual TransmissionTimingSnapshot getTransmissionTimingSnapshot(int id) const = 0; + + /** + * Returns the interval before the current transmission at its transmitter. + * For the same transmitter, the interval starts at the previous transmission end. + * For different transmitters, it starts at the previous signal arrival end at the current transmitter. + * Missing current transmission or arrival data returns no value. + * The method uses the supplied snapshot and changes no cache or propagation state. + */ + virtual std::optional computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const = 0; + /** * Returns the listening decision that describes what the receiver detects * on the medium. diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg b/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg index abca21a254b..cf9802507b8 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg @@ -126,4 +126,5 @@ class SignalTimeInd extends SignalTagBase { simtime_t startTime; simtime_t endTime; + int transmissionId = -1; // Medium transmission identity; -1 means unavailable. } diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h new file mode 100644 index 00000000000..312e9bc7476 --- /dev/null +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_TRANSMISSIONTIMINGSNAPSHOT_H +#define __INET_TRANSMISSIONTIMINGSNAPSHOT_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +/** + * This value copies the physical endpoints of one transmission from the medium cache. + * The endpoints retain the propagation result that the medium used for each radio. + * No packet, transmission, arrival, or radio pointer remains in this value. + * A transmissionId of -1 means that the medium no longer has the transmission. + */ +struct INET_API TransmissionTimingSnapshot +{ + int transmissionId = -1; + int transmitterRadioId = -1; + simtime_t startTime = -1; // First transmitted bit, in seconds. + simtime_t endTime = -1; // Last transmitted bit, in seconds. + std::map arrivalEndTimes; // Radio ID to last received bit, in seconds. +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc b/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc index 1366587e9fb..48fe60a49d6 100644 --- a/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc +++ b/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc @@ -656,6 +656,40 @@ const ITransmission *RadioMedium::getTransmission(int id) const return communicationCache->getTransmission(id); } +TransmissionTimingSnapshot RadioMedium::getTransmissionTimingSnapshot(int id) const +{ + TransmissionTimingSnapshot snapshot; + auto transmission = communicationCache->getTransmission(id); + if (!transmission) + return snapshot; + snapshot.transmissionId = transmission->getId(); + snapshot.transmitterRadioId = transmission->getTransmitterRadioId(); + snapshot.startTime = transmission->getStartTime(); + snapshot.endTime = transmission->getEndTime(); + communicationCache->mapRadios([&](const IRadio *radio) { + if (!radio || !radio->getReceiver() || radio->getId() == snapshot.transmitterRadioId) + return; + auto arrival = communicationCache->findCachedArrival(radio, transmission); + if (arrival) + snapshot.arrivalEndTimes[radio->getId()] = arrival->getEndTime(); + }); + return snapshot; +} + +std::optional RadioMedium::computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const +{ + auto current = communicationCache->getTransmission(currentTransmissionId); + if (!current || preceding.transmissionId < 0 || preceding.transmissionId == currentTransmissionId) + return std::nullopt; + if (current->getTransmitterRadioId() == preceding.transmitterRadioId) + return current->getStartTime() - preceding.endTime; + auto arrival = preceding.arrivalEndTimes.find(current->getTransmitterRadioId()); + if (arrival == preceding.arrivalEndTimes.end()) + return std::nullopt; + return current->getStartTime() - arrival->second; +} + const IListeningDecision *RadioMedium::listenOnMedium(const IRadio *radio, const IListening *listening) const { Enter_Method("listenOnMedium"); diff --git a/src/inet/physicallayer/wireless/common/medium/RadioMedium.h b/src/inet/physicallayer/wireless/common/medium/RadioMedium.h index 3952b247ddd..659b86c067e 100644 --- a/src/inet/physicallayer/wireless/common/medium/RadioMedium.h +++ b/src/inet/physicallayer/wireless/common/medium/RadioMedium.h @@ -337,6 +337,9 @@ class INET_API RadioMedium : public SimpleModule, public cListener, public IRadi virtual IWirelessSignal *transmitPacket(const IRadio *transmitter, Packet *packet) override; virtual Packet *receivePacket(const IRadio *receiver, IWirelessSignal *signal) override; virtual const ITransmission *getTransmission(int id) const override; + virtual TransmissionTimingSnapshot getTransmissionTimingSnapshot(int id) const override; + virtual std::optional computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const override; virtual const IListeningDecision *listenOnMedium(const IRadio *receiver, const IListening *listening) const override; diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.h b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.h new file mode 100644 index 00000000000..acbcb72e179 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_IIEEE80211AMPDUERRORMODEL_H +#define __INET_IIEEE80211AMPDUERRORMODEL_H + +#include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h" + +namespace inet { +namespace physicallayer { + +class INET_API IIeee80211AmpduErrorModel +{ + public: + virtual ~IIeee80211AmpduErrorModel() = default; + // This query calculates no probability and consumes no random number. + virtual bool supportsAmpduMode(const IIeee80211Mode *mode) const = 0; + // Invalid input and unsupported modes cause a runtime error. The result is in [0, 1]. + virtual double computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *mode, B length) const = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.ned b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.ned new file mode 100644 index 00000000000..307d9c40b05 --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.ned @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +package inet.physicallayer.wireless.ieee80211.contract.packetlevel; + +import inet.physicallayer.wireless.common.contract.packetlevel.IErrorModel; + +// Optional WLAN role for probabilities over bounded A-MPDU ranges. +moduleinterface IIeee80211AmpduErrorModel extends IErrorModel +{ +} diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index cecad21b7ad..4224c730c4e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -83,6 +83,8 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject virtual const simtime_t getSlotTime() const = 0; virtual const simtime_t getSifsTime() const = 0; virtual const simtime_t getRifsTime() const = 0; + // Default extension after the PPDU, in seconds. A received waveform can omit it for RIFS. + virtual const simtime_t getSignalExtensionTime() const = 0; virtual const simtime_t getCcaTime() const = 0; virtual const simtime_t getPhyRxStartDelay() const = 0; virtual const simtime_t getRxTxTurnaroundTime() const = 0; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg index 52c42112f65..425faaaa0cd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg @@ -57,6 +57,7 @@ class IIeee80211Mode extends cObject simtime_t slotTime; simtime_t sifsTime; simtime_t rifsTime; + simtime_t signalExtensionTime; simtime_t ccaTime; simtime_t phyRxStartDelay; simtime_t rxTxTurnaroundTime; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h index e5c644ff0fc..e0f11ce9eba 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h @@ -33,7 +33,8 @@ class INET_API Ieee80211ErpOfdmMode : public Ieee80211OfdmMode virtual const simtime_t getSifsTime() const override { return 10E-6; } // For ERP-OFDM modes, an ERP packet is followed by a period of no transmission // with a length of 6 μs called the signal extension. - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + signalMode->getDuration() + dataMode->getDuration(dataLength) + 6E-6; } + virtual const simtime_t getSignalExtensionTime() const override { return SimTime(6, SIMTIME_US); } + virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + signalMode->getDuration() + dataMode->getDuration(dataLength) + getSignalExtensionTime(); } }; class INET_API Ieee80211ErpOfdmCompliantModes diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.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 62bc6aa98c0..27b0162ba9c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -258,6 +258,8 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual const simtime_t getShortSlotTime() const; virtual const simtime_t getSifsTime() const override; virtual const simtime_t getRifsTime() const override { return 2E-6; } + // IEEE Std 802.11-2024, Table 19-25: the 2.4 GHz HT PPDU includes a 6 us extension. + virtual const simtime_t getSignalExtensionTime() const override { return centerFrequencyMode == BAND_2_4GHZ ? SimTime(6, SIMTIME_US) : SIMTIME_ZERO; } virtual const simtime_t getCcaTime() const override { return 4E-6; } // < 4 virtual const simtime_t getPhyRxStartDelay() const override { return 24E-6; } virtual const simtime_t getRxTxTurnaroundTime() const override { return 2E-6; } // < 2 @@ -266,7 +268,16 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength); } + // 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(); } }; // A specification of the high-throughput (HT) physical layer (PHY) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h index 36edd5ba0e7..39b7af724dd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h @@ -29,6 +29,7 @@ class INET_API Ieee80211ModeBase : public IIeee80211Mode virtual PreambleType getLegacyPreambleType() const override { return PreambleType::UNKNOWN; } virtual int getHtMcsIndex() const override { return -1; } virtual bool isHtShortGuardInterval() const override { return false; } + virtual const simtime_t getSignalExtensionTime() const override { return SIMTIME_ZERO; } virtual const char *getName() const override { return name.c_str(); } }; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index 3767f5d8001..58294878ce8 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/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211AmpduReceptionDecision.h new file mode 100644 index 00000000000..f279f6f2c32 --- /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/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet { +namespace physicallayer { + +class INET_API Ieee80211AmpduReceptionDecision : public ReceptionDecision +{ + protected: + const bool headerSuccessful; + const bool contentComplete; + const std::vector ranges; + + public: + Ieee80211AmpduReceptionDecision(const IReception *reception, IRadioSignal::SignalPart part, + bool possible, bool attempted, bool successful, bool headerSuccessful, bool contentComplete, + const std::vector& ranges) : + ReceptionDecision(reception, part, possible, attempted, successful), + headerSuccessful(headerSuccessful), contentComplete(contentComplete), ranges(ranges) {} + bool isHeaderSuccessful() const { return headerSuccessful; } + bool isContentComplete() const { return contentComplete; } + const std::vector& getRanges() const { return ranges; } +}; + +} // namespace physicallayer +} // namespace inet + +#endif 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 ee235f05f2e..f147a4812fd 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/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -21,6 +22,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" @@ -258,6 +260,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()); @@ -298,6 +307,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); if (auto ofdmHeader = dynamicPtrCast(phyHeader)) { diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 4c9d2ecd6d1..dcdbe05d9a8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -10,11 +10,29 @@ #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/contract/packetlevel/IIeee80211AmpduErrorModel.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/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.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,13 +45,15 @@ 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"); if (channelNumber != -1) setChannelNumber(channelNumber); } + else if (stage == INITSTAGE_PHYSICAL_LAYER) + validateAmpduErrorModel(modeSet); } std::ostream& Ieee80211Receiver::printToStream(std::ostream& stream, int level, int evFlags) const @@ -62,8 +82,35 @@ 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()); + indication->setRangesArraySize(decision->getRanges().size()); + for (size_t i = 0; i < decision->getRanges().size(); i++) + indication->setRanges(i, decision->getRanges()[i]); + if (!decision->isHeaderSuccessful() || !decision->isContentComplete() || decision->getRanges().empty()) + 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; @@ -71,9 +118,128 @@ const IReceptionResult *Ieee80211Receiver::computeReceptionResult(const IListeni void Ieee80211Receiver::setModeSet(const Ieee80211ModeSet *modeSet) { + validateAmpduErrorModel(modeSet); this->modeSet = modeSet; } +void Ieee80211Receiver::validateAmpduErrorModel(const Ieee80211ModeSet *modes) const +{ + if (modes != nullptr && modes->isHtOperationSupported() && errorModel != nullptr && + dynamic_cast(errorModel) == nullptr) + throw cRuntimeError("HT reception requires IIeee80211AmpduErrorModel on a present error model"); +} + +void Ieee80211Receiver::validateHtReceptionCapabilities(const Ieee80211ModeSet *modes, + const std::set& channelWidths, int spatialStreamLimit) const +{ + Enter_Method("validateHtReceptionCapabilities"); + validateAmpduErrorModel(modes); + if (errorModel == nullptr || modes == nullptr || !modes->isHtOperationSupported()) + return; + auto aggregateModel = check_and_cast(errorModel); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + if (mode->getHtMcsIndex() >= 0 && channelWidths.count(mode->getDataMode()->getBandwidth()) && + mode->getDataMode()->getNumberOfSpatialStreams() <= spatialStreamLimit && + !aggregateModel->supportsAmpduMode(mode)) + throw cRuntimeError("A-MPDU error model does not cover an advertised HT receive mode: MCS %d, short GI %d", + mode->getHtMcsIndex(), mode->isHtShortGuardInterval()); + } +} + +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() || !isIeee80211HtSignalValid(*header, transmission->getMode()))) + return ReceiverBase::computeReceivedPacket(snir, isIeee80211HtSignalValid(*header, transmission->getMode())); + 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; + std::vector ranges; + auto prerequisite = [&](IRadioSignal::SignalPart previous) { + return medium->getReceptionDecision(radio, listening, transmission, previous)->isReceptionSuccessful(); + }; + auto sample = [&](double probability) { + if (!(probability >= 0 && probability <= 1)) + throw cRuntimeError("Invalid A-MPDU success probability: %g", probability); + return probability == 1 || (probability != 0 && dblrand() > 1 - probability); + }; + 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) { + auto layout = ieee80211::inspectMpduAggregate(packet, B(header->getChunkLength()), header->getLengthField()); + auto aggregateModel = dynamic_cast(errorModel); + successful = layout.complete; + for (const auto& range : layout.ranges) { + Ieee80211AmpduRange outcome; + outcome.kind = range.kind == ieee80211::MpduRangeKind::DELIMITER ? 0 : 1; + outcome.offset = range.offset; + outcome.length = range.length; + outcome.valid = range.valid; + outcome.successful = sample(aggregateModel == nullptr ? 1 : + aggregateModel->computeAmpduRangeSuccessRate(snir, transmission->getMode(), range.length)); + successful &= outcome.successful && outcome.valid; + ranges.push_back(outcome); + } + } + } + else if (part == IRadioSignal::SIGNAL_PART_WHOLE) { + auto data = check_and_cast(medium->getReceptionDecision( + radio, listening, transmission, IRadioSignal::SIGNAL_PART_DATA)); + successful = successful && data->isReceptionSuccessful(); + headerSuccessful = data->isHeaderSuccessful(); + contentComplete = data->isContentComplete(); + } + else + throw cRuntimeError("Invalid A-MPDU signal part"); + return new Ieee80211AmpduReceptionDecision(reception, part, possible, attempted, successful, + headerSuccessful, contentComplete, ranges); +} + 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..79541de5bf8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -30,6 +30,11 @@ 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; + void validateAmpduErrorModel(const Ieee80211ModeSet *modes) const; + virtual const IReceptionResult *computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const override; public: @@ -42,6 +47,15 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase virtual void setChannel(const Ieee80211Channel *channel); virtual void setChannelNumber(int channelNumber); virtual bool isHtChannelWidthSupported(Hz channelWidth) const; + /** + * The MAC calls this method before it updates local HT capabilities in the MIB. + * For HT, a present error model must implement IIeee80211AmpduErrorModel and cover every advertised mode in the supplied bounds. + * The bounds include the supplied channel widths, spatial stream limit, and every guard interval in modes. + * Overrides must preserve this check and throw cRuntimeError before the update for an unsupported mode. + * An absent error model needs no probability coverage. + * Validation must not change capability state or consume random draws. + */ + virtual void validateHtReceptionCapabilities(const Ieee80211ModeSet *modes, const std::set& channelWidths, int spatialStreamLimit) const; }; } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg index 8a8d2b21a29..dcc7dd48626 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag.msg @@ -6,11 +6,31 @@ 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; +// A value copy of one scored PSDU range. Kind 0 denotes a delimiter; kind 1 denotes an MPDU. +struct Ieee80211AmpduRange +{ + int kind = 0; + B offset = B(0); + B length = B(0); + bool successful = false; + bool valid = false; +} + +// PHY facts only. MAC delimiter and FCS checks still control acceptance. +class Ieee80211AmpduInd extends TagBase +{ + bool headerSuccessful = false; + bool aggregation = false; + bool contentComplete = false; + Ieee80211AmpduRange ranges[]; +} + // // This is an abstract base class that should not be directly added as a tag. // diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc index 4f1be224443..a5eac8b7868 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc @@ -6,6 +6,7 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" namespace inet { @@ -27,6 +28,17 @@ std::ostream& Ieee80211Transmission::printToStream(std::ostream& stream, int lev return TransmissionBase::printToStream(stream, level); } +std::optional Ieee80211Transmission::getSignalExtensionDuration() const +{ + auto header = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(getPacket()); + auto defaultExtension = mode->getSignalExtensionTime(); + // The PHY Length field contains MAC octets. The mode duration already includes PHY headers and padding. + auto extension = getEndTime() - getStartTime() - mode->getDuration(B(header->getLengthField())) + defaultExtension; + if (extension == SIMTIME_ZERO || extension == defaultExtension) + return extension; + return std::nullopt; +} + } // namespace physicallayer } // namespace inet diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h index 2bf3eae2a82..f1a9e9bbce1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211TRANSMISSION_H #define __INET_IEEE80211TRANSMISSION_H +#include + #include "inet/physicallayer/wireless/common/base/packetlevel/TransmissionBase.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" @@ -30,6 +32,9 @@ class INET_API Ieee80211Transmission : public TransmissionBase virtual const IIeee80211Mode *getMode() const { return mode; } virtual const Ieee80211Channel *getChannel() const { return channel; } + // The actual waveform can omit the default extension through NO_SIG_EXTN for RIFS. + // An unsupported duration returns no value; the method changes no state. + std::optional getSignalExtensionDuration() const; }; } // namespace physicallayer diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc index 48783a2badd..bc59b093e0a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.cc @@ -18,17 +18,35 @@ Ieee80211ErrorModelBase::Ieee80211ErrorModelBase() { } +bool Ieee80211ErrorModelBase::supportsAmpduMode(const IIeee80211Mode *mode) const +{ + return mode != nullptr && mode->getModulationClass() == IIeee80211Mode::ModulationClass::HT && + mode->getHtMcsIndex() >= 0 && mode->getHtMcsIndex() <= 31 && mode->getDataMode()->getBandwidth() == MHz(20); +} + +double Ieee80211ErrorModelBase::computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *mode, B length) const +{ + Enter_Method("computeAmpduRangeSuccessRate"); + if (!supportsAmpduMode(mode)) + throw cRuntimeError("Unsupported HT A-MPDU receive mode"); + if (snir == nullptr || length <= B(0) || length > B(65535)) + throw cRuntimeError("Invalid A-MPDU probability range"); + return getDataSuccessRate(mode, b(length).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..0b53872f749 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211ErrorModelBase.h @@ -10,12 +10,13 @@ #include "inet/physicallayer/wireless/common/base/packetlevel/ErrorModelBase.h" #include "inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211AmpduErrorModel.h" namespace inet { namespace physicallayer { -class INET_API Ieee80211ErrorModelBase : public ErrorModelBase +class INET_API Ieee80211ErrorModelBase : public ErrorModelBase, public IIeee80211AmpduErrorModel { protected: double spectralEfficiency1bit = 2000000.0 / 1000000.0; // 1 bit per symbol with 1 MSPS @@ -37,6 +38,9 @@ class INET_API Ieee80211ErrorModelBase : public ErrorModelBase public: Ieee80211ErrorModelBase(); + virtual bool supportsAmpduMode(const IIeee80211Mode *mode) const override; + virtual double computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *mode, B length) const override; + virtual double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override; virtual double computeBitErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override; virtual double computeSymbolErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.ned index fdf6c7710d0..69651c2ad73 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.ned @@ -8,8 +8,9 @@ package inet.physicallayer.wireless.ieee80211.packetlevel.errormodel; import inet.physicallayer.wireless.common.base.packetlevel.ErrorModelBase; +import inet.physicallayer.wireless.ieee80211.contract.packetlevel.IIeee80211AmpduErrorModel; -module Ieee80211NistErrorModel extends ErrorModelBase +module Ieee80211NistErrorModel extends ErrorModelBase like IIeee80211AmpduErrorModel { parameters: @class(Ieee80211NistErrorModel); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.ned index 1fde176f539..5f27ba31459 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211YansErrorModel.ned @@ -8,8 +8,9 @@ package inet.physicallayer.wireless.ieee80211.packetlevel.errormodel; import inet.physicallayer.wireless.common.base.packetlevel.ErrorModelBase; +import inet.physicallayer.wireless.ieee80211.contract.packetlevel.IIeee80211AmpduErrorModel; -module Ieee80211YansErrorModel extends ErrorModelBase +module Ieee80211YansErrorModel extends ErrorModelBase like IIeee80211AmpduErrorModel { parameters: @class(Ieee80211YansErrorModel); diff --git a/src/inet/queueing/contract/ISelectivePacketCollection.h b/src/inet/queueing/contract/ISelectivePacketCollection.h new file mode 100644 index 00000000000..7dd07ba071c --- /dev/null +++ b/src/inet/queueing/contract/ISelectivePacketCollection.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_ISELECTIVEPACKETCOLLECTION_H +#define __INET_ISELECTIVEPACKETCOLLECTION_H + +#include + +#include "inet/common/packet/Packet.h" + +namespace inet { +namespace queueing { + +class INET_API UnsupportedPacketSelection : public cRuntimeError +{ + public: + explicit UnsupportedPacketSelection(const char *message) : cRuntimeError("%s", message) {} +}; + +// Optional atomic selection in the configured service order. +// UnsupportedPacketSelection identifies an unavailable capability; other errors retain their original type. +class INET_API ISelectivePacketCollection +{ + public: + virtual ~ISelectivePacketCollection() {} + // The result is borrowed. The query changes no packet or queue state. + virtual Packet *peekEligiblePacket(const std::function& predicate) const = 0; + // The caller receives ownership. Each layer applies its ordinary pull effects once. + virtual Packet *extractPacket(Packet *packet) = 0; +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.cc b/src/inet/queueing/queue/CompoundPacketQueueBase.cc index 7d290d0cc1e..d633b869478 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.cc +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.cc @@ -14,6 +14,26 @@ namespace queueing { Define_Module(CompoundPacketQueueBase); +Packet *CompoundPacketQueueBase::peekEligiblePacket(const std::function& predicate) const +{ + if (auto selected = dynamic_cast(collection)) + return selected->peekEligiblePacket(predicate); + throw UnsupportedPacketSelection("CompoundPacketQueueBase provider does not support ISelectivePacketCollection"); +} + +Packet *CompoundPacketQueueBase::extractPacket(Packet *packet) +{ + Enter_Method("extractPacket"); + if (auto selected = dynamic_cast(collection)) + selected->extractPacket(packet); + else + throw UnsupportedPacketSelection("Provider does not support selected extraction"); + take(packet); + emit(packetPulledSignal, packet); + drop(packet); + return packet; +} + void CompoundPacketQueueBase::initialize(int stage) { PacketQueueBase::initialize(stage); diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.h b/src/inet/queueing/queue/CompoundPacketQueueBase.h index 706d9fb32c5..bff3acf749a 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.h +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.h @@ -13,11 +13,12 @@ #include "inet/queueing/common/PassivePacketSourceRef.h" #include "inet/queueing/contract/IPacketCollection.h" #include "inet/queueing/contract/IPacketDropperFunction.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" namespace inet { namespace queueing { -class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListener +class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListener, public ISelectivePacketCollection { protected: int packetCapacity = -1; @@ -37,6 +38,8 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual bool isOverloaded() const; public: + virtual Packet *peekEligiblePacket(const std::function& predicate) const override; + virtual Packet *extractPacket(Packet *packet) override; virtual ~CompoundPacketQueueBase() { delete packetDropperFunction; } virtual int getMaxNumPackets() const override { return packetCapacity; } diff --git a/src/inet/queueing/queue/PacketQueue.cc b/src/inet/queueing/queue/PacketQueue.cc index 5cb89c3a638..dc6c386d1e7 100644 --- a/src/inet/queueing/queue/PacketQueue.cc +++ b/src/inet/queueing/queue/PacketQueue.cc @@ -118,14 +118,38 @@ void PacketQueue::pushPacket(Packet *packet, const cGate *gate) Packet *PacketQueue::pullPacket(const cGate *gate) { Enter_Method("pullPacket"); - auto packet = check_and_cast(queue.front()); + return pullSelectedPacket(check_and_cast(queue.front())); +} + +Packet *PacketQueue::peekEligiblePacket(const std::function& predicate) const +{ + for (cQueue::Iterator it(queue); !it.end(); it++) { + auto packet = check_and_cast(*it); + if (predicate(packet)) + return packet; + } + return nullptr; +} + +Packet *PacketQueue::extractPacket(Packet *packet) +{ + Enter_Method("extractPacket"); + if (!queue.contains(packet)) + throw cRuntimeError("Selected packet is not in this queue"); + pullSelectedPacket(packet); + drop(packet); + return packet; +} + +Packet *PacketQueue::pullSelectedPacket(Packet *packet) +{ EV_INFO << "Pulling packet" << EV_FIELD(packet) << EV_ENDL; if (buffer != nullptr) { queue.remove(packet); buffer->removePacket(packet); } else - queue.pop(); + queue.remove(packet); auto queueingTime = simTime() - packet->getArrivalTime(); auto packetEvent = new PacketEvent(); insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); diff --git a/src/inet/queueing/queue/PacketQueue.h b/src/inet/queueing/queue/PacketQueue.h index 4debf8541ec..0f9b5f5945a 100644 --- a/src/inet/queueing/queue/PacketQueue.h +++ b/src/inet/queueing/queue/PacketQueue.h @@ -17,11 +17,12 @@ #include "inet/queueing/contract/IPacketBuffer.h" #include "inet/queueing/contract/IPacketComparatorFunction.h" #include "inet/queueing/contract/IPacketDropperFunction.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" namespace inet { namespace queueing { -class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICallback, public TransparentProtocolRegistrationListener +class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICallback, public TransparentProtocolRegistrationListener, public ISelectivePacketCollection { protected: int packetCapacity = -1; @@ -43,8 +44,11 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual IPacketComparatorFunction *createComparatorFunction(const char *comparatorClass) const; virtual bool isOverloaded() const; + Packet *pullSelectedPacket(Packet *packet); public: + virtual Packet *peekEligiblePacket(const std::function& predicate) const override; + virtual Packet *extractPacket(Packet *packet) override; virtual ~PacketQueue() { delete packetDropperFunction; } virtual cGate *getRegistrationForwardingGate(cGate *gate) override; diff --git a/src/inet/queueing/scheduler/PriorityScheduler.cc b/src/inet/queueing/scheduler/PriorityScheduler.cc index 0c2c1d5d3a0..c352065993e 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.cc +++ b/src/inet/queueing/scheduler/PriorityScheduler.cc @@ -6,12 +6,58 @@ #include "inet/queueing/scheduler/PriorityScheduler.h" +#include "inet/common/Simsignals.h" namespace inet { namespace queueing { Define_Module(PriorityScheduler); +Packet *PriorityScheduler::peekEligiblePacket(const std::function& predicate) const +{ + if (isStreamingPacket()) + throw UnsupportedPacketSelection("Selected extraction does not support packet streaming"); + for (size_t i = 0; i < providers.size(); i++) { + int index = getInputGateIndex(i); + if (auto selected = dynamic_cast(collections[index])) { + if (auto packet = selected->peekEligiblePacket(predicate)) + return packet; + } + else if (!collections[index] || !collections[index]->isEmpty()) + throw UnsupportedPacketSelection("PriorityScheduler child does not support ISelectivePacketCollection"); + } + return nullptr; +} + +Packet *PriorityScheduler::extractPacket(Packet *packet) +{ + Enter_Method("extractPacket"); + if (isStreamingPacket()) + throw UnsupportedPacketSelection("Selected extraction does not support packet streaming"); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (!collection) + throw UnsupportedPacketSelection("PriorityScheduler provider does not support IPacketCollection"); + for (int j = 0; j < collection->getNumPackets(); j++) { + if (collection->getPacket(j) != packet) + continue; + auto selected = dynamic_cast(collection); + if (!selected) + throw UnsupportedPacketSelection("Provider does not support selected extraction"); + selected->extractPacket(packet); + take(packet); + handlePacketScheduled(packet, i); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; + } + } + throw cRuntimeError("Selected packet is not in this scheduler"); +} + void PriorityScheduler::initialize(int stage) { PacketSchedulerBase::initialize(stage); diff --git a/src/inet/queueing/scheduler/PriorityScheduler.h b/src/inet/queueing/scheduler/PriorityScheduler.h index cf35dce4822..2cfe2704249 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.h +++ b/src/inet/queueing/scheduler/PriorityScheduler.h @@ -10,11 +10,12 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" namespace inet { namespace queueing { -class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public ISelectivePacketCollection { protected: std::vector collections; @@ -24,6 +25,8 @@ class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IP virtual int schedulePacket() const override; public: + virtual Packet *peekEligiblePacket(const std::function& predicate) const override; + virtual Packet *extractPacket(Packet *packet) override; virtual int getMaxNumPackets() const override { return -1; } virtual int getNumPackets() const override; diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 3ae180b8557..175356337e3 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -7,7 +7,7 @@ # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, dd36-1076/tplx;b703-3ef2/~tNl;e07d-2980/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, b80d-4ae8/tplx;72b7-86c2/~tNl;d385-62e0/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 4fd2-bb56/tplx;ecaf-89f7/~tNl;9398-0fae/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 2f2d-73b1/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 2c80-6844/tplx;4e9e-cf86/~tNl;ea3d-e802/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -658,7 +658,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, d929-5913/tplx;f602-4424/~tNl;f2fb-4d98/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 0473-f9d2/tplx;e92c-7352/~tNl;317d-aa4f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, e07a-4c5f/tplx;9f6c-7889/~tNl;29ea-169c/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, fbab-b84e/tplx;47cb-b9e5/~tNl;c4f3-fa00/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 6edb-0326/tplx;2a8a-50ee/~tNl;4301-882d/~tND;08b5-d005/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 572d1330c90..9baf2771fb6 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -199,9 +199,9 @@ /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;7b7c-2bfb/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 1082-899c/tplx;0068-9ae9/~tNl;e0b3-3193/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 2235-516d/tplx;7b51-e67c/~tNl;9a3f-afc7/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 8bfb-6ac9/tplx;5647-2cd1/~tNl;481a-ec9d/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 9161-625d/tplx;6a02-09aa/~tNl;cbc0-3b0a/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 36f4-5c22/tplx;66b0-94cd/~tNl;45f3-af4f/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 351b-d4ba/tplx;5c1f-69a8/~tNl;6182-6da3/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 8df2-40a3/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, c99f-354d/tplx;8e10-a3f0/~tNl;ac4b-3ba1/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 4228-9743/tplx;8610-abcd/~tNl;68c2-19f0/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 2597-1e2e/tplx;d269-c6ae/~tNl;2f5c-4cca/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 6150-7a8b/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb5-d354/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 4ffd-4fbf/tplx;3a89-2021/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, ab91-87f3/tplx;f30c-6aa4/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 diff --git a/tests/module/Ieee80211AmpduAdmission_1.test b/tests/module/Ieee80211AmpduAdmission_1.test new file mode 100644 index 00000000000..fce3b8d4d82 --- /dev/null +++ b/tests/module/Ieee80211AmpduAdmission_1.test @@ -0,0 +1,700 @@ +%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/MpduAggregationUtils.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 computeAmpduRangeSuccessRate(const ISnir *, const IIeee80211Mode *, 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); + auto layout = inspectMpduAggregate(packet, B(0), submittedLength); + ASSERT(layout.complete); + simtime_t duration = -1; + int currentMembers = 0; + for (const auto& range : layout.ranges) { + if (range.kind != MpduRangeKind::MPDU) continue; + auto header = packet->peekDataAt(range.offset); + ASSERT(header->getRetry() == singletonCase()); + ASSERT(header->getAckPolicy() == NORMAL_ACK); + if (currentMembers == 0) duration = header->getDurationField(); + ASSERT(header->getDurationField() == duration); + submittedMembers++; + currentMembers++; + } + 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/Ieee80211AmpduCapabilities_1.test b/tests/module/Ieee80211AmpduCapabilities_1.test new file mode 100644 index 00000000000..67dec4d175e --- /dev/null +++ b/tests/module/Ieee80211AmpduCapabilities_1.test @@ -0,0 +1,124 @@ +%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/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; + +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()); + 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); + 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/Ieee80211AmpduExchange_1.test b/tests/module/Ieee80211AmpduExchange_1.test new file mode 100644 index 00000000000..2fc4e53dc3b --- /dev/null +++ b/tests/module/Ieee80211AmpduExchange_1.test @@ -0,0 +1,373 @@ +%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/MpduAggregationUtils.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 computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *, B length) const override { + 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)); + auto layout = inspectMpduAggregate(packet, B(0), B(packet->getDataLength())); + ASSERT(layout.complete); + MacAddress receiver, transmitter; + Tid tid = -1; + simtime_t duration = -1; + int count = 0; + for (const auto& range : layout.ranges) { + ASSERT(range.valid); + if (range.kind != MpduRangeKind::MPDU) + continue; + Packet member("observed-member", packet->peekDataAt(range.offset, range.length)); + 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(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/Ieee80211AmpduMacBoundary_1.test b/tests/module/Ieee80211AmpduMacBoundary_1.test new file mode 100644 index 00000000000..d8949740108 --- /dev/null +++ b/tests/module/Ieee80211AmpduMacBoundary_1.test @@ -0,0 +1,280 @@ +%description: +The production radio sends aggregate containers to the production MAC and Rx. +Rx accepts correct members with valid FCS trailers and drops failed members. +The aggregate retains the PHY range outcomes 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. +Typed and byte representations preserve the same recovered ranges. + +%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/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 computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *, B length) const override { + auto packet = snir->getReception()->getTransmission()->getPacket(); + bool failed = packet->isName("failed") || (packet->isName("partial") && length == B(101)); + return length != B(4) && 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->hasBitError()); + auto indication = packet->getTag(); + ASSERT(indication->getAggregation() && indication->getHeaderSuccessful() && indication->getContentComplete()); + if (received == 3) { + ASSERT(indication->getRangesArraySize() == 30); + ASSERT(!indication->getRanges(0).valid && indication->getRanges(0).offset == B(0)); + int members = 0; + for (size_t i = 0; i < indication->getRangesArraySize(); i++) { + const auto& range = indication->getRanges(i); + ASSERT(range.offset + range.length <= packet->getDataLength()); + ASSERT(range.successful); + if (range.kind == 1) { + ASSERT(range.valid); + ASSERT(range.offset == (members == 0 ? B(108) : B(216))); + ASSERT(range.length == (members == 0 ? B(101) : B(102))); + members++; + } + } + ASSERT(members == 2); + } + else { + ASSERT(indication->getRangesArraySize() == 6); + int failed = 0; + for (size_t i = 0; i < indication->getRangesArraySize(); i++) { + const auto& range = indication->getRanges(i); + ASSERT(range.valid); + failed += !range.successful; + } + ASSERT(failed == (received == 0 ? 0 : received == 1 ? 1 : 3)); + } + 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}; + ASSERT(reception.anyCorrect == !expected.empty()); + ASSERT(reception.members.size() == expected.size()); + ASSERT(reception.topology && reception.topology->length == B(318)); + ASSERT(reception.topology->members.size() == (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; + protected: + void initialize() override { + 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 *details) override { + ASSERT(signal == packetDroppedSignal); + auto packet = check_and_cast(object); + ASSERT(dropped < 4); + ASSERT(packet->isName(dropped == 0 ? "partial" : "failed")); + int sequence = dropped == 0 ? 1 : dropped - 1; + ASSERT(packet->getDataLength() == B(100 + sequence)); + ASSERT(packet->peekAtFront()->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 && rx->received == 4 && 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); + Packet mpdu("member"); + auto member = makeShared(); + 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(std::vector(4 - length % 4, 0))); + } + } + 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 && rx->received == 4 && dropped == 4 && 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; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211RadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; + +module BoundaryErrorModel extends Ieee80211NistErrorModel +{ + parameters: + @class(::BoundaryErrorModel); +} +simple BoundaryRx extends Rx +{ + parameters: + @class(::BoundaryRx); +} +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].mac.rx.typename = "BoundaryRx" +*.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/Ieee80211AmpduPhyCompatibility_1.test b/tests/module/Ieee80211AmpduPhyCompatibility_1.test new file mode 100644 index 00000000000..c739c7944c1 --- /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->getTransmissionId() >= 0); + 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.") diff --git a/tests/module/Ieee80211AmpduPhyReception_1.test b/tests/module/Ieee80211AmpduPhyReception_1.test new file mode 100644 index 00000000000..34f8532a3bb --- /dev/null +++ b/tests/module/Ieee80211AmpduPhyReception_1.test @@ -0,0 +1,664 @@ +%description: +Real HT20 radios preserve separate delimiter and MPDU outcomes after partial loss. +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 must refuse absent roles and incomplete advertised receive coverage before the MIB update. +Ordinary NIST, Yans, absent-model, and non-HT controls retain their defined behavior and metadata. + +%extraargs: -r 0..467 + +%postrun-command: python3 check-models.py + +%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/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; } +}; + +class AmpduModelAudit +{ + public: + virtual ~AmpduModelAudit() = default; + mutable int rangeCalls = 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)"); + ASSERT(!this->supportsAmpduMode(nullptr)); + 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); + ASSERT(this->supportsAmpduMode(mode) == supported); + 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::computeAmpduRangeSuccessRate(&snir, mode, B(length)); + ASSERT(actual == expected && actual >= 0 && actual <= 1); + checked++; + } + } + } + ASSERT(checked == 64 * 7 * 6); + ASSERT(this->getRNG(0)->getNumbersDrawn() == draws); + ASSERT(rangeCalls == 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 computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *mode, B length) const override { + auto draws = this->getRNG(0)->getNumbersDrawn(); + double actual = Base::computeAmpduRangeSuccessRate(snir, mode, 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); + rangeCalls++; + return actual; + } +}; + +class AmpduNistProbe : public AmpduSharedModel {}; +class AmpduYansProbe : public AmpduSharedModel {}; +Define_Module(AmpduNistProbe); +Define_Module(AmpduYansProbe); + +class AmpduDeterministicModel : public Ieee80211NistErrorModel +{ + public: + mutable int rangeCalls = 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 computeAmpduRangeSuccessRate(const ISnir *, const IIeee80211Mode *, B length) const override { + int index = rangeCalls++; + 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; + bool supportsAmpduMode(const IIeee80211Mode *mode) const override { + return Ieee80211NistErrorModel::supportsAmpduMode(mode) && + (!par("longOnly").boolValue() || !mode->isHtShortGuardInterval()) && + mode->getDataMode()->getNumberOfSpatialStreams() == 1; + } + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override { + probabilityCalls++; + return Ieee80211NistErrorModel::computePacketErrorRate(snir, part); + } + double computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *mode, B length) const override { + probabilityCalls++; + return Ieee80211NistErrorModel::computeAmpduRangeSuccessRate(snir, mode, length); + } +}; +Define_Module(AmpduLimitedModel); + +class AmpduNoRoleModel : 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(AmpduNoRoleModel); + +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->rangeCalls == 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->rangeCalls == 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->rangeCalls == 0 && audit->wholeCalls == 0); + ASSERT(draws == getSubmodule("receiver")->getRNG(0)->getNumbersDrawn()); + } + } + 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")); + auto audit = dynamic_cast(getSubmodule("receiver")->getSubmodule("errorModel")); + int calls = model ? model->rangeCalls : 0; + int auditCalls = audit ? audit->rangeCalls + 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++) { + 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->rangeCalls); + if (audit) { + ASSERT(auditCalls == audit->rangeCalls + audit->headerCalls + audit->preambleCalls + audit->wholeCalls); + auto data = check_and_cast(medium->getReceptionDecision(this, listening, transmission, IRadioSignal::SIGNAL_PART_DATA)); + audit->outcomes = data->getRanges(); + } + } + Ieee80211Radio::endReception(timer); + } +}; +Define_Module(AmpduProbeRadio); + +class AmpduPhyDriver : public cSimpleModule, public cListener +{ + protected: + int received = 0; + simtime_t duration; + Ptr retained; + 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() == 22) + ASSERT(message.find("requires IIeee80211AmpduErrorModel") != std::string::npos); + else if (scenario() == 23) + ASSERT(message.find("does not cover an advertised") != std::string::npos); + else 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() == 22 || scenario() == 23 || 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")); + bool refused = false; + try { receiver->setModeSet(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); } + catch (const cRuntimeError& error) { refused = std::string(error.what()).find("requires IIeee80211AmpduErrorModel") != std::string::npos; } + ASSERT(refused); + } + } + 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(std::vector(3, 0))); + } + } + 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() == (scenario() >= 35 ? 7 : 0) && mode->getDataMode()->getBandwidth() == MHz(20) && + mode->isHtShortGuardInterval() == (scenario() == 15 || (scenario() >= 31 && (scenario() - 31) % 4 >= 2))) { + packet->addTag()->setMode(mode); + break; + } + } + send(packet, "out", 0); + } + else { + auto packet = check_and_cast(message); + if (scenario() >= 31) { + auto indication = packet->getTag(); + retained = makeShared(*indication); + ASSERT(packet->getDataLength() == B(322)); + ASSERT(packet->hasBitError() == !indication->getHeaderSuccessful()); + 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->hasBitError() == refused); + if (ordinary()) + ASSERT(packet->getDataLength() == B(100)); + else { + auto indication = packet->getTag(); + ASSERT(packet->getDataLength() == B(322)); + retained = makeShared(*indication); + bool badHeader = refused && scenario() != 14 && scenario() != 19; + ASSERT(indication->getHeaderSuccessful() == !badHeader); + ASSERT(indication->getAggregation() == !badHeader); + ASSERT(indication->getContentComplete() == !refused); + ASSERT(indication->getRangesArraySize() == (refused ? 0 : 7)); + if (!refused) { + int failed = 0; + for (size_t i = 0; i < indication->getRangesArraySize(); i++) + failed += !indication->getRanges(i).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()->getTransmissionId() >= 0); + 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"); + 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->rangeCalls == (header ? 7 : 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).successful == expected); + ASSERT(retained && retained->getRanges(i).successful == 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) { + ASSERT(model->wholeCalls == 0); + ASSERT(model->rangeCalls == (scenario() >= 10 && scenario() <= 14 ? 0 : 7)); + if (scenario() == 11) ASSERT(model->headerCalls == 0); + ASSERT(rx->getRNG(0)->getNumbersDrawn() == (scenario() == 16 ? 7 : 0)); + } + auto medium = check_and_cast(getModuleByPath("^.radioMedium")); + ASSERT(medium->getCommunicationCache()->getNumTransmissions() == 0); + if (retained && !headerFailure && scenario() != 14 && scenario() != 19) + ASSERT(retained->getRangesArraySize() == 7); + 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.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: + bool longOnly; + @class(::AmpduLimitedModel); +} +module AmpduNoRoleModel extends ErrorModelBase +{ + parameters: + @class(::AmpduNoRoleModel); +} + +module AmpduDeterministicModel extends Ieee80211NistErrorModel +{ + parameters: + int scenario; + @class(::AmpduDeterministicModel); +} +module AmpduProbeRadio extends Ieee80211Radio +{ + parameters: + int scenario; + receiver.errorModel.typename = default(scenario >= 31 ? ((scenario - 31) % 2 == 0 ? "AmpduNistProbe" : "AmpduYansProbe") : (scenario == 29 ? "AmpduNoRoleModel" : (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 ? "AmpduNoRoleModel" : (scenario == 23 || scenario == 24 ? "AmpduLimitedModel" : (scenario == 27 || scenario == 28 ? "Ieee80211BerTableErrorModel" : ""))); +} + +simple AmpduPhyDriver +{ + parameters: + @class(::AmpduPhyDriver); + int scenario; + int seed; + 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 = ${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..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} >= 3 && ${scenario} < 22 +*.node[*].radio.receiver.bandwidth = 20MHz +*.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 +*.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 = ${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} +*.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 + +%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.") diff --git a/tests/module/Ieee80211AmpduPreparation_1.test b/tests/module/Ieee80211AmpduPreparation_1.test new file mode 100644 index 00000000000..b831c42a15f --- /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([](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: diff --git a/tests/module/Ieee80211AmpduRecovery_1.test b/tests/module/Ieee80211AmpduRecovery_1.test new file mode 100644 index 00000000000..30be19018a3 --- /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 computeAmpduRangeSuccessRate(const ISnir *, const IIeee80211Mode *, 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 new file mode 100644 index 00000000000..a8be9aa524c --- /dev/null +++ b/tests/module/Ieee80211AmpduReordering_1.test @@ -0,0 +1,461 @@ +%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 computeAmpduRangeSuccessRate(const ISnir *snir, const IIeee80211Mode *, B length) const override { + 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 == 1 ? 0 : 1; + } + return packet->isName("all-failed") || (packet->isName("partial") && length == B(101)) ? 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 frameTransmissionStarted(Packet *) override {} + 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)); + ASSERT(transmission->getSignalExtensionDuration() == 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/Ieee80211AmpduResponseWindow_1.test b/tests/module/Ieee80211AmpduResponseWindow_1.test new file mode 100644 index 00000000000..63e2764edd8 --- /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 computeAmpduRangeSuccessRate(const ISnir *, const IIeee80211Mode *, 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/Ieee80211AmpduRxBoundary_1.test b/tests/module/Ieee80211AmpduRxBoundary_1.test new file mode 100644 index 00000000000..5f447a161aa --- /dev/null +++ b/tests/module/Ieee80211AmpduRxBoundary_1.test @@ -0,0 +1,161 @@ +%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, public cListener +{ + 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 { + Rx::initialize(stage); + 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(indication->getRangesArraySize() == 1); + const auto& range = indication->getRanges(0); + ASSERT(range.kind == 1 && range.offset == B(0) && range.length == B(1)); + ASSERT(range.valid && !range.successful); + 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)); + ASSERT(time->getTransmissionId() == 73); + 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); + indication->setRangesArraySize(1); + Ieee80211AmpduRange range; + range.kind = 1; + range.offset = B(0); + range.length = B(1); + range.valid = true; + range.successful = false; + indication->setRanges(0, range); + source->addTag()->setPower(W(0.001)); + source->addTag()->setMinimumSnir(42); + auto time = source->addTag(); + time->setStartTime(SIMTIME_ZERO); + time->setEndTime(SimTime(1, SIMTIME_US)); + time->setTransmissionId(73); + 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 && retained->getRanges(0).length == B(1)); + 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 diff --git a/tests/module/Ieee80211AmpduTxStart_1.test b/tests/module/Ieee80211AmpduTxStart_1.test new file mode 100644 index 00000000000..d3c151b1e50 --- /dev/null +++ b/tests/module/Ieee80211AmpduTxStart_1.test @@ -0,0 +1,225 @@ +%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; } + 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/Ieee80211BlockAckActionDelivery_1.test b/tests/module/Ieee80211BlockAckActionDelivery_1.test new file mode 100644 index 00000000000..b2ac3745b86 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDelivery_1.test @@ -0,0 +1,70 @@ +%description: +Check complete Block Ack Action delivery through the production recipient data service. +An original Action reaches HCF. Its MAC retry does not reach the agreement handler. +A new Action with another sequence number remains eligible for dispatch. +Two management fragments produce one complete Action after the final fragment. + +%file: ActionDelivery.cc +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" + +namespace inet::ieee80211 { +class ActionDeliveryTest : public cSimpleModule { + protected: + void initialize() override { + scheduleAt(SIMTIME_ZERO, new cMessage("test")); + } + void handleMessage(cMessage *message) override { + delete message; + auto service = check_and_cast(getModuleByPath("^.recipient")); + for (int index = 0; index < 6; index++) { + auto header = makeShared(); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); + header->setSequenceNumber(SequenceNumberCyclic(index < 2 ? 10 : index == 2 ? 11 : 12)); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + header->setDialogToken(37); + header->setTid(5); + header->setBufferSize(8); + header->setBlockAckPolicy(true); + header->setRetry(index == 1 || index == 5); + header->setMoreFragments(index == 3); + header->setFragmentNumber(index >= 4 ? 1 : 0); + auto packet = new Packet("action", header); + packet->insertAtBack(makeShared(B(2))); + packet->insertAtBack(makeShared()); + auto frames = service->managementFrameReceived(packet, header); + ASSERT(frames.size() == (index == 0 || index == 2 || index == 4 ? 1 : 0)); + for (auto frame : frames) { + ASSERT(frame->peekAtFront() != nullptr); + take(frame); + delete frame; + } + } + std::cout << "Block Ack Action delivery assertions passed.\n"; + } +}; +Define_Module(ActionDeliveryTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +simple ActionDeliveryTest { parameters: @class(::inet::ieee80211::ActionDeliveryTest); } +network ActionDeliveryNetwork { + submodules: + recipient: RecipientQosMacDataService; + test: ActionDeliveryTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDeliveryNetwork +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Block Ack Action delivery assertions passed. diff --git a/tests/module/Ieee80211BlockAckActionDispatch_1.test b/tests/module/Ieee80211BlockAckActionDispatch_1.test new file mode 100644 index 00000000000..e09a34d1ab8 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDispatch_1.test @@ -0,0 +1,179 @@ +%description: +Check duplicate Block Ack Actions through initialized production HCF. +The test observes response submission without a wireless transmission. +HCF must acknowledge a MAC retry without a second agreement dispatch. + +The deadline check derives 5 ms from an agreement created at 1 ms with a 4 ms timeout. + +%extraargs: -r 0..2 + +%file: ActionDispatch.cc +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ActionDispatchHcf : public Hcf { + public: + int dispatches = 0; + int acknowledgments = 0; + int responses = 0; + int delivered = 0; + Ptr response; + void checkAgreementDeadline() { + Enter_Method("checkAgreementDeadline"); + receiveRequest(false); + auto expectedDeadline = SimTime(5, SIMTIME_MS); + ASSERT(recipientBlockAckAgreementHandler->computeEarliestExpirationTime() == expectedDeadline); + scheduleInactivityTimer(); + ASSERT(inactivityTimer->getArrivalTime() == expectedDeadline); + cancelEvent(inactivityTimer); + } + void receiveRequest(bool retry) { + Enter_Method("receiveRequest"); + auto header = makeShared(); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setReceiverAddress(mac->getAddress()); + header->setSequenceNumber(SequenceNumberCyclic(10)); + header->setRetry(retry); + header->setTid(5); + header->setDialogToken(37); + header->setBlockAckPolicy(true); + header->setBufferSize(8); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("request", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode( + modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + } + void checkNegotiatedWindow() { + Enter_Method("checkNegotiatedWindow"); + receiveRequest(false); + ASSERT(response != nullptr && response->getBufferSize() == 8); + auto agreement = recipientBlockAckAgreementHandler->getAgreement(5, MacAddress("02:00:00:00:00:02")); + ASSERT(agreement != nullptr && agreement->getBufferSize() == 8); + auto bar = makeShared(); + bar->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + bar->setReceiverAddress(mac->getAddress()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(104)); + bar->setFragmentNumber(0); + auto request = new Packet("bar-before-data", bar); + request->insertAtBack(makeShared()); + request->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(request, bar); + ASSERT(delivered == 0); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(data->getTransmitterAddress()); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(104)); + data->setAckPolicy(BLOCK_ACK); + auto packet = new Packet("first-data", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, data); + ASSERT(delivered == 1); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered++; + take(frame); + delete frame; + } + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + dispatches++; + Hcf::recipientProcessReceivedManagementFrame(header); + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + acknowledgments++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + responses++; + response = dynamicPtrCast(header); + delete packet; + } +}; +Define_Module(ActionDispatchHcf); +class ActionDispatchTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + if (par("scenario").intValue() == 1) + hcf->checkAgreementDeadline(); + else if (par("scenario").intValue() == 2) + hcf->checkNegotiatedWindow(); + else { + hcf->receiveRequest(false); + hcf->receiveRequest(true); + ASSERT(hcf->acknowledgments == 2); + ASSERT(hcf->dispatches == 1); + ASSERT(hcf->responses == 1); + ASSERT(hcf->response != nullptr && hcf->response->getStatusCode() == SC_SUCCESSFUL); + ASSERT(hcf->response->getDialogToken() == 37); + ASSERT(hcf->response->getBufferSize() == 64); + ASSERT(hcf->response->getBlockAckTimeoutValue() == SimTime(4, SIMTIME_MS)); + } + std::cout << "Block Ack Action dispatch assertions passed.\n"; + } +}; +Define_Module(ActionDispatchTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ActionDispatchHcf extends Hcf { parameters: @class(::inet::ieee80211::ActionDispatchHcf); } +simple ActionDispatchTest { parameters: int scenario; @class(::inet::ieee80211::ActionDispatchTest); } +network ActionDispatchNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ActionDispatchTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDispatchNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +*.test.scenario = ${scenario=0,1,2} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ActionDispatchHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.blockAckTimeoutValue = 4ms +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.maximumAllowedBufferSize = ${scenario} == 2 ? 8 : 64 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack Action dispatch assertions passed. diff --git a/tests/module/Ieee80211BlockAckActionDisposal_1.test b/tests/module/Ieee80211BlockAckActionDisposal_1.test new file mode 100644 index 00000000000..b8444ee40c4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDisposal_1.test @@ -0,0 +1,470 @@ +%description: +Check owned Block Ack Actions through actual pending-queue overflow. Request disposal before transmission cancels initial setup without teardown. Successful response disposal preserves its recipient agreement. Originator DELBA disposal drops its deferred batch and leaves recovery required. + +Later DATA starts another recovery DELBA. Recipient DELBA disposal preserves the opposite role. Later VO queue eviction must release deferred BE DATA without another BE arrival. An isolated cleanup case supplies another peer's retry through the production recovery API. Reset must clear a retry from an actual failed RTS before the protected Action transmits. + +The fixture supplies agreement records and does not transmit frames through a peer radio. + +%extraargs: -r 0..10 + +%file: ActionDisposal.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/queueing/queue/PacketQueue.h" + +namespace inet::ieee80211 { +class DisposalQueue : public queueing::PacketQueue { + public: + void setCapacity(int capacity) { packetCapacity = capacity; } +}; +Define_Module(DisposalQueue); +class DisposalOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid = 5) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(100), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } + void prepare(const Ptr& request) { createAgreement(request); } +}; +class DisposalRecipient : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class ActionDisposalHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + int queueDrops = 0; + int requestDrops = 0; + int responseDrops = 0; + int delbaDrops = 0; + int delbaSubmissions = 0; + int attempts = 0; + int beAdmissions = 0; + int beAttempts = 0; + int requestAttempts = 0; + simtime_t firstBeAttempt = SIMTIME_MAX; + simtime_t beDeadline = SIMTIME_MAX; + Ptr rtsChargedHeader; + int selectedScenario = -1; + void run(int scenario) { + Enter_Method("run"); + selectedScenario = scenario; + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new DisposalOriginator(); + auto recipient = new DisposalRecipient(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + auto queue = check_and_cast(edca->getEdcaf(AC_VO)->getPendingQueue()); + if (scenario == 9 || scenario == 10) { + queue->setCapacity(100); + queuePendingBeRequest(); + return; + } + if (scenario == 8) { + queue->setCapacity(100); + originator->accept(peer, 0); + beginTeardown(peer, 0, true, 37); + queueData("deferred-be-after-delba", 0); + ASSERT(!check_and_cast(edca->getEdcaf(AC_BE)->getSubmodule("contention"))->isContentionInProgress()); + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verifyBeResume")); + return; + } + if (scenario == 6) { + queue->setCapacity(1); + queueVoice("voice-blocker", B(1000)); + return; + } + if (scenario == 7) { + queue->setCapacity(100); + auto edcaf = edca->getEdcaf(AC_VO); + auto recovery = edcaf->getMgmtAndNonQoSRecoveryProcedure(); + auto other = makeShared(); + other->setReceiverAddress(MacAddress("02:00:00:00:00:03")); + other->setSequenceNumber(SequenceNumberCyclic(0)); + auto otherPacket = new Packet("other-peer-retry", other); + recovery->dataOrMgmtFrameTransmissionFailed(otherPacket, other, edcaf->getStationRetryCounters()); + ASSERT(recovery->getRetryCount(otherPacket, other) == 1); + queuePendingBeRequest(); + auto selected = edcaf->getInProgressFrames()->getFrameToTransmit(); + ASSERT(dynamicPtrCast(selected->peekAtFront()) != nullptr); + ASSERT(edcaf->getInProgressFrames()->getLength() == 1); + auto request = edcaf->getInProgressFrames()->getFrames(0)->peekAtFront(); + ASSERT(request->getSequenceNumber() == other->getSequenceNumber()); + ASSERT(request->getFragmentNumber() == other->getFragmentNumber()); + ASSERT(attempts == 0); + resetPeer(peer); + ASSERT(recovery->getRetryCount(otherPacket, other) == 1); + ASSERT(ownedActions.empty()); + delete otherPacket; + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + queue->setCapacity(scenario == 4 ? 1 : scenario == 5 ? 100 : 0); + handlingAgreementAction = true; + if (scenario == 0 || scenario == 4) { + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + originator->prepare(request); + queueData("deferred"); + processMgmtFrame(new Packet("request", request), request); + if (scenario == 4) { + ASSERT(queue->getNumPackets() == 1); + auto filler = makeShared(); + filler->setReceiverAddress(peer); + filler->setType(ST_PROBEREQUEST); + processUpperFrame(new Packet("filler", filler), filler); + } + ASSERT(requestDrops == 1); + ASSERT(originator->getAgreement(peer, 5) == nullptr); + ASSERT(originator->getProcedures().count({peer, 5}) == 0); + ASSERT(delbaSubmissions == 0); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 1); + } + else if (scenario == 1) { + originator->accept(peer); + auto request = makeShared(); + request->setTransmitterAddress(peer); + request->setReceiverAddress(mac->getAddress()); + request->setTid(5); + request->setDialogToken(37); + request->setSequenceNumber(SequenceNumberCyclic(10)); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("incoming-request", request); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, request); + ASSERT(responseDrops == 1); + ASSERT(recipient->getAgreement(5, peer) != nullptr); + ASSERT(originator->getAgreement(peer, 5) != nullptr); + } + else { + originator->accept(peer); + recipient->accept(peer); + if (scenario == 2 || scenario == 5) { + queueData("batch-first"); + queueData("batch-second"); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 2); + beginTeardown(peer, 5, true, 37); + if (scenario == 5) { + handlingAgreementAction = false; + updateContention(); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("delbaFailure")); + return; + } + ASSERT(originator->getAgreement(peer, 5) == nullptr); + ASSERT(recipient->getAgreement(5, peer) != nullptr); + ASSERT(originator->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::RECOVERY_NEEDED); + ASSERT(!hasQueuedData(peer, 5)); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(delbaDrops == 1); + ASSERT(delbaSubmissions == 1); + queue->setCapacity(100); + queueData("later"); + ASSERT(delbaSubmissions == 2); + ASSERT(originator->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING); + ASSERT(hasQueuedData(peer, 5)); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 1); + } + else { + beginTeardown(peer, 5, false, 37); + ASSERT(recipient->getAgreement(5, peer) == nullptr); + ASSERT(originator->getAgreement(peer, 5) != nullptr); + ASSERT(!recipient->isTeardownPending(peer, 5)); + ASSERT(delbaDrops == 1); + } + } + ASSERT(attempts == 0); + ASSERT(queueDrops == 1); + if (scenario != 2) + ASSERT(ownedActions.empty()); + handlingAgreementAction = false; + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + protected: + void handleMessage(cMessage *message) override { + if (message->isName("verifyRtsTerminal")) { + delete message; + ASSERT(rtsChargedHeader != nullptr); + ASSERT(attempts == 1); + ASSERT(requestAttempts == 0); + ASSERT(queueDrops == 0); + ASSERT(edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->getShortRetryCount(nullptr, rtsChargedHeader) == 0); + ASSERT(ownedActions.empty()); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 0) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 0}) == 0); + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + if (message->isName("resetAfterRtsFailure")) { + delete message; + auto recovery = edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure(); + ASSERT(rtsChargedHeader != nullptr); + ASSERT(recovery->getShortRetryCount(nullptr, rtsChargedHeader) == 1); + ASSERT(requestAttempts == 0); + resetPeer(peer); + ASSERT(recovery->getShortRetryCount(nullptr, rtsChargedHeader) == 0); + ASSERT(ownedActions.empty()); + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + if (message->isName("delbaAck")) { + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("delba-ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 0}) == 0); + return; + } + if (message->isName("queueDeferredBe")) { + delete message; + ASSERT(tx->hasTransmission()); + auto contention = check_and_cast(edca->getEdcaf(AC_BE)->getSubmodule("contention")); + ASSERT(!contention->isContentionInProgress()); + queuePendingBeRequest(); + ASSERT(!contention->isContentionInProgress()); + ASSERT(edca->getEdcaf(AC_VO)->getPendingQueue()->getNumPackets() == 1); + ASSERT(requestAttempts == 0); + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("evictPendingRequest")); + return; + } + if (message->isName("evictPendingRequest")) { + delete message; + auto contention = check_and_cast(edca->getEdcaf(AC_BE)->getSubmodule("contention")); + ASSERT(!contention->isContentionInProgress()); + ASSERT(tx->hasTransmission()); + queueVoice("voice-evictor", B(10)); + ASSERT(requestDrops == 1); + ASSERT(requestAttempts == 0); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 0) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 0}) == 0); + ASSERT(hasQueuedData(peer, 0)); + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verifyBeResume")); + return; + } + if (message->isName("verifyBeResume")) { + delete message; + ASSERT(beAdmissions == 1); + ASSERT(beAttempts >= 1); + ASSERT(firstBeAttempt < beDeadline); + ASSERT(simTime() < beDeadline); + ASSERT(requestAttempts == 0); + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + if (!message->isName("delbaFailure")) { + Hcf::handleMessage(message); + return; + } + delete message; + ASSERT(attempts == 1); + ASSERT(queueDrops == 0); + ASSERT(delbaSubmissions == 1); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 5) == nullptr); + ASSERT(recipientBlockAckAgreementHandler->getAgreement(5, peer) != nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::RECOVERY_NEEDED); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(!hasQueuedData(peer, 5)); + ASSERT(ownedActions.empty()); + handlingAgreementAction = true; + queueData("later-after-retry-limit"); + ASSERT(delbaSubmissions == 2); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().at({peer, 5}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 1); + handlingAgreementAction = false; + std::cout << "Block Ack Action disposal assertions passed.\n"; + endSimulation(); + } + void queueVoice(const char *name, B payload) { + auto header = makeShared(); + header->setType(ST_PROBEREQUEST); + header->setReceiverAddress(MacAddress("02:00:00:00:00:03")); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(payload)); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void queuePendingBeRequest() { + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(0); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + check_and_cast(originatorBlockAckAgreementHandler)->prepare(request); + queueData("deferred-be", 0); + processMgmtFrame(new Packet("pending-be-request", request), request); + } + void queueData(const char *name, Tid tid = 5) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(mac->getAddress()); + header->setReceiverAddress(peer); + header->setAddress3(peer); + header->setTid(tid); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + if (tid == 0) { + beAdmissions++; + beDeadline = edca->getLifetimeHandler()->getDeadline(packet); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == packetDroppedSignal && dynamic_cast(source) != nullptr) { + queueDrops++; + auto packet = check_and_cast(object); + auto header = packet->peekAtFront(); + requestDrops += dynamicPtrCast(header) != nullptr; + responseDrops += dynamicPtrCast(header) != nullptr; + delbaDrops += dynamicPtrCast(header) != nullptr; + } + Hcf::receiveSignal(source, signal, object, details); + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbaSubmissions++; + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + attempts++; + auto header = packet->peekAtFront(); + auto data = dynamicPtrCast(header); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && packet->isName("voice-blocker")) + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("queueDeferredBe")); + if (accepted && data && data->getTid() == 0) { + beAttempts++; + firstBeAttempt = std::min(firstBeAttempt, simTime()); + } + if (accepted && dynamicPtrCast(header)) + requestAttempts++; + return accepted; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + bool delba = dynamicPtrCast(header) != nullptr; + Hcf::transmissionComplete(packet, header); + if (delba && beAdmissions == 1) + scheduleAfter(SimTime(60, SIMTIME_US), new cMessage("delbaAck")); + } + void originatorProcessRtsProtectionFailed(Packet *packet) override { + Enter_Method("originatorProcessRtsProtectionFailed"); + auto request = dynamicPtrCast(packet->peekAtFront()); + Hcf::originatorProcessRtsProtectionFailed(packet); + if (request && selectedScenario == 10) { + rtsChargedHeader = request; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("verifyRtsTerminal")); + return; + } + if (request && !rtsChargedHeader) { + ASSERT(requestAttempts == 0); + ASSERT(ownedActions.count(packet->getId()) == 1); + ASSERT(!ownedActions.at(packet->getId()).attempted); + ASSERT(edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->getShortRetryCount(packet, request) == 1); + rtsChargedHeader = request; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("resetAfterRtsFailure")); + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { delete packet; } + void sendUp(const std::vector& packets) override { + for (auto packet : packets) { + take(packet); + delete packet; + } + } +}; +Define_Module(ActionDisposalHcf); +class ActionDisposalTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->run(par("scenario")); + } +}; +Define_Module(ActionDisposalTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.queue.PendingQueue; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ActionDisposalHcf extends Hcf { parameters: @class(::inet::ieee80211::ActionDisposalHcf); } +simple DisposalQueue extends PendingQueue { parameters: @class(::inet::ieee80211::DisposalQueue); } +simple ActionDisposalTest { parameters: int scenario; @class(::inet::ieee80211::ActionDisposalTest); } +network ActionDisposalNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ActionDisposalTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDisposalNetwork +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +*.test.scenario = ${scenario=0..10} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ActionDisposalHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.mgmtAndNonQoSRecoveryProcedure.shortRetryLimit = ${scenario} == 9 ? 3 : 1 +**.mgmtAndNonQoSRecoveryProcedure.longRetryLimit = 1 +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = (${scenario} == 9 || ${scenario} == 10) ? 0B : 2346B +**.wlan[*].mac.hcf.rtsPolicy.ctsTimeout = 1ms +**.wlan[*].mac.hcf.edca.edcaf[*].pendingQueue.typename = "DisposalQueue" +**.wlan[*].mac.hcf.edca.edcaf[*].pendingQueue.dropperClass = "inet::queueing::PacketAtCollectionBeginDropper" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack Action disposal assertions passed. diff --git a/tests/module/Ieee80211BlockAckAdmission_1.test b/tests/module/Ieee80211BlockAckAdmission_1.test new file mode 100644 index 00000000000..ef1c114ebfa --- /dev/null +++ b/tests/module/Ieee80211BlockAckAdmission_1.test @@ -0,0 +1,132 @@ +%description: +Check isolated profile admission through initialized production policies. +Basic permits legacy capability state. Compressed requires local and peer HT eligibility. +The fixture leaves production HCF on Basic and creates no Compressed agreement. + +%file: Admission.cc +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" + +namespace inet::ieee80211 { +class AdmissionTest : public cSimpleModule { + protected: + void checkResponse(IOriginatorBlockAckAgreementPolicy *policy) { + const MacAddress peer("02:00:00:00:00:02"); + OriginatorBlockAckAgreement agreement(peer, 5, SequenceNumberCyclic(100), 64, true, false); + agreement.setDialogToken(37); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setTid(5); + response->setDialogToken(37); + response->setStatusCode(SC_SUCCESSFUL); + response->setBlockAckPolicy(true); + response->setBufferSize(8); + ASSERT(policy->isAddbaReqAccepted(response, &agreement)); + ASSERT(!policy->isAddbaReqAccepted(response, nullptr)); + response->setDialogToken(0); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setDialogToken(38); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setDialogToken(37); + response->setTid(4); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setTid(5); + response->setTransmitterAddress(MacAddress("02:00:00:00:00:03")); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setTransmitterAddress(peer); + response->setStatusCode(SC_UNSPECIFIED); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setStatusCode(SC_SUCCESSFUL); + response->setBlockAckPolicy(false); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setBlockAckPolicy(true); + response->setBufferSize(0); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + response->setBufferSize(8); + agreement.setIsAddbaResponseReceived(true); + ASSERT(!policy->isAddbaReqAccepted(response, &agreement)); + } + + void checkRequest(IRecipientBlockAckAgreementPolicy *policy) { + auto request = makeShared(); + request->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + request->setTid(5); + request->setDialogToken(37); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBlockAckPolicy(true); + for (int size : {0, 8, 64, 128}) { + request->setBufferSize(size); + ASSERT(policy->isAddbaReqAccepted(request)); + } + request->setBlockAckPolicy(false); + ASSERT(!policy->isAddbaReqAccepted(request)); + request->setBlockAckPolicy(true); + request->setDialogToken(0); + ASSERT(!policy->isAddbaReqAccepted(request)); + request->setDialogToken(37); + request->setTransmitterAddress(MacAddress::BROADCAST_ADDRESS); + ASSERT(!policy->isAddbaReqAccepted(request)); + } + + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + auto originator = check_and_cast( + getModuleByPath("^.host.wlan[0].mac.hcf.originatorBlockAckAgreementPolicy")); + auto recipient = check_and_cast( + getModuleByPath("^.host.wlan[0].mac.hcf.recipientBlockAckAgreementPolicy")); + checkResponse(originator); + checkRequest(recipient); + for (bool localHt : {false, true}) { + for (bool peerHt : {false, true}) { + ASSERT(originator->isProfileAllowed(BlockAckVariant::BASIC, localHt, peerHt)); + ASSERT(recipient->isProfileAllowed(BlockAckVariant::BASIC, localHt, peerHt)); + ASSERT(originator->isProfileAllowed(BlockAckVariant::COMPRESSED, localHt, peerHt) == (localHt && peerHt)); + ASSERT(recipient->isProfileAllowed(BlockAckVariant::COMPRESSED, localHt, peerHt) == (localHt && peerHt)); + } + } + std::cout << "Block Ack admission assertions passed.\n"; + } +}; +Define_Module(AdmissionTest); +} + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple AdmissionTest { parameters: @class(::inet::ieee80211::AdmissionTest); } +network AdmissionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: AdmissionTest; +} + +%inifile: omnetpp.ini +[General] +network = AdmissionNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack admission assertions passed. diff --git a/tests/module/Ieee80211BlockAckAssociationReset_1.test b/tests/module/Ieee80211BlockAckAssociationReset_1.test new file mode 100644 index 00000000000..f63356732b5 --- /dev/null +++ b/tests/module/Ieee80211BlockAckAssociationReset_1.test @@ -0,0 +1,451 @@ +%description: +Check Basic agreement cleanup through AP and STA management after a real association. AP reassociation uses identical capability and rate fields. Deletion callbacks require removal of both roles before the first callback. Another peer remains unchanged. Rate callbacks require coherent committed state. The fixture supplies Block Ack agreements and controlled management completion. + +The handler's waiting phase records a completed eligible DATA transmission until another DATA packet arrives. Reassociation removes this phase. A lone DATA packet in the new association must not cause ADDBA. + +%extraargs: -r 0..15 + +%file: AssociationReset.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.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/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ResetOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(100), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class ResetRecipient : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class AssociationResetHcf : public Hcf { + public: + MacAddress selected; + const MacAddress other = MacAddress("02:00:00:00:00:09"); + int deletions = 0; + int deliveries = 0; + bool observeData = false; + bool retainRealAcks = false; + bool checkOtherProcedure = true; + int addbaRequests = 0; + int dataAttempts = 0; + int dataCompletions = 0; + void prepare(MacAddress peer) { + Enter_Method("prepare"); + selected = peer; + observeData = true; + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new ResetOriginator(); + auto recipient = new ResetRecipient(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + for (auto address : {selected, other}) { + originator->accept(address); + recipient->accept(address); + originator->beginSetupIntent(address, 6); + } + subscribe(blockAckAgreementDeletedSignal, this); + } + void receiveNormalAckFragment(int fragment, bool more) { + Enter_Method("receiveNormalAckFragment"); + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(selected); + header->setReceiverAddress(mib->address); + header->setAddress3(selected); + header->setTid(7); + header->setSequenceNumber(SequenceNumberCyclic(100)); + header->setFragmentNumber(fragment); + header->setMoreFragments(more); + header->setAckPolicy(NORMAL_ACK); + auto packet = new Packet(fragment == 0 ? "old-unit-frag0" : "new-unit-frag1", header); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + } + bool hasBoth(MacAddress peer) const { + return originatorBlockAckAgreementHandler->getAgreement(peer, 5) != nullptr && + recipientBlockAckAgreementHandler->getAgreement(5, peer) != nullptr; + } + bool hasNeither(MacAddress peer) const { + return originatorBlockAckAgreementHandler->getAgreement(peer, 5) == nullptr && + recipientBlockAckAgreementHandler->getAgreement(5, peer) == nullptr; + } + bool waitsForSuccessor() const { + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + auto it = procedures.find({selected, 6}); + return it != procedures.end() && it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::WAITING_FOR_SUCCESSOR; + } + bool hasSuccessorProcedure() const { + return originatorBlockAckAgreementHandler->getProcedures().count({selected, 6}) != 0; + } + void clearSeededSuccessorIntent() { + Enter_Method("clearSeededSuccessorIntent"); + originatorBlockAckAgreementHandler->finishProcedure(selected, 6); + } + void sendEligibleData(const char *name) { + Enter_Method("sendEligibleData"); + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(mib->address); + header->setReceiverAddress(selected); + header->setAddress3(mib->address); + header->setTid(6); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + protected: + bool transmitFrame(Packet *packet, simtime_t ifs) override { + bool accepted = Hcf::transmitFrame(packet, ifs); + auto data = dynamicPtrCast(packet->peekAtFront()); + if (accepted && data && data->getReceiverAddress() == selected && data->getTid() == 6) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + dataAttempts++; + } + return accepted; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto data = dynamicPtrCast(header); + Hcf::transmissionComplete(packet, header); + if (data && data->getReceiverAddress() == selected && data->getTid() == 6) + dataCompletions++; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + addbaRequests++; + Hcf::processMgmtFrame(packet, header); + } + void sendUp(const std::vector& packets) override { + for (auto packet : packets) { + if (observeData && dynamicPtrCast(packet->peekAtFront())) { + deliveries++; + take(packet); + delete packet; + } + else + Hcf::sendUp({packet}); + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr& header, + Packet *received, const Ptr& receivedHeader) override { + if (observeData && !retainRealAcks) + delete packet; + else + Hcf::transmitControlResponseFrame(packet, header, received, receivedHeader); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *payload, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, payload, details); + return; + } + ASSERT(payload != nullptr); + ASSERT(hasNeither(selected)); + ASSERT(hasBoth(other)); + for (const auto& item : originatorBlockAckAgreementHandler->getProcedures()) + ASSERT(item.first.first != selected); + if (checkOtherProcedure) + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({other, 6}) == 1); + if (auto originator = dynamic_cast(payload)) { + ASSERT(originator->getReceiverAddr() == selected); + ASSERT(originator->getTid() == 5); + ASSERT(originator->getBufferSize() == 64); + } + else { + auto recipient = check_and_cast(payload); + ASSERT(recipient->getBlockAckRecord()->getOriginatorAddress() == selected); + ASSERT(recipient->getBlockAckRecord()->getTid() == 5); + ASSERT(recipient->getBufferSize() == 64); + } + ASSERT(hasNeither(selected)); + deletions++; + } +}; +Define_Module(AssociationResetHcf); +class AssociationResetAp : public Ieee80211MgmtAp { + public: + Ptr response; + bool controlled = false; + void reassociate(MacAddress peer, const Ptr& original, bool acknowledged = true) { + Enter_Method("reassociate"); + controlled = true; + auto request = makeShared(); + request->setCurrentAP(mib->address); + request->setSSID(original->getSSID()); + request->setSupportedRates(original->getSupportedRates()); + request->setExtendedSupportedRatesPresent(original->getExtendedSupportedRatesPresent()); + request->setExtendedSupportedRates(original->getExtendedSupportedRates()); + request->setHtCapabilitiesPresent(original->getHtCapabilitiesPresent()); + request->setHtCapabilities(original->getHtCapabilities()); + request->setChunkLength(B(100)); + auto packet = new Packet("reassociation", request); + auto header = makeShared(); + header->setTransmitterAddress(peer); + header->setReceiverAddress(mib->address); + header->setType(ST_REASSOCIATIONREQUEST); + handleReassociationRequestFrame(packet, header); + ASSERT(response->getStatusCode() == SC_SUCCESSFUL); + Packet acknowledgment("response-ack"); + auto responseHeader = makeShared(); + responseHeader->setType(ST_REASSOCIATIONRESPONSE); + responseHeader->setReceiverAddress(peer); + acknowledgment.insertAtBack(responseHeader); + acknowledgment.addTag()->setTransactionId(staList.at(peer).pendingAssociationTransactionId); + frameTransmissionFinished(&acknowledgment, acknowledged ? FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED : FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + } + void disassociatePeer(MacAddress peer) { + Enter_Method("disassociatePeer"); + auto packet = new Packet("disassociation", makeShared()); + auto header = makeShared(); + header->setTransmitterAddress(peer); + header->setType(ST_DISASSOCIATION); + handleDisassociationFrame(packet, header); + } + protected: + void sendManagementFrame(const char *name, const Ptr& body, int subtype, + const MacAddress& peer, uint64_t transactionId = 0) override { + if (subtype == ST_ASSOCIATIONRESPONSE || subtype == ST_REASSOCIATIONRESPONSE) + response = dynamicPtrCast(body); + if (!controlled) + Ieee80211MgmtAp::sendManagementFrame(name, body, subtype, peer, transactionId); + } +}; +Define_Module(AssociationResetAp); +class AssociationResetSta : public Ieee80211MgmtSta { + public: + Ptr request; + bool controlled = false; + void reassociate(const Ptr& response, bool accepted = true) { + Enter_Method("reassociate"); + controlled = true; + auto peer = assocAP.address; + auto ap = lookupAP(peer); + ASSERT(ap != nullptr); + startReassociation(ap, SimTime(1, SIMTIME_MS)); + auto body = makeShared(*response); + if (!accepted) + body->setStatusCode(SC_UNSUP_CAP); + auto packet = new Packet("reassociation-response", body); + physicallayer::Ieee80211Channel channel(&physicallayer::Ieee80211CompliantBands::band2_4GHz, 0); + packet->addTag()->setChannel(&channel); + auto header = makeShared(); + header->setTransmitterAddress(peer); + header->setType(ST_REASSOCIATIONRESPONSE); + handleReassociationResponseFrame(packet, header); + ASSERT(mib->bssStationData.isAssociated == accepted); + } + void disassociatePeer() { + Enter_Method("disassociatePeer"); + auto packet = new Packet("disassociation", makeShared()); + auto header = makeShared(); + header->setTransmitterAddress(assocAP.address); + header->setType(ST_DISASSOCIATION); + handleDisassociationFrame(packet, header); + } + protected: + void sendManagementFrame(const char *name, const Ptr& body, int subtype, + const MacAddress& peer) override { + if (subtype == ST_ASSOCIATIONREQUEST) + request = dynamicPtrCast(body); + if (!controlled) + Ieee80211MgmtSta::sendManagementFrame(name, body, subtype, peer); + } +}; +Define_Module(AssociationResetSta); +class AssociationResetTest : public cSimpleModule, public cListener { + Ieee80211Mib *observed = nullptr; + MacAddress observedPeer; + bool expectPresent = true; + int notifications = 0; + protected: + void initialize() override { scheduleAt(SimTime(500, SIMTIME_MS), new cMessage("test")); } + void receiveSignal(cComponent *, simsignal_t, bool, cObject *) override { + const auto *rates = observed->findPeerRateSet(observedPeer); + const auto *ht = observed->findPeerHtState(observedPeer); + if (rates != nullptr) { + ASSERT(rates != nullptr && rates->operational.known); + ASSERT((ht != nullptr) == observed->isHtOperationSupported()); + } + else { + ASSERT(rates == nullptr); + ASSERT(ht == nullptr); + } + notifications++; + } + void handleMessage(cMessage *message) override { + if (message->isName("checkWaitingPhase")) { + delete message; + auto hcf = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + auto ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + auto sta = check_and_cast(getModuleByPath("^.sta.wlan[0].mgmt")); + ASSERT(hcf->dataAttempts == 1); + ASSERT(hcf->dataCompletions == 1); + ASSERT(hcf->addbaRequests == 0); + ASSERT(hcf->waitsForSuccessor()); + ap->reassociate(observedPeer, sta->request); + ASSERT(!hcf->hasSuccessorProcedure()); + ASSERT(hcf->deletions == 2); + hcf->sendEligibleData("new-association-first"); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("checkNewAssociationData")); + return; + } + if (message->isName("checkNewAssociationData")) { + delete message; + auto hcf = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + ASSERT(hcf->dataAttempts == 2); + ASSERT(hcf->dataCompletions == 2); + ASSERT(hcf->addbaRequests == 0); + ASSERT(hcf->waitsForSuccessor()); + ASSERT(hcf->hasNeither(observedPeer)); + ASSERT(hcf->hasBoth(hcf->other)); + std::cout << "Block Ack association-reset assertions passed.\n"; + endSimulation(); + } + delete message; + auto ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + auto sta = check_and_cast(getModuleByPath("^.sta.wlan[0].mgmt")); + auto apMib = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + auto staMib = check_and_cast(getModuleByPath("^.sta.wlan[0].mib")); + ASSERT(staMib->bssStationData.isAssociated); + ASSERT(ap->response != nullptr && sta->request != nullptr); + int scenario = par("scenario"); + bool useAp = scenario < 2 || scenario == 4 || scenario == 6 || scenario == 7; + auto hcf = check_and_cast(getModuleByPath(useAp ? "^.ap.wlan[0].mac.hcf" : "^.sta.wlan[0].mac.hcf")); + observed = useAp ? apMib : staMib; + observedPeer = useAp ? staMib->address : apMib->address; + observed->subscribe(Ieee80211Mib::rateStateChangedSignal, this); + hcf->prepare(observedPeer); + if (scenario == 7) { + auto peerHcf = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.hcf")); + peerHcf->observeData = true; + peerHcf->retainRealAcks = true; + hcf->retainRealAcks = true; + hcf->checkOtherProcedure = false; + hcf->clearSeededSuccessorIntent(); + hcf->sendEligibleData("old-association-first"); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("checkWaitingPhase")); + return; + } + expectPresent = scenario == 0 || scenario == 2 || scenario == 4 || scenario == 6; + if (scenario == 6) { + ASSERT(observed->findPeerHtState(observedPeer) != nullptr || !observed->isHtOperationSupported()); + hcf->receiveNormalAckFragment(0, true); + ASSERT(hcf->deliveries == 0); + ap->reassociate(staMib->address, sta->request); + hcf->receiveNormalAckFragment(1, false); + ASSERT(hcf->deliveries == 0); + } + else if (scenario == 0) + ap->reassociate(staMib->address, sta->request); + else if (scenario == 1) + ap->disassociatePeer(staMib->address); + else if (scenario == 2) + sta->reassociate(ap->response); + else if (scenario == 3) + sta->disassociatePeer(); + else if (scenario == 4) + ap->reassociate(staMib->address, sta->request, false); + else + sta->reassociate(ap->response, false); + ASSERT(hcf->deletions == (scenario == 4 ? 0 : 2)); + if (scenario == 4) + ASSERT(hcf->hasBoth(observedPeer)); + else + ASSERT(hcf->hasNeither(observedPeer)); + ASSERT(hcf->hasBoth(hcf->other)); + ASSERT((observed->findPeerRateSet(observedPeer) != nullptr) == expectPresent); + ASSERT((observed->findPeerHtState(observedPeer) != nullptr) == + (expectPresent && observed->isHtOperationSupported())); + if (scenario == 0 || scenario == 4 || scenario == 6) + ASSERT(notifications == 0); + std::cout << "Block Ack association-reset assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(AssociationResetTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtSta; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module AssociationResetHcf extends Hcf { parameters: @class(::inet::ieee80211::AssociationResetHcf); } +simple AssociationResetAp extends Ieee80211MgmtAp { parameters: @class(::inet::ieee80211::AssociationResetAp); } +simple AssociationResetSta extends Ieee80211MgmtSta { parameters: @class(::inet::ieee80211::AssociationResetSta); } +simple AssociationResetTest { parameters: int scenario; @class(::inet::ieee80211::AssociationResetTest); } +network AssociationResetNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta: WirelessHost; + test: AssociationResetTest; +} + +%inifile: omnetpp.ini +[General] +network = AssociationResetNetwork +ned-path = .;../../../../src +sim-time-limit = 505ms +seed-set = 0 +*.test.scenario = ${0..7} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.ap.wlan[0].mgmt.typename = "AssociationResetAp" +*.sta.wlan[0].mgmt.typename = "AssociationResetSta" +*.sta.wlan[0].agent.startingTime = 0s +*.sta.wlan[0].agent.channelsToScan = "0" +*.sta.wlan[0].agent.probeDelay = 1ms +*.sta.wlan[0].agent.minChannelTime = 10ms +*.sta.wlan[0].agent.maxChannelTime = 20ms +*.ap.wlan[0].address = "02:00:00:00:00:01" +*.sta.wlan[0].address = "02:00:00:00:00:02" +**.wlan[0].opMode = ${"g(mixed)","n(mixed-2.4Ghz)"} +**.wlan[0].mac.qosStation = true +**.wlan[0].mac.hcf.typename = "AssociationResetHcf" +**.wlan[0].mac.hcf.isBlockAckSupported = true +**.wlan[0].mac.hcf.blockAckVariant = "basic" +**.wlan[0].radio.bandName = "2.4 GHz" +**.wlan[0].radio.channelNumber = 0 +**.wlan[0].radio.transmitter.power = 100mW +**.wlan[0].radio.receiver.sensitivity = -85dBm +**.wlan[0].radio.receiver.snirThreshold = 4dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 10m +**.mobility.constraintAreaMaxY = 10m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +Block Ack association-reset assertions passed. diff --git a/tests/module/Ieee80211BlockAckCompletionCost_1.test b/tests/module/Ieee80211BlockAckCompletionCost_1.test new file mode 100644 index 00000000000..358aee3883e --- /dev/null +++ b/tests/module/Ieee80211BlockAckCompletionCost_1.test @@ -0,0 +1,210 @@ +%description: +Measure production Basic setup and transfer completion with equal UDP payload totals. +One run sends twelve packets continuously. The other run sends three bursts of four packets. +Both runs use seed zero, 6 Mbit/s, zero inactivity timeout, and a fixed 100 ms interval. +The measurements describe these traffic shapes. They do not estimate agreement-reuse performance. + +%extraargs: -r 0..1 + +%file: CompletionCost.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/common/TimeTag_m.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class CompletionCostHcf : public Hcf { + simtime_t deferredAt = -1; + simtime_t requestQueuedAt = -1; + simtime_t requestSentAt = -1; + simtime_t delbaQueuedAt = -1; + public: + simtime_t deferral; + simtime_t requestContention; + simtime_t responseAfterRequest; + simtime_t delbaCompletion; + protected: + void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& header, AccessCategory ac) override { + Hcf::originatorProcessTransmittedDataFrame(packet, header, ac); + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + if (procedures.count({header->getReceiverAddress(), header->getTid()}) && deferredAt < 0) + deferredAt = simTime(); + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + requestQueuedAt = simTime(); + if (dynamicPtrCast(header)) + delbaQueuedAt = simTime(); + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto request = dynamicPtrCast(packet->peekAtFront()); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && request) { + requestContention += simTime() + ifs - requestQueuedAt; + requestSentAt = simTime() + ifs; + } + return accepted; + } + void processLowerFrame(Packet *packet, const Ptr& header) override { + bool response = dynamicPtrCast(header) != nullptr; + Hcf::processLowerFrame(packet, header); + if (response && deferredAt >= 0) { + deferral += simTime() - deferredAt; + responseAfterRequest += simTime() - requestSentAt; + deferredAt = -1; + } + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + bool delba = dynamicPtrCast(transmitted->peekAtFront()) != nullptr; + Hcf::originatorProcessReceivedFrame(packet, transmitted); + if (delba) + delbaCompletion += simTime() - delbaQueuedAt; + } +}; +Define_Module(CompletionCostHcf); +class CompletionCostTest : public cSimpleModule, public cListener { + cModule *sender = nullptr; + cModule *sink = nullptr; + int sent = 0; + int received = 0; + int requests = 0; + int responses = 0; + int delbas = 0; + int drops = 0; + int64_t payloadBytes = 0; + simtime_t managementAirtime; + simtime_t deliveryDelay; + protected: + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + auto packet = check_and_cast(value); + if (source == sender) { + sent++; + return; + } + if (source == sink) { + received++; + payloadBytes += packet->getByteLength(); + auto creation = packet->peekAtFront()->getTag()->getCreationTime(); + deliveryDelay += simTime() - creation; + return; + } + if (signal == packetDroppedSignal) { + drops++; + return; + } + auto header = packet->peekAtFront(); + if (dynamicPtrCast(header)) + requests++; + if (dynamicPtrCast(header)) + responses++; + if (dynamicPtrCast(header)) + delbas++; + if (dynamicPtrCast(header)) + managementAirtime += packet->getTag()->getMode()->getDuration(packet->getDataLength()); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + sender = getModuleByPath("^.host1.app[0]"); + sink = getModuleByPath("^.host2.app[0]"); + sender->subscribe(packetSentSignal, this); + sink->subscribe(packetReceivedSignal, this); + for (const char *path : {"^.host1.wlan[0].mac.hcf", "^.host2.wlan[0].mac.hcf"}) { + getModuleByPath(path)->subscribe(packetSentToPeerSignal, this); + getModuleByPath(path)->subscribe(packetDroppedSignal, this); + } + scheduleAt(SimTime(99, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + auto hcf = check_and_cast(getModuleByPath("^.host1.wlan[0].mac.hcf")); + std::cout << "Completion cost: gaps=" << par("gaps").boolValue() << " sent=" << sent + << " received=" << received << " bytes=" << payloadBytes << " requests=" << requests + << " responses=" << responses << " delbas=" << delbas << " drops=" << drops + << " managementAirtime=" << managementAirtime << " deferral=" << hcf->deferral + << " requestContention=" << hcf->requestContention << " responseAfterRequest=" << hcf->responseAfterRequest + << " delbaCompletion=" << hcf->delbaCompletion << " deliveryDelaySum=" << deliveryDelay + << " goodputBps=" << payloadBytes * 8 / 0.1 << "\n"; + ASSERT(sent == 12 && received == 12 && payloadBytes == 8400 && drops == 0); + ASSERT(requests == (par("gaps").boolValue() ? 3 : 1)); + ASSERT(responses == requests && delbas == requests); + std::cout << "Block Ack completion cost assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(CompletionCostTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.AdhocHost; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module CompletionCostHcf extends Hcf { parameters: @class(::inet::ieee80211::CompletionCostHcf); } +simple CompletionCostTest { parameters: bool gaps; @class(::inet::ieee80211::CompletionCostTest); } +network CompletionCostNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: AdhocHost; + host2: AdhocHost; + test: CompletionCostTest; +} + +%inifile: omnetpp.ini +[General] +network = CompletionCostNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.gaps = ${gaps=false,true} +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicBurst" +*.host1.app[0].destAddresses = "host2" +*.host1.app[0].destPort = 1000 +*.host1.app[0].messageLength = 700B +*.host1.app[0].chooseDestAddrMode = "once" +*.host1.app[0].startTime = 10ms +*.host1.app[0].stopTime = ${gaps} ? 52ms : 11.15ms +*.host1.app[0].sendInterval = 100us +*.host1.app[0].burstDuration = ${gaps} ? 350us : 0s +*.host1.app[0].sleepDuration = ${gaps} ? 20ms : 0s +*.host2.numApps = 1 +*.host2.app[0].typename = "UdpSink" +*.host2.app[0].localPort = 1000 +*.host*.ipv4.arp.typename = "GlobalArp" +*.host*.wlan[*].opMode = "a" +*.host*.wlan[*].bitrate = 6Mbps +*.host*.wlan[*].classifier.typename = "QosClassifier" +*.host*.wlan[*].mac.qosStation = true +*.host*.wlan[*].mac.hcf.typename = "CompletionCostHcf" +*.host*.wlan[*].mac.hcf.isBlockAckSupported = true +*.host*.wlan[*].mac.hcf.blockAckVariant = "basic" +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +*.host*.wlan[*].radio.receiver.energyDetection = -105dBm +*.host*.wlan[*].radio.receiver.sensitivity = -105dBm +*.host*.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.host1.mobility.initialX = 0m +*.host2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack completion cost assertions passed. diff --git a/tests/module/Ieee80211BlockAckDataSelection_1.test b/tests/module/Ieee80211BlockAckDataSelection_1.test new file mode 100644 index 00000000000..534e93fb524 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDataSelection_1.test @@ -0,0 +1,321 @@ +%description: +Check DATA extraction through the production QoS data service. +A compound queue uses reversed service order and a multicast/unicast classifier. +The service must extract unicast DATA individually without a multicast aggregate. +The multicast sources must retain their identities, sequence numbers, and deadlines. +Other cases use a genuine compound queue with a provider without selected extraction. +Ordinary service and existence queries must work without that optional capability. + +%extraargs: -r 0..3 + +%file: DataSelection.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/queueing/base/PacketQueueBase.h" +#include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/common/PacketEventTag.h" +#include "inet/common/TimeTag.h" + +using namespace inet; +using namespace inet::ieee80211; + +class LegacySelectionQueue : public queueing::PacketQueueBase { + cPacketQueue frames; + public: + int getMaxNumPackets() const override { return -1; } + int getNumPackets() const override { return frames.getLength(); } + b getMaxTotalLength() const override { return b(-1); } + b getTotalLength() const override { return b(frames.getBitLength()); } + Packet *getPacket(int index) const override { return check_and_cast(frames.get(index)); } + bool isEmpty() const override { return frames.isEmpty(); } + void removePacket(Packet *frame) override { frames.remove(frame); } + void removeAllPackets() override { frames.clear(); } + bool supportsPacketPushing(const cGate *) const override { return true; } + bool supportsPacketPulling(const cGate *) const override { return true; } + bool canPushPacket(Packet *, const cGate *) const override { return true; } + void pushPacket(Packet *frame, const cGate *) override { frames.insert(frame); } + Packet *canPullPacket(const cGate *) const override { return isEmpty() ? nullptr : getPacket(0); } + Packet *pullPacket(const cGate *) override { return check_and_cast(frames.pop()); } + void enqueuePacket(Packet *frame) override { frames.insert(frame); } + Packet *dequeuePacket() override { return check_and_cast(frames.pop()); } +}; +Define_Module(LegacySelectionQueue); + +class CompatibilityCompoundQueue : public queueing::CompoundPacketQueueBase { + public: + bool unrelatedFailure = false; + Packet *peekEligiblePacket(const std::function& predicate) const override { + if (unrelatedFailure) + throw cRuntimeError("Unrelated fixture query failure"); + return CompoundPacketQueueBase::peekEligiblePacket(predicate); + } +}; +Define_Module(CompatibilityCompoundQueue); + +class DataSelectionHcf : public Hcf { + public: + bool queued(MacAddress peer, Tid tid) const { return hasQueuedData(peer, tid); } +}; +Define_Module(DataSelectionHcf); + +class DataSelectionTest : public cSimpleModule, public cListener { + int pulls = 0; + int removals = 0; + int aggregates = 0; + protected: + void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { + if (signal == packetPulledSignal) pulls++; + if (signal == packetRemovedSignal) removals++; + if (signal == IOriginatorMacDataService::packetAggregatedSignal) aggregates++; + } + Packet *makeFrame(const char *name, MacAddress receiver) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(0); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(receiver); + auto frame = new Packet(name, header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + auto timeTag = frame->addRegionTag(); + timeTag->setPacketTotalTimesArraySize(1); + timeTag->setPacketTotalTimes(0, SIMTIME_ZERO); + frame->addRegionTag(); + return frame; + } + Packet *accept(Hcf *hcf, const char *name, MacAddress receiver) { + auto frame = makeFrame(name, receiver); + hcf->processUpperFrame(frame, frame->peekAtFront()); + return frame; + } + void activity() override { + auto mac = getModuleByPath("^.host.wlan[0].mac"); + auto hcf = check_and_cast(mac->getSubmodule("hcf")); + auto edca = check_and_cast(hcf->getSubmodule("edca")); + auto queue = edca->getEdcaf(AC_BE)->getPendingQueue(); + auto lifetime = edca->getLifetimeHandler(); + auto service = check_and_cast(hcf->getSubmodule("originatorMacDataService")); + auto queueModule = check_and_cast(queue); + check_and_cast(mac->getSubmodule("rx"))->setContentionBlocked(true); + int scenario = par("scenario"); + if (scenario != 0) { + MacAddress firstPeer("02:00:00:00:00:02"); + MacAddress secondPeer("02:00:00:00:00:03"); + Packet *head; + Packet *tail; + if (scenario == 1) { + head = accept(hcf, "ordinary-head", firstPeer); + tail = accept(hcf, "ordinary-tail", secondPeer); + ASSERT(hcf->queued(firstPeer, 0)); + ASSERT(hcf->queued(secondPeer, 0)); + ASSERT(!hcf->queued(firstPeer, 5)); + service->setEligibility([](Packet *) { return true; }, [](Packet *) { return false; }); + ASSERT(service->peekEligibleFrame(queue) == head); + auto ordinary = service->extractFramesToTransmit(queue); + ASSERT(ordinary != nullptr && ordinary->size() == 1 && ordinary->front() == head); + ASSERT(queue->getNumPackets() == 1 && queue->getPacket(0) == tail); + ASSERT(head->getOwner() == service); + ASSERT(head->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(0)); + lifetime->forgetFrame(head->getId()); + take(head); + delete head; + delete ordinary; + auto composite = check_and_cast(queue); + composite->unrelatedFailure = true; + bool propagated = false; + try { hcf->queued(firstPeer, 0); } + catch (const cRuntimeError& error) { + propagated = std::string(error.what()).find("Unrelated fixture query failure") != std::string::npos; + } + ASSERT(propagated); + } + else { + head = makeFrame("deferred-head", firstPeer); + tail = makeFrame("eligible-tail", secondPeer); + queue->enqueuePacket(head); + queue->enqueuePacket(tail); + lifetime->registerFrame(head, AC_BE); + lifetime->registerFrame(tail, AC_BE); + service->setEligibility([scenario, tail](Packet *frame) { return scenario == 3 && frame == tail; }, + [](Packet *) { return false; }); + if (scenario == 2) { + ASSERT(service->peekEligibleFrame(queue) == nullptr); + ASSERT(service->extractFramesToTransmit(queue) == nullptr); + ASSERT(hcf->queued(secondPeer, 0)); + } + else { + ASSERT(hcf->queued(secondPeer, 0)); + bool refused = false; + try { service->extractFramesToTransmit(queue); } + catch (const cRuntimeError& error) { + refused = std::string(error.what()).find("Deferred DATA hides an eligible frame") != std::string::npos; + } + ASSERT(refused); + } + ASSERT(queue->getNumPackets() == 2 && queue->getPacket(0) == head && queue->getPacket(1) == tail); + ASSERT(head->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime->getDeadline(head) == SimTime(512, SIMTIME_MS)); + } + ASSERT(tail->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime->getDeadline(tail) == SimTime(512, SIMTIME_MS)); + std::cout << "Compound compatibility scenario " << scenario << " passed.\n"; + endSimulation(); + } + queueModule->subscribe(packetPulledSignal, this); + queueModule->subscribe(packetRemovedSignal, this); + service->subscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + auto first = accept(hcf, "multicast-first", MacAddress("01:00:5e:00:00:01")); + auto second = accept(hcf, "multicast-second", MacAddress("01:00:5e:00:00:01")); + auto selected = accept(hcf, "selected-unicast", MacAddress("02:00:00:00:00:02")); + auto deadline = lifetime->getDeadline(first); + ASSERT(deadline == SimTime(512, SIMTIME_MS)); + ASSERT(queue->getPacket(0) == first); + ASSERT(service->peekEligibleFrame(queue) == selected); + ASSERT(service->getNextSequenceNumber(MacAddress("02:00:00:00:00:02"), 0) == SequenceNumberCyclic(0)); + auto previewService = check_and_cast(service); + auto frames = edca->getEdcaf(AC_BE)->getInProgressFrames(); + auto owner = selected->getOwner(); + ASSERT(previewService->peekEligibleFrame(queue) == selected); + ASSERT(frames->peekFrameToTransmit() == selected); + ASSERT(frames->getCandidateSequenceNumber(selected) == SequenceNumberCyclic(0)); + ASSERT(frames->getLength() == 0 && selected->getOwner() == owner); + ASSERT(selected->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + wait(SimTime(100, SIMTIME_MS)); + auto extracted = service->extractFramesToTransmit(queue); + ASSERT(extracted != nullptr && extracted->size() == 1); + ASSERT(extracted->front() == selected); + ASSERT(selected->getOwner() == service); + auto header = selected->peekAtFront(); + ASSERT(!header->getAMsduPresent()); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(queue->getNumPackets() == 2); + ASSERT(queue->getPacket(0) == first && queue->getPacket(1) == second); + for (auto source : {first, second}) { + ASSERT(source->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime->getDeadline(source) == deadline); + ASSERT(lifetime->findFrame(source->getId()) == source); + ASSERT(source->findRegionTag()->getPacketTotalTimes(0) == SIMTIME_ZERO); + ASSERT(source->findRegionTag()->getPacketEventsArraySize() == 0); + } + auto times = selected->getAllRegionTags(); + ASSERT(times.size() == 1); + ASSERT(times.front().getTag()->getPacketTotalTimes(0) == SimTime(100, SIMTIME_MS)); + auto eventRegions = selected->getAllRegionTags(); + ASSERT(eventRegions.size() == 1); + auto events = eventRegions.front().getTag(); + ASSERT(events->getPacketEventsArraySize() == 1); + ASSERT(events->getPacketEvents(0)->getKind() == PEK_QUEUED); + ASSERT(events->getPacketEvents(0)->getPacketDuration() == SimTime(100, SIMTIME_MS)); + ASSERT(lifetime->getDeadline(selected) == deadline); + ASSERT(lifetime->getNumFrames() == 3); + ASSERT(pulls == 3); + ASSERT(removals == 0 && aggregates == 0); + lifetime->forgetFrame(selected->getId()); + take(selected); + delete selected; + delete extracted; + queueModule->unsubscribe(packetPulledSignal, this); + queueModule->unsubscribe(packetRemovedSignal, this); + service->unsubscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + auto legacy = check_and_cast(getModuleByPath("^.legacy")); + auto head = first->dup(); + auto tail = first->dup(); + legacy->enqueuePacket(head); + legacy->enqueuePacket(tail); + service->setEligibility([](Packet *) { return false; }, [](Packet *) { return false; }); + ASSERT(service->extractFramesToTransmit(legacy) == nullptr); + ASSERT(legacy->getNumPackets() == 2 && legacy->getPacket(0) == head); + service->setEligibility([tail](Packet *frame) { return frame == tail; }, [](Packet *) { return false; }); + bool refused = false; + try { service->extractFramesToTransmit(legacy); } + catch (const cRuntimeError& error) { + refused = std::string(error.what()).find("ISelectivePacketCollection") != std::string::npos; + } + ASSERT(refused); + ASSERT(legacy->getNumPackets() == 2 && legacy->getPacket(0) == head && legacy->getPacket(1) == tail); + service->setEligibility([](Packet *) { return true; }, [](Packet *) { return false; }); + auto ordinary = service->extractFramesToTransmit(legacy); + ASSERT(ordinary != nullptr && ordinary->size() == 1 && ordinary->front() == head); + ASSERT(legacy->getNumPackets() == 1 && legacy->getPacket(0) == tail); + take(ordinary->front()); + delete ordinary->front(); + delete ordinary; + std::cout << "Block Ack DATA selection assertions passed.\n"; + endSimulation(); + } + public: + DataSelectionTest() : cSimpleModule(65536) {} +}; +Define_Module(DataSelectionTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.queueing.base.PacketQueueBase; +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +simple LegacySelectionQueue extends PacketQueueBase { parameters: @class(::LegacySelectionQueue); } +module CompatibilityCompoundQueue extends CompoundPacketQueueBase { + parameters: + @class(::CompatibilityCompoundQueue); + submodules: + provider: LegacySelectionQueue; + connections: + in --> provider.in; + provider.out --> out; +} +module DataSelectionHcf extends Hcf { parameters: @class(::DataSelectionHcf); } +simple DataSelectionTest { parameters: int scenario; @class(::DataSelectionTest); } +network DataSelectionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + legacy: LegacySelectionQueue; + test: DataSelectionTest; +} + +%inifile: omnetpp.ini +[General] +network = DataSelectionNetwork +sim-time-limit = 1s +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0..3} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.isBlockAckSupported = false +**.wlan[*].mac.hcf.typename = "DataSelectionHcf" +**.wlan[*].mac.hcf.edca.edcaf[*].pendingQueue.typename = ${queueType="CompoundPendingQueue","CompatibilityCompoundQueue","CompatibilityCompoundQueue","CompatibilityCompoundQueue" ! scenario} +**.pendingQueue.classifier.classifierClass = "inet::ieee80211::MgmtOverMulticastOverUnicastClassifier" +**.pendingQueue.prioritizer.reverseOrder = true +**.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.originatorMacDataService.msduAggregationPolicy.qOsCheck = true +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack DATA selection assertions passed. + +%contains: stdout +Compound compatibility scenario 3 passed. diff --git a/tests/module/Ieee80211BlockAckDeferral_1.test b/tests/module/Ieee80211BlockAckDeferral_1.test new file mode 100644 index 00000000000..b1d38cbbfa9 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -0,0 +1,412 @@ +%description: +Check Basic setup deferral through wireless DATA, ADDBA, ACK, and DELBA exchanges. +The receiver holds its actual response queue until 150 ms after request ACK. +The 100 ms default must expire; a 250 ms timeout must accept the delayed response. +Other runs check first-DATA numbering, deferred voice versus eligible video, and genuine collision. +Request delays of 100 ms and 450 ms reverse the response deadline and DATA lifetime order. +Newer DATA must preserve the response deadline after the original DATA expires. +RTS failure and lifetime expiry must retire an agreement before its first DATA transmission. +Spaced DATA must start setup before later extraction after an earlier eligible DATA completes. +The same rule applies when greedy A-MSDU aggregation empties the earlier queue. +All runs use seed 0 and zero contention windows. + +%extraargs: -r 0..11 + +%file: Deferral.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" + +using namespace inet; +using namespace inet::ieee80211; + +class DeferralHcf : public Hcf { + public: + int requests = 0; + int delbas = 0; + int dataVoice = 0; + int dataVideo = 0; + int dataAfterRequest = 0; + int dataWithAgreement = 0; + int dataAfterDelba = 0; + int aggregatesAccepted = 0; + int requestStart = -1; + int firstData = -1; + simtime_t requestAccepted = SIMTIME_MAX; + simtime_t requestAck = SIMTIME_MAX; + simtime_t observedDeadline = SIMTIME_MAX; + simtime_t responseQueued = SIMTIME_MAX; + simtime_t responseAccepted = SIMTIME_MAX; + simtime_t responseReceived = SIMTIME_MAX; + bool holdResponse = false; + bool holdRequest = false; + bool dropProtectedCts = false; + int droppedCts = 0; + int firstRtsSequence = -1; + int rtsFailures = 0; + bool dataEligible(Packet *frame) const { return isFrameEligible(frame, false); } + bool hasAgreement(Tid tid) const { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(MacAddress("02:00:00:00:00:02"), tid); + return agreement && agreement->getIsAddbaResponseReceived(); + } + bool deferred(Tid tid) const { + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + return procedures.count({MacAddress("02:00:00:00:00:02"), tid}) != 0; + } + bool contending(AccessCategory ac) const { + return check_and_cast(edca->getEdcaf(ac)->getSubmodule("contention"))->isContentionInProgress(); + } + void processMgmtFrame(Packet *frame, const Ptr& header) override { + if (dynamicPtrCast(header) && responseQueued == SIMTIME_MAX) { + responseQueued = simTime(); + if (holdResponse) + rx->setContentionBlocked(true); + } + Hcf::processMgmtFrame(frame, header); + } + protected: + void transmitControlResponseFrame(Packet *response, const Ptr& responseHeader, + Packet *received, const Ptr& receivedHeader) override { + if (dropProtectedCts && droppedCts == 0 && responseAccepted != SIMTIME_MAX && dynamicPtrCast(receivedHeader)) { + droppedCts++; + delete response; + return; + } + Hcf::transmitControlResponseFrame(response, responseHeader, received, receivedHeader); + } + void originatorProcessRtsProtectionFailed(Packet *frame) override { + if (requestStart >= 0) { + auto data = dynamicPtrCast(frame->peekAtFront()); + if (data && data->getTid() == 6) { + rtsFailures++; + if (firstRtsSequence < 0) + firstRtsSequence = data->getSequenceNumber().get(); + } + } + Hcf::originatorProcessRtsProtectionFailed(frame); + } + void frameSequenceFinished() override { + Hcf::frameSequenceFinished(); + if (holdRequest && requestAccepted == SIMTIME_MAX && deferred(6)) + rx->setContentionBlocked(true); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + take(frame); + delete frame; + } + } + bool transmitFrame(Packet *frame, simtime_t ifs) override { + auto header = frame->peekAtFront(); + bool accepted = Hcf::transmitFrame(frame, ifs); + if (!accepted) + return false; + if (auto request = dynamicPtrCast(header)) { + requests++; + if (requestStart < 0) { + requestStart = request->getStartingSequenceNumber().get(); + requestAccepted = simTime(); + ASSERT(observedDeadline == SIMTIME_MAX); + } + } + if (dynamicPtrCast(header)) + delbas++; + if (dynamicPtrCast(header) && responseAccepted == SIMTIME_MAX) + responseAccepted = simTime(); + if (auto data = dynamicPtrCast(header)) { + if (data->getAMsduPresent()) aggregatesAccepted++; + if (data->getTid() == 6) dataVoice++; + if (data->getTid() == 5) dataVideo++; + if (requestStart >= 0 && data->getTid() == 6) { + if (hasAgreement(6)) + dataWithAgreement++; + if (delbas > 0 && !hasAgreement(6)) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + dataAfterDelba++; + } + if (firstData < 0) firstData = data->getSequenceNumber().get(); + dataAfterRequest++; + } + } + return true; + } + void originatorProcessReceivedControlFrame(Packet *frame, const Ptr& header, + Packet *last, const Ptr& lastHeader, AccessCategory ac) override { + bool request = dynamicPtrCast(lastHeader) != nullptr; + Hcf::originatorProcessReceivedControlFrame(frame, header, last, lastHeader, ac); + if (request && requestAck == SIMTIME_MAX) { + requestAck = simTime(); + auto it = originatorBlockAckAgreementHandler->getProcedures().find({MacAddress("02:00:00:00:00:02"), 6}); + if (it != originatorBlockAckAgreementHandler->getProcedures().end()) + observedDeadline = it->second.responseDeadline; + } + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + if (dynamicPtrCast(header) && responseReceived == SIMTIME_MAX) + responseReceived = simTime(); + Hcf::recipientProcessReceivedManagementFrame(header); + } +}; +Define_Module(DeferralHcf); + +class DeferralTest : public cSimpleModule, public cListener { + int collisions = 0; + int videoRetryDrops = 0; + int lifetimeDrops = 0; + protected: + void receiveSignal(cComponent *, simsignal_t, unsigned long value, cObject *) override { collisions += value; } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + auto drop = check_and_cast(details); + auto header = dynamicPtrCast(check_and_cast(object)->peekAtFront()); + if (drop->getReason() == RETRY_LIMIT_REACHED && header && header->getTid() == 5) + videoRetryDrops++; + if (drop->getReason() == LIFETIME_EXPIRED) + lifetimeDrops++; + } + Packet *accept(DeferralHcf *hcf, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(header->getReceiverAddress()); + auto frame = new Packet(tid == 6 ? "voice-data" : "video-data", header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + hcf->processUpperFrame(frame, header); + return frame; + } + void activity() override { + auto sender = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf")); + auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); + auto senderRx = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.rx")); + auto receiverRx = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.rx")); + int scenario = par("scenario"); + sender->subscribe(Hcf::edcaCollisionDetectedSignal, this); + sender->subscribe(packetDroppedSignal, this); + senderRx->setContentionBlocked(true); + if (scenario >= 10) { + auto edca = check_and_cast(sender->getSubmodule("edca")); + accept(sender, 6); + if (scenario == 11) + accept(sender, 6); + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + ASSERT(sender->dataVoice == 1); + ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 1 : 0)); + ASSERT(sender->requests == 0); + ASSERT(edca->getEdcaf(AC_VO)->getPendingQueue()->isEmpty()); + ASSERT(edca->getEdcaf(AC_VO)->getInProgressFrames()->getLength() == 0); + auto later = accept(sender, 6); + bool deferredBeforeExtraction = !sender->dataEligible(later); + if (scenario == 11) + accept(sender, 6); + std::cout << "Later DATA admission at " << simTime() << ": deferred=" << deferredBeforeExtraction << ".\n"; + wait(SimTime(5, SIMTIME_MS)); + std::cout << "Spaced DATA: requests=" << sender->requests << ", DATA accepted=" << sender->dataVoice + << ", aggregates=" << sender->aggregatesAccepted << ", request start=" << sender->requestStart + << ", first DATA=" << sender->firstData << ".\n"; + ASSERT(deferredBeforeExtraction); + ASSERT(sender->requests == 1 && !sender->hasAgreement(6) && sender->delbas == 1); + ASSERT(sender->requestStart == 1 && sender->firstData == sender->requestStart); + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 2 : 0)); + } + else if (scenario == 4) { + accept(sender, 6); + accept(sender, 5); + ASSERT(sender->contending(AC_VO) && sender->contending(AC_VI)); + senderRx->setContentionBlocked(false); + wait(SimTime(50, SIMTIME_US)); + ASSERT(sender->dataVoice == 1 && sender->dataVideo == 0); + ASSERT(collisions == 1 && videoRetryDrops == 1); + } + else { + receiver->holdResponse = scenario != 2 && scenario < 8; + receiver->dropProtectedCts = scenario >= 8; + sender->holdRequest = scenario >= 5 && scenario != 8; + accept(sender, 6); + accept(sender, 6); + auto deferredFrame = accept(sender, 6); + auto deferredId = deferredFrame->getId(); + ASSERT(sender->contending(AC_VO)); + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + if (scenario >= 5 && scenario != 8) { + ASSERT(sender->requests == 0 && sender->requestAck == SIMTIME_MAX); + ASSERT(sender->deferred(6)); + auto release = SimTime(scenario == 9 ? 500 : scenario == 5 ? 100 : 450, SIMTIME_MS); + wait(release - simTime()); + sender->holdRequest = false; + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + } + std::cout << "Requests=" << sender->requests << ", start=" << sender->requestStart << ", voice DATA=" << sender->dataVoice << ".\n"; + ASSERT(sender->requests == 1); + ASSERT(sender->requestStart == sender->dataVoice - sender->dataAfterRequest); + ASSERT(sender->requestAck != SIMTIME_MAX); + ASSERT(sender->requestAck > sender->requestAccepted); + auto timeout = SimTime(scenario == 1 ? 250 : 100, SIMTIME_MS); + ASSERT(sender->observedDeadline == sender->requestAck + timeout); + if (scenario >= 8) { + if (scenario == 9) + wait(SimTime(560, SIMTIME_MS) - simTime()); + ASSERT(sender->rtsFailures >= 1); + ASSERT(sender->firstRtsSequence == sender->requestStart); + ASSERT(sender->dataWithAgreement == 0); + if (scenario == 8) { + ASSERT(sender->dataAfterRequest == 1 && sender->dataAfterDelba == 1); + ASSERT(sender->firstData == (sender->requestStart + 1) % 4096); + } + else + ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); + ASSERT(!sender->hasAgreement(6)); + ASSERT(sender->delbas == 1); + ASSERT(lifetimeDrops == (scenario == 9 ? 2 : 0)); + } + else if (scenario >= 5) { + auto edca = check_and_cast(sender->getSubmodule("edca")); + auto lifetime = edca->getLifetimeHandler(); + ASSERT(lifetime->getDeadline(deferredFrame) == SimTime(512, SIMTIME_MS)); + if (scenario == 5) { + ASSERT(sender->observedDeadline < SimTime(512, SIMTIME_MS)); + wait(sender->observedDeadline + SimTime(5, SIMTIME_MS) - simTime()); + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 1); + ASSERT(lifetimeDrops == 0); + } + else { + ASSERT(sender->observedDeadline > SimTime(512, SIMTIME_MS)); + if (scenario == 7) { + wait(SimTime(480, SIMTIME_MS) - simTime()); + accept(sender, 6); + } + wait(SimTime(513, SIMTIME_MS) - simTime()); + // Preview leaves both unsent frames under their original lifetime deadlines. + ASSERT(lifetimeDrops == 2 && lifetime->findFrame(deferredId) == nullptr); + if (scenario == 6) + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 0); + else { + ASSERT(sender->delbas == 0 && sender->deferred(6)); + wait(sender->observedDeadline + SimTime(5, SIMTIME_MS) - simTime()); + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 1); + } + } + std::cout << "Request release=" << SimTime(scenario == 5 ? 100 : 450, SIMTIME_MS) << ", request ACK=" << sender->requestAck + << ", response deadline=" << sender->observedDeadline << ", original DATA deadline=0.512.\n"; + } + else if (scenario == 2) { + ASSERT(sender->dataAfterRequest == 2); + ASSERT(sender->firstData == sender->requestStart); + } + else { + ASSERT(sender->deferred(6)); + ASSERT(sender->dataAfterRequest == 0); + ASSERT(deferredFrame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(!sender->contending(AC_VO)); + std::cout << "The pre-setup VO request resolved. Deferred VO has no contention request at " << simTime() << ".\n"; + if (scenario == 3) { + accept(sender, 5); + wait(SimTime(5, SIMTIME_MS)); + ASSERT(sender->dataVideo == 1); + ASSERT(sender->dataAfterRequest == 0); + ASSERT(collisions == 0 && videoRetryDrops == 0); + } + else { + wait(sender->requestAck + SimTime(150, SIMTIME_MS) - simTime()); + receiverRx->setContentionBlocked(false); + wait(SimTime(10, SIMTIME_MS)); + if (scenario == 1) { + ASSERT(receiver->responseAccepted != SIMTIME_MAX); + ASSERT(receiver->responseAccepted >= sender->requestAck + SimTime(150, SIMTIME_MS)); + } + std::cout << "Request ACK=" << sender->requestAck << ", response deadline=" << sender->observedDeadline + << ", response queued=" << receiver->responseQueued << ", response Tx accepted=" << receiver->responseAccepted << ".\n"; + if (scenario == 0) { + ASSERT(sender->delbas >= 1); + ASSERT(!sender->hasAgreement(6)); + } + else { + // The accepted response releases the final DATA before completion causes DELBA. + ASSERT(simTime() < sender->observedDeadline); + ASSERT(sender->delbas == 1); + ASSERT(!sender->hasAgreement(6)); + std::cout << "Completed deferred DATA: " << sender->dataAfterRequest << ".\n"; + ASSERT(sender->dataAfterRequest == 2); + ASSERT(sender->firstData == sender->requestStart); + } + } + } + } + sender->unsubscribe(Hcf::edcaCollisionDetectedSignal, this); + sender->unsubscribe(packetDroppedSignal, this); + std::cout << "Block Ack deferral scenario " << scenario << " passed.\n"; + endSimulation(); + } + public: + DeferralTest() : cSimpleModule(65536) {} +}; +Define_Module(DeferralTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.blockack.OriginatorBlockAckAgreementPolicy; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module DeferralHcf extends Hcf { parameters: @class(::DeferralHcf); } +simple LongTimeoutPolicy extends OriginatorBlockAckAgreementPolicy { parameters: addbaFailureTimeout = 250ms; } +simple DeferralTest { parameters: int scenario; @class(::DeferralTest); } +network DeferralNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sender: WirelessHost; + receiver: WirelessHost; + test: DeferralTest; +} + +%inifile: omnetpp.ini +[General] +network = DeferralNetwork +sim-time-limit = 1s +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0..11} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "DeferralHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} != 4 +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.typename = ${policy="OriginatorBlockAckAgreementPolicy","LongTimeoutPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy" ! scenario} +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = ${aggregation="","","","","","","","","","","","BasicMsduAggregationPolicy" ! scenario} +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = ${scenario} >= 8 && ${scenario} <= 9 ? 1B : 2346B +**.wlan[*].mac.hcf.rtsPolicy.ctsTimeout = ${scenario} == 9 ? 50ms : -1s +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = ${scenario} == 9 ? 10 : 1 +*.sender.wlan[0].address = "02:00:00:00:00:01" +*.receiver.wlan[0].address = "02:00:00:00:00:02" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sender.mobility.initialX = 0m +*.sender.mobility.initialY = 0m +*.receiver.mobility.initialX = 1m +*.receiver.mobility.initialY = 0m + +%contains: stdout +Block Ack deferral scenario 11 passed. diff --git a/tests/module/Ieee80211BlockAckDeferredCollision_1.test b/tests/module/Ieee80211BlockAckDeferredCollision_1.test new file mode 100644 index 00000000000..8bc3eef8be4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDeferredCollision_1.test @@ -0,0 +1,203 @@ +%description: +Check a pending EDCA request after production inactivity teardown defers its DATA. +The fixture supplies an accepted Basic agreement; it does not test negotiation. +VI and BE request the same grant time, at 34 microseconds. +The VI agreement expires at 33 microseconds and queues DELBA in VO. +VO uses six AIFS slots and grants later, at 70 microseconds. +Deferred VI must not cause collision feedback or disposal of eligible BE DATA. +The control retains eligible VI and must preserve the genuine collision. +The third run gives BE an accepted agreement with SSN zero and two DATA frames. +An internal collision discards sequence zero before transmission. +HCF must retire that agreement and queue DELBA before a successor can transmit. + +%extraargs: -r 0..2 + +%file: DeferredCollision.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" + +using namespace inet; +using namespace inet::ieee80211; + +class DeferredCollisionOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(simtime_t timeout, Tid tid) { + MacAddress peer("02:00:00:00:00:02"); + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; + +class DeferredCollisionHcf : public Hcf { + public: + int videoAccepted = 0; + int bestEffortAccepted = 0; + int delbas = 0; + void prepare(bool expire, Tid tid) { + Enter_Method("prepare"); + delete originatorBlockAckAgreementHandler; + auto handler = new DeferredCollisionOriginator(); + originatorBlockAckAgreementHandler = handler; + handler->accept(expire ? SimTime(33, SIMTIME_US) : SIMTIME_ZERO, tid); + if (expire) + scheduleInactivityTimer(); + } + bool contending(AccessCategory ac) const { + return check_and_cast(edca->getEdcaf(ac)->getSubmodule("contention"))->isContentionInProgress(); + } + bool eligible(AccessCategory ac) { return hasFrameToTransmit(ac); } + int queued(AccessCategory ac) const { return edca->getEdcaf(ac)->getPendingQueue()->getNumPackets(); } + bool hasAgreement(Tid tid) const { + return originatorBlockAckAgreementHandler->getAgreement(MacAddress("02:00:00:00:00:02"), tid) != nullptr; + } + protected: + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbas++; + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *frame, simtime_t ifs) override { + auto header = dynamicPtrCast(frame->peekAtFront()); + bool accepted = Hcf::transmitFrame(frame, ifs); + if (accepted && header) { + if (header->getTid() == 5) videoAccepted++; + if (header->getTid() == 0) bestEffortAccepted++; + } + return accepted; + } +}; +Define_Module(DeferredCollisionHcf); + +class DeferredCollisionTest : public cSimpleModule, public cListener { + int collisions = 0; + int bestEffortRetryDrops = 0; + protected: + void receiveSignal(cComponent *, simsignal_t, unsigned long count, cObject *) override { collisions += count; } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + auto drop = check_and_cast(details); + auto header = dynamicPtrCast(check_and_cast(object)->peekAtFront()); + if (drop->getReason() == RETRY_LIMIT_REACHED && header && header->getTid() == 0) + bestEffortRetryDrops++; + } + void accept(DeferredCollisionHcf *hcf, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(header->getReceiverAddress()); + auto frame = new Packet(tid == 5 ? "video-data" : "best-effort-data", header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + hcf->processUpperFrame(frame, header); + } + void activity() override { + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + int scenario = par("scenario"); + bool expire = scenario == 0; + Tid agreementTid = scenario == 2 ? 0 : 5; + hcf->prepare(expire, agreementTid); + hcf->subscribe(Hcf::edcaCollisionDetectedSignal, this); + hcf->subscribe(packetDroppedSignal, this); + accept(hcf, 5); + accept(hcf, 0); + if (scenario == 2) + accept(hcf, 0); + ASSERT(hcf->contending(AC_VI) && hcf->contending(AC_BE)); + wait(SimTime(33500, SIMTIME_NS)); + ASSERT(hcf->hasAgreement(agreementTid) == !expire); + ASSERT(hcf->eligible(AC_VI) == !expire); + ASSERT(hcf->eligible(AC_BE)); + std::cout << "At " << simTime() << ": VI eligible=" << hcf->eligible(AC_VI) + << ", VI pending request=" << hcf->contending(AC_VI) + << ", BE pending request=" << hcf->contending(AC_BE) + << ", VO pending request=" << hcf->contending(AC_VO) << ".\n"; + wait(SimTime(16500, SIMTIME_NS)); + std::cout << "At " << simTime() << ": VI DATA accepted=" << hcf->videoAccepted + << ", BE DATA accepted=" << hcf->bestEffortAccepted + << ", collisions=" << collisions << ", BE retry drops=" << bestEffortRetryDrops << ".\n"; + ASSERT(hcf->videoAccepted == (expire ? 0 : 1)); + ASSERT(hcf->bestEffortAccepted == (expire ? 1 : 0)); + ASSERT(collisions == (expire ? 0 : 1)); + ASSERT(bestEffortRetryDrops == (expire ? 0 : 1)); + if (scenario == 2) { + ASSERT(!hcf->hasAgreement(0)); + ASSERT(hcf->delbas == 1); + ASSERT(!hcf->eligible(AC_BE)); + ASSERT(hcf->queued(AC_BE) == 1); + ASSERT(hcf->bestEffortAccepted == 0); + std::cout << "First-DATA collision retires the agreement before successor transmission.\n"; + } + hcf->unsubscribe(Hcf::edcaCollisionDetectedSignal, this); + hcf->unsubscribe(packetDroppedSignal, this); + std::cout << "Deferred collision scenario " << par("scenario").intValue() << " passed.\n"; + endSimulation(); + } + public: + DeferredCollisionTest() : cSimpleModule(65536) {} +}; +Define_Module(DeferredCollisionTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module DeferredCollisionHcf extends Hcf { parameters: @class(::DeferredCollisionHcf); } +simple DeferredCollisionTest { parameters: int scenario; @class(::DeferredCollisionTest); } +network DeferredCollisionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: DeferredCollisionTest; +} + +%inifile: omnetpp.ini +[General] +network = DeferredCollisionNetwork +sim-time-limit = 1ms +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0,1,2} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "DeferredCollisionHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +*.host.wlan[0].mac.hcf.edca.edcaf[3].aifsn = 6 +**.wlan[*].mac.hcf.edca.edcaf[*].aifsn = 2 +**.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = 1 +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Deferred collision scenario 1 passed. + +%contains: stdout +First-DATA collision retires the agreement before successor transmission. diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test new file mode 100644 index 00000000000..5a15b2b964a --- /dev/null +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -0,0 +1,348 @@ +%description: +Check the sequence boundary after ordinary originator teardown of fragmented DATA. +IEEE Std 802.11-2024, 11.5.3.4 requires the next MPDU to use a later sequence +when the originator discards the last transmitted MPDU. +The fixture supplies accepted Basic agreements and injects immediate ACKs and Basic Block Ack responses. +Production fragmentation, inactivity expiry, retirement, and transmission remain active. +Runs cover sequence 0 and wraparound at 4095, with expiry during transmission +or after the first fragment receives its Normal Ack. +Later intact DATA and an unrelated peer must retain their transmissions. +Additional runs transmit an earlier DATA unit before a gap in sequence allocation. +The fixture extracts and disposes of the skipped DATA through the production allocator. +Distances 2047, 2048, 2049, and 4096 cover the half-range boundary and a full wrap. +A deferred successor keeps the transfer open while the fixture advances the allocator. + +%extraargs: -r 0..19 + +%file: FragmentRetirement.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class FragmentRetirementOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, int start, simtime_t timeout) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; + +class FragmentRetirementHcf : public Hcf { + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress other = MacAddress("02:00:00:00:00:03"); + int start = 0; + int distance = 0; + bool acknowledgedFirst = false; + bool firstCompleted = false; + bool firstAcknowledged = false; + bool retired = false; + bool successorSent = false; + bool otherSent = false; + bool otherRetired = false; + Ptr pendingBar; + Packet *deferredSuccessor = nullptr; + int delbas = 0; + + void skipData(int count) { + auto queue = edca->getEdcaf(AC_VI)->getPendingQueue(); + for (int i = 0; i < count; i++) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(5); + auto packet = new Packet("sequence-setup", header); + packet->insertAtBack(makeShared()); + queue->enqueuePacket(packet); + auto frames = originatorDataService->extractFramesToTransmit(queue); + for (auto frame : *frames) { + take(frame); + delete frame; + } + delete frames; + } + } + + void startRetirement() { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 5); + agreement->setStartingSequenceNumber(SequenceNumberCyclic(start)); + agreement->setBlockAckTimeoutValue(SimTime(1024, SIMTIME_US)); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(); + scheduleAfter(SimTime(acknowledgedFirst ? 650 : 974, SIMTIME_US), new cMessage("queue-data")); + } + + Packet *queueData(const char *name, MacAddress address, int length) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(5); + header->setReceiverAddress(address); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(address); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(length))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + return packet; + } + + public: + void begin() { + Enter_Method("begin"); + start = par("startSequence"); + distance = par("sequenceDistance"); + acknowledgedFirst = par("acknowledgedFirst"); + delete originatorBlockAckAgreementHandler; + auto handler = new FragmentRetirementOriginator(); + originatorBlockAckAgreementHandler = handler; + handler->accept(peer, start, SIMTIME_ZERO); + handler->accept(other, 0, SIMTIME_ZERO); + // Advance the production allocator without a radio exchange for each earlier sequence. + skipData(start); + subscribe(blockAckAgreementDeletedSignal, this); + if (distance == 0) + startRetirement(); + else { + check_and_cast(originatorDataService)->setEligibility( + [this](Packet *packet) { return !packet->isName("deferred-successor") && isFrameEligible(packet, false); }, + [this](Packet *packet) { return isAMsduPermitted(packet); }); + // Force Normal Ack for the earlier DATA, independent of the fragment ACK policy. + handler->getAgreement(peer, 5)->setStartingSequenceNumber(SequenceNumberCyclic(start) + 1); + queueData("earlier-data", peer, 20); + deferredSuccessor = queueData("deferred-successor", peer, 20); + } + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verify")); + } + + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, object, details); + return; + } + auto agreement = check_and_cast(object); + if (agreement->getReceiverAddr() == other) { + ASSERT(otherSent); + ASSERT(!hasQueuedData(other, 5) && !hasNumberedData(other, 5)); + otherRetired = true; + return; + } + ASSERT(!retired); + retired = true; + ASSERT(firstCompleted == acknowledgedFirst); + ASSERT(firstAcknowledged == acknowledgedFirst); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 5) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + auto frames = edca->getEdcaf(AC_VI)->getInProgressFrames(); + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + ASSERT(!data || data->getReceiverAddress() != peer || data->getSequenceNumber() != SequenceNumberCyclic(start)); + } + } + + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto data = dynamicPtrCast(packet->peekAtFront()); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && data) { + if (packet->isName("earlier-data")) { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start)); + ASSERT(data->getAckPolicy() == NORMAL_ACK); + return accepted; + } + if (data->getReceiverAddress() == peer) { + if (!retired) { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start)); + ASSERT(data->getFragmentNumber() == 0); + } + else { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start) + 1); + ASSERT(data->getFragmentNumber() == 0 && !data->getMoreFragments()); + successorSent = true; + } + } + else { + ASSERT(data->getReceiverAddress() == other); + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(0)); + otherSent = true; + } + } + return accepted; + } + + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto data = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); + if (bar) { + ASSERT(bar->getReceiverAddress() == other); + pendingBar = bar; + } + bool earlier = packet->isName("earlier-data"); + bool first = !earlier && data && data->getReceiverAddress() == peer && data->getSequenceNumber() == SequenceNumberCyclic(start); + bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); + if (first) + firstCompleted = true; + if (dynamicPtrCast(header)) + delbas++; + Hcf::transmissionComplete(packet, header); + if (ack) + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage(earlier ? "ack-earlier" : first ? "ack-first" : "ack")); + if (bar) + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("block-ack")); + } + + void handleMessage(cMessage *message) override { + if (message->isName("queue-data")) { + delete message; + queueData("fragmented-data", peer, 400); + queueData("intact-successor", peer, 20); + queueData("other-peer", other, 20); + } + else if (message->isName("block-ack")) { + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + header->setTransmitterAddress(other); + header->setTidInfo(5); + header->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + BitVector bits; + bits.appendBit(true); + for (int i = 1; i < 16; i++) + bits.appendBit(false); + header->setBlockAckBitmap(0, bits); + for (int position = 1; position < 64; position++) + header->setBlockAckBitmap(position, BitVector(std::vector{0, 0})); + auto packet = new Packet("block-ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(24000000))); + pendingBar = nullptr; + processLowerFrame(packet, header); + } + else if (message->isName("ack") || message->isName("ack-first") || message->isName("ack-earlier")) { + bool first = message->isName("ack-first"); + bool earlier = message->isName("ack-earlier"); + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + if (first) + firstAcknowledged = true; + if (earlier) { + auto queue = edca->getEdcaf(AC_VI)->getPendingQueue(); + queue->removePacket(deferredSuccessor); + take(deferredSuccessor); + edca->getLifetimeHandler()->forgetFrame(deferredSuccessor->getId()); + delete deferredSuccessor; + deferredSuccessor = nullptr; + skipData(distance - 1); + start = (start + distance) % 4096; + startRetirement(); + } + } + else if (message->isName("verify")) { + delete message; + std::cout << "Fragment retirement state: retired=" << retired << " firstCompleted=" << firstCompleted + << " successorSent=" << successorSent << " otherSent=" << otherSent << " delbas=" << delbas << "\n"; + ASSERT(retired && firstCompleted && successorSent && otherSent); + ASSERT(delbas == 2 && otherRetired); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) == nullptr); + unsubscribe(blockAckAgreementDeletedSignal, this); + std::cout << "Fragment retirement passed: start=" << start << ", acknowledgedFirst=" << acknowledgedFirst + << ", distance=" << distance << std::endl; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(FragmentRetirementHcf); + +class FragmentRetirementDriver : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(FragmentRetirementDriver); +} + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module FragmentRetirementHcf extends Hcf { + parameters: + int startSequence; + int sequenceDistance; + bool acknowledgedFirst; + @class(::inet::ieee80211::FragmentRetirementHcf); +} +simple FragmentRetirementDriver { parameters: @class(::inet::ieee80211::FragmentRetirementDriver); } +network FragmentRetirementNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + driver: FragmentRetirementDriver; +} + +%inifile: omnetpp.ini +[General] +network = FragmentRetirementNetwork +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "FragmentRetirementHcf" +**.wlan[*].mac.hcf.startSequence = ${start=0,4095} +**.wlan[*].mac.hcf.sequenceDistance = ${distance=0,2047,2048,2049,4096} +**.wlan[*].mac.hcf.acknowledgedFirst = ${acked=false,true} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 200B +**.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = ${acked} ? 0B : 1000B +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].aifsn = 20 +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = 0s +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Fragment retirement passed: start=0, acknowledgedFirst=0 +%contains: stdout +Fragment retirement passed: start=0, acknowledgedFirst=1 +%contains: stdout +Fragment retirement passed: start=4095, acknowledgedFirst=0 +%contains: stdout +Fragment retirement passed: start=4095, acknowledgedFirst=1 diff --git a/tests/module/Ieee80211BlockAckLifecycle_1.test b/tests/module/Ieee80211BlockAckLifecycle_1.test new file mode 100644 index 00000000000..eb6d073cafd --- /dev/null +++ b/tests/module/Ieee80211BlockAckLifecycle_1.test @@ -0,0 +1,358 @@ +%description: +Check Basic teardown through initialized HCF and production receive paths. Each role has agreements for two peers and two TIDs. Deletion callbacks inspect the selected agreement after its removal. Recipient teardown releases complete units behind a gap before the callback. Absent-agreement DATA and BAR cause recipient-role DELBA without agreement allocation. The fixture supplies accepted agreements; it does not test negotiation or wireless transmission. + +The inactivity timer uses the earliest absolute deadline from both agreement roles. The fixture checks that derived deadline and the later expiration of the opposite role. + +%extraargs: -r 0..9 + +%file: Lifecycle.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class LifecycleOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid, int start = 100, simtime_t timeout = SIMTIME_ZERO) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; +class LifecycleRecipient : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid, int start = 100, simtime_t timeout = SIMTIME_ZERO) { + auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), 64, timeout); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; +class LifecycleHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress other = MacAddress("02:00:00:00:00:03"); + std::vector delivered; + int deletions = 0; + int delbas = 0; + int controlResponses = 0; + bool selectedRecipient = false; + bool requireRelease = false; + bool strictOpposite = true; + bool dynamicRole = false; + void prepare(int start = 100, simtime_t originatorTimeout = SIMTIME_ZERO, simtime_t recipientTimeout = SIMTIME_ZERO) { + Enter_Method("prepare"); + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new LifecycleOriginator(); + auto recipient = new LifecycleRecipient(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + for (auto address : {peer, other}) + for (Tid tid : {5, 6}) { + bool selected = address == peer && tid == 5; + originator->accept(address, tid, start, selected ? originatorTimeout : SIMTIME_ZERO); + recipient->accept(address, tid, start, selected ? recipientTimeout : SIMTIME_ZERO); + } + subscribe(blockAckAgreementDeletedSignal, this); + } + void checkInactivityDeadline(simtime_t expectedDeadline) { + Enter_Method("checkInactivityDeadline"); + scheduleInactivityTimer(); + ASSERT(inactivityTimer->isScheduled()); + ASSERT(inactivityTimer->getArrivalTime() == expectedDeadline); + } + bool hasInactivityTimer() const { return inactivityTimer->isScheduled(); } + void retireRecipientLocally() { + Enter_Method("retireRecipientLocally"); + selectedRecipient = true; + beginTeardown(peer, 5, false, 37); + } + bool hasOriginator(MacAddress address, Tid tid) { + return originatorBlockAckAgreementHandler->getAgreement(address, tid) != nullptr; + } + bool hasRecipient(MacAddress address, Tid tid) { + return recipientBlockAckAgreementHandler->getAgreement(tid, address) != nullptr; + } + void receiveDelba(bool initiator, int sequence) { + Enter_Method("receiveDelba"); + selectedRecipient = initiator; + auto header = makeShared(); + header->setTransmitterAddress(peer); + header->setReceiverAddress(mac->getAddress()); + header->setTid(5); + header->setInitiator(initiator); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + auto packet = new Packet("delba", header); + packet->addTag()->setMode( + physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + void receiveData(int sequence, const char *name, MacAddress address, bool incomplete = false) { + Enter_Method("receiveData"); + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(address); + header->setReceiverAddress(mac->getAddress()); + header->setAddress3(address); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setMoreFragments(incomplete); + auto packet = new Packet(name, header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + void receiveBar(MacAddress address, int fragment = 0) { + Enter_Method("receiveBar"); + auto header = makeShared(); + header->setTransmitterAddress(address); + header->setReceiverAddress(mac->getAddress()); + header->setTidInfo(5); + header->setFragmentNumber(fragment); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("bar", header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *payload, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, payload, details); + return; + } + if (requireRelease) + ASSERT((delivered == std::vector{"second", "third"})); + ASSERT(payload != nullptr); + if (dynamicRole) + selectedRecipient = dynamic_cast(payload) != nullptr; + if (selectedRecipient) { + auto agreement = check_and_cast(payload); + ASSERT(agreement->getBlockAckRecord()->getOriginatorAddress() == peer); + ASSERT(agreement->getBlockAckRecord()->getTid() == 5); + ASSERT(!hasRecipient(peer, 5)); + if (strictOpposite) + ASSERT(hasOriginator(peer, 5)); + ASSERT(agreement->getBufferSize() == 64); + } + else { + auto agreement = check_and_cast(payload); + ASSERT(agreement->getReceiverAddr() == peer); + ASSERT(agreement->getTid() == 5); + ASSERT(!hasOriginator(peer, 5)); + if (strictOpposite) + ASSERT(hasRecipient(peer, 5)); + ASSERT(agreement->getBufferSize() == 64); + } + ASSERT(hasOriginator(other, 5)); + ASSERT(hasRecipient(other, 5)); + ASSERT(hasOriginator(peer, 6)); + ASSERT(hasRecipient(peer, 6)); + deletions++; + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + if (dynamicPtrCast(packet->peekAtFront())) + delivered.push_back(packet->getName()); + take(packet); + delete packet; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + controlResponses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (auto delba = dynamicPtrCast(header)) { + if (!dynamicRole) + ASSERT(!delba->getInitiator()); + delbas++; + } + delete packet; + } +}; +Define_Module(LifecycleHcf); +class LifecycleTest : public cSimpleModule { + protected: + LifecycleHcf *hcf = nullptr; + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + if (message->isName("recipientExpiry")) { + delete message; + ASSERT(hcf->hasOriginator(hcf->peer, 5)); + ASSERT(!hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + ASSERT((hcf->delivered == std::vector{"second", "third"})); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + if (message->isName("firstExpiry")) { + delete message; + ASSERT(!hcf->hasOriginator(hcf->peer, 5)); + ASSERT(hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + scheduleAt(SimTime(5000001, SIMTIME_NS), new cMessage("secondExpiry")); + return; + } + if (message->isName("secondExpiry")) { + delete message; + ASSERT(!hcf->hasOriginator(hcf->peer, 5)); + ASSERT(!hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 2); + ASSERT(hcf->delbas == 2); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + if (message->isName("zeroExpiry")) { + delete message; + ASSERT(hcf->hasOriginator(hcf->peer, 5)); + ASSERT(hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 0); + ASSERT(hcf->delbas == 0); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + delete message; + hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + int scenario = par("scenario"); + hcf->prepare(scenario == 5 ? 4094 : 100, + scenario == 6 ? SimTime(2, SIMTIME_MS) : SIMTIME_ZERO, + scenario == 6 ? SimTime(4, SIMTIME_MS) : scenario == 9 ? SimTime(2, SIMTIME_MS) : SIMTIME_ZERO); + if (scenario >= 8) { + hcf->requireRelease = true; + hcf->selectedRecipient = true; + hcf->receiveData(101, "second", hcf->peer); + hcf->receiveData(102, "third", hcf->peer); + ASSERT(hcf->delivered.empty()); + if (scenario == 8) { + hcf->retireRecipientLocally(); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + hcf->retireRecipientLocally(); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + std::cout << "Block Ack lifecycle assertions passed.\n"; + } + else { + hcf->checkInactivityDeadline(SimTime(3, SIMTIME_MS)); + scheduleAt(SimTime(3000001, SIMTIME_NS), new cMessage("recipientExpiry")); + } + return; + } + if (scenario >= 6) { + if (scenario == 6) { + hcf->strictOpposite = false; + hcf->dynamicRole = true; + hcf->checkInactivityDeadline(SimTime(3, SIMTIME_MS)); + scheduleAt(SimTime(3000001, SIMTIME_NS), new cMessage("firstExpiry")); + } + else + scheduleAt(SimTime(6, SIMTIME_MS), new cMessage("zeroExpiry")); + return; + } + if (scenario <= 2 || scenario == 5) { + if (scenario == 2 || scenario == 5) { + hcf->requireRelease = true; + if (scenario == 5) + hcf->receiveData(4094, "incomplete-frag0", hcf->peer, true); + hcf->receiveData(scenario == 5 ? 4095 : 101, "second", hcf->peer); + hcf->receiveData(scenario == 5 ? 0 : 102, "third", hcf->peer); + ASSERT(hcf->delivered.empty()); + } + hcf->receiveDelba(scenario != 0, 20); + ASSERT(hcf->deletions == 1); + hcf->receiveDelba(scenario != 0, 21); + ASSERT(hcf->deletions == 1); + if (scenario == 2 || scenario == 5) + ASSERT((hcf->delivered == std::vector{"second", "third"})); + } + else { + const MacAddress absent("02:00:00:00:00:04"); + if (scenario == 4) { + hcf->receiveBar(absent, 1); + ASSERT(hcf->delbas == 0); + ASSERT(hcf->controlResponses == 0); + } + if (scenario == 3) + hcf->receiveData(100, "absent", absent); + else + hcf->receiveBar(absent); + ASSERT(hcf->delivered.empty()); + ASSERT(hcf->delbas == 1); + ASSERT(hcf->controlResponses == 0); + ASSERT(!hcf->hasRecipient(absent, 5)); + ASSERT(!hcf->hasOriginator(absent, 5)); + if (scenario == 3) + hcf->receiveData(101, "absent-again", absent); + else + hcf->receiveBar(absent); + ASSERT(hcf->delbas == 1); + ASSERT(hcf->delivered.empty()); + } + std::cout << "Block Ack lifecycle assertions passed.\n"; + } +}; +Define_Module(LifecycleTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module LifecycleHcf extends Hcf { parameters: @class(::inet::ieee80211::LifecycleHcf); } +simple LifecycleTest { parameters: int scenario; @class(::inet::ieee80211::LifecycleTest); } +network LifecycleNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: LifecycleTest; +} + +%inifile: omnetpp.ini +[General] +network = LifecycleNetwork +ned-path = .;../../../../src +sim-time-limit = 7ms +seed-set = 0 +*.test.scenario = ${0..9} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "LifecycleHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack lifecycle assertions passed. diff --git a/tests/module/Ieee80211BlockAckNegotiation_1.test b/tests/module/Ieee80211BlockAckNegotiation_1.test new file mode 100644 index 00000000000..3f8b9c11a56 --- /dev/null +++ b/tests/module/Ieee80211BlockAckNegotiation_1.test @@ -0,0 +1,83 @@ +%description: +Check recipient negotiation through the production agreement handler. +The recipient response determines buffer size and inactivity timeout. +The response copies the request dialog token. + +%extraargs: -r 0..3 + +%file: Negotiation.cc +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" + +namespace inet::ieee80211 { +class NegotiationPolicy : public IRecipientBlockAckAgreementPolicy { + public: + bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override { + return variant == BlockAckVariant::BASIC || (localHt && peerHt); + } + bool isAddbaReqAccepted(const Ptr&) override { return true; } + bool isDelbaAccepted(const Ptr&) override { return true; } + simtime_t getBlockAckTimeoutValue() const override { return SimTime(20, SIMTIME_MS); } + bool aMsduSupported() const override { return false; } + bool delayedBlockAckPolicySupported() const override { return false; } + int getMaximumAllowedBufferSize() const override { return 64; } +}; +class NegotiationTest : public cSimpleModule, public IProcedureCallback { + protected: + RecipientBlockAckAgreementHandler handler; + NegotiationPolicy policy; + Ptr response; + void initialize() override { scheduleAt(SIMTIME_ZERO, new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + const MacAddress peer("02:00:00:00:00:02"); + auto request = makeShared(); + request->setTransmitterAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(par("requestSize")); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBlockAckTimeoutValue(SimTime(40, SIMTIME_MS)); + handler.processReceivedAddbaRequest(request, &policy, this); + ASSERT(response != nullptr); + ASSERT(response->getDialogToken() == 37); + ASSERT(response->getBufferSize() == 64); + ASSERT(response->getBlockAckTimeoutValue() == SimTime(20, SIMTIME_MS)); + auto agreement = handler.getAgreement(5, peer); + ASSERT(agreement != nullptr); + ASSERT(agreement->getBufferSize() == response->getBufferSize()); + ASSERT(agreement->getBlockAckTimeoutValue() == response->getBlockAckTimeoutValue()); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(100)); + std::cout << "Block Ack negotiation assertions passed.\n"; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + response = dynamicPtrCast(header); + delete packet; + } + void transmitControlResponseFrame(Packet *, const Ptr&, + Packet *, const Ptr&) override { throw cRuntimeError("Unexpected control response"); } +}; +Define_Module(NegotiationTest); +} + +%file: test.ned +simple NegotiationTest { + parameters: + int requestSize; + @class(::inet::ieee80211::NegotiationTest); +} +network NegotiationNetwork { submodules: test: NegotiationTest; } + +%inifile: omnetpp.ini +[General] +network = NegotiationNetwork +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +*.test.requestSize = ${0,8,64,128} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Block Ack negotiation assertions passed. diff --git a/tests/module/Ieee80211BlockAckReordering_1.test b/tests/module/Ieee80211BlockAckReordering_1.test index 449a6041fde..3731b956b54 100644 --- a/tests/module/Ieee80211BlockAckReordering_1.test +++ b/tests/module/Ieee80211BlockAckReordering_1.test @@ -3,9 +3,10 @@ Check S1 recipient delivery through RecipientQosMacDataService. Supply accepted Basic agreements with known buffer sizes. Check fragment capacity, complete gap release, cyclic delivery, and BAR before DATA. Check window movement, incomplete-unit disposal, peer/TID isolation, and A-MSDU order. -The fixture supplies agreements; it does not test ADDBA negotiation or Compressed exchanges. +The fixture supplies agreements; it does not test ADDBA negotiation. +Compressed cases check gap release, cyclic delivery, peer/TID isolation, and receipt retention through the production recipient service. -%extraargs: -r 0..7 +%extraargs: -r 0..10 %file: RecipientReordering.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -40,8 +41,9 @@ Define_Module(ReorderingRecipient); class ReorderingAgreements : public RecipientBlockAckAgreementHandler { public: - void accept(MacAddress peer, Tid tid, int start, int size) { + void accept(MacAddress peer, Tid tid, int start, int size, BlockAckVariant variant = BlockAckVariant::BASIC) { auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), size, SIMTIME_ZERO); + agreement->setVariant(variant); agreement->addbaResposneSent(); blockAckAgreements[{peer, tid}] = agreement; } @@ -224,6 +226,33 @@ class RecipientReorderingTest : public cSimpleModule { checkDelivery(receive(makeData(101, "fragmented-frag1", 1)), {"fragmented", "third"}); checkWindow(103, 0, peer); break; + case 8: + case 9: { + int start = scenario == 8 ? 100 : 4095; + agreements.accept(peer, 5, start, 4, BlockAckVariant::COMPRESSED); + checkDelivery(receive(makeData(start, "first")), {"first"}); + checkDelivery(receive(makeData((start + 2) % 4096, "third")), {}); + checkDelivery(receive(makeData((start + 1) % 4096, "second")), {"second", "third"}); + checkWindow((start + 3) % 4096, 0, peer); + auto record = agreements.getAgreement(5, peer)->getBlockAckRecord(); + for (int offset = 0; offset < 4; offset++) + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start) + offset) == (offset < 3)); + break; + } + case 10: + agreements.accept(peer, 5, 100, 4, BlockAckVariant::COMPRESSED); + agreements.accept(otherPeer, 5, 100, 4, BlockAckVariant::COMPRESSED); + agreements.accept(peer, 6, 100, 4, BlockAckVariant::COMPRESSED); + checkDelivery(receive(makeData(101, "peer-one")), {}); + checkDelivery(receive(makeData(101, "peer-two", 0, false, otherPeer)), {}); + checkDelivery(receive(makeData(101, "tid-six", 0, false, peer, 6)), {}); + checkDelivery(receive(makeData(100, "first")), {"first", "peer-one"}); + checkWindow(102, 0, peer); + checkWindow(100, 1, otherPeer); + checkWindow(100, 1, peer, 6); + ASSERT(!agreements.getAgreement(5, otherPeer)->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + ASSERT(!agreements.getAgreement(6, peer)->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + break; default: throw cRuntimeError("Unknown recipient scenario"); } @@ -267,7 +296,7 @@ seed-set = 0 cmdenv-express-mode = true record-vector-results = false record-scalar-results = false -*.test.scenario = ${scenario=0..7} +*.test.scenario = ${scenario=0..10} %contains: stdout Recipient reordering checks passed. diff --git a/tests/module/Ieee80211BlockAckResponseWait_1.test b/tests/module/Ieee80211BlockAckResponseWait_1.test new file mode 100644 index 00000000000..9fc88c204d4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckResponseWait_1.test @@ -0,0 +1,242 @@ +%description: +Check Action reception during the final ADDBA Request ACK wait through initialized Basic HCF. The request uses production contention, transmission, and frame-sequence handling. The fixture supplies the initial request record and injects an incoming Action after transmission. Matching success and rejection must suppress obsolete request failure. Wrong-token and unrelated-Action controls must retain ordinary request failure. The test does not model wireless reception or peer contention. + +%extraargs: -r 0..3 + +%file: ResponseWait.cc +#include +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ResponseWaitOriginator : public OriginatorBlockAckAgreementHandler { + public: + void prepare(const Ptr& request) { createAgreement(request); } +}; +class ResponseWaitHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + int attempts = 0; + int failures = 0; + int acknowledgments = 0; + int delbas = 0; + int linkBreaks = 0; + int requestOutcomes = 0; + bool injected = false; + bool dispatchingAction = false; + void begin() { + Enter_Method("begin"); + subscribe(linkBrokenSignal, this); + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + delete originatorBlockAckAgreementHandler; + auto handler = new ResponseWaitOriginator(); + originatorBlockAckAgreementHandler = handler; + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + handler->prepare(request); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(mac->getAddress()); + data->setReceiverAddress(peer); + data->setAddress3(peer); + data->setTid(5); + auto packet = new Packet("deferred-data", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, data); + Hcf::processMgmtFrame(new Packet("request", request), request); + } + void verify() { + Enter_Method("verify"); + ASSERT(injected); + ASSERT(attempts == 1); + ASSERT(acknowledgments == 1); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 5); + int scenario = par("scenario"); + if (scenario < 2) { + ASSERT(failures == 0); + ASSERT(delbas == 0); + ASSERT(linkBreaks == 0); + ASSERT(requestOutcomes <= 1); + ASSERT(!addbaResponseTimer->isScheduled()); + ASSERT(!startRxTimer->isScheduled()); + if (scenario == 0) { + ASSERT(agreement != nullptr); + ASSERT(agreement->getIsAddbaResponseReceived()); + ASSERT(agreement->getBufferSize() == 32); + } + else + ASSERT(agreement == nullptr || !agreement->getIsAddbaResponseReceived()); + } + else { + ASSERT(failures == 1); + ASSERT(linkBreaks == 1); + ASSERT(requestOutcomes == 1); + ASSERT(agreement == nullptr || !agreement->getIsAddbaResponseReceived()); + } + std::cout << "Block Ack response-wait assertions passed.\n"; + endSimulation(); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == linkBrokenSignal) + linkBreaks++; + else if (signal == Ieee80211Mac::frameTransmissionOutcomeSignal) { + auto packet = check_and_cast(object); + if (dynamicPtrCast(packet->peekAtFront())) + requestOutcomes++; + } + else + Hcf::receiveSignal(source, signal, object, details); + } + void handleMessage(cMessage *message) override { + if (message->isName("injectAction")) { + delete message; + ASSERT(frameSequenceHandler->isSequenceRunning()); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::RECEIVE); + ASSERT(startRxTimer->isScheduled()); + int scenario = par("scenario"); + Ptr header; + if (scenario == 3) { + auto request = makeShared(); + request->setTid(6); + request->setDialogToken(91); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + header = request; + } + else { + auto response = makeShared(); + response->setTid(5); + response->setDialogToken(scenario == 2 ? 38 : 37); + response->setStatusCode(scenario == 1 ? 37 : 0); + response->setBlockAckPolicy(true); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(SIMTIME_ZERO); + header = response; + } + header->setTransmitterAddress(peer); + header->setReceiverAddress(mac->getAddress()); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet("incoming-action", header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + injected = true; + dispatchingAction = true; + processLowerFrame(packet, header); + dispatchingAction = false; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("verify")); + } + else if (message->isName("verify")) { + delete message; + verify(); + } + else + Hcf::handleMessage(message); + } + void originatorProcessTransmittedFrame(Packet *packet) override { + auto request = dynamicPtrCast(packet->peekAtFront()); + Hcf::originatorProcessTransmittedFrame(packet); + if (request) { + attempts++; + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("injectAction")); + } + } + void originatorProcessFailedFrame(Packet *packet) override { + if (dynamicPtrCast(packet->peekAtFront())) + failures++; + Hcf::originatorProcessFailedFrame(packet); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + if (dynamicPtrCast(packet->peekAtFront())) + ASSERT(!dispatchingAction); + return Hcf::transmitFrame(packet, ifs); + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + acknowledgments++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbas++; + delete packet; + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + take(packet); + delete packet; + } + } +}; +Define_Module(ResponseWaitHcf); +class ResponseWaitTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(ResponseWaitTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ResponseWaitHcf extends Hcf { + parameters: + int scenario; + @class(::inet::ieee80211::ResponseWaitHcf); +} +simple ResponseWaitTest { parameters: @class(::inet::ieee80211::ResponseWaitTest); } +network ResponseWaitNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ResponseWaitTest; +} + +%inifile: omnetpp.ini +[General] +network = ResponseWaitNetwork +ned-path = .;../../../../src +sim-time-limit = 5ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ResponseWaitHcf" +**.wlan[*].mac.hcf.scenario = ${0..3} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.mgmtAndNonQoSRecoveryProcedure.shortRetryLimit = 1 +**.mgmtAndNonQoSRecoveryProcedure.longRetryLimit = 1 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack response-wait assertions passed. diff --git a/tests/module/Ieee80211BlockAckRetiredData_1.test b/tests/module/Ieee80211BlockAckRetiredData_1.test new file mode 100644 index 00000000000..0898ec6f565 --- /dev/null +++ b/tests/module/Ieee80211BlockAckRetiredData_1.test @@ -0,0 +1,325 @@ +%description: +Check peer reset during actual Normal Ack DATA exchanges through HCF and Tx. Reset occurs during ACK wait, transmission, or the SIFS before a second DATA frame. The accepted transmission retains safe ownership until completion. Each completed target transmission must emit one packetSentToPeer signal from its EDCAF. A late ACK must not restore ACK state or cause another DATA attempt. The test injects ACK frames at the HCF receive boundary and uses no peer radio. + +The late ACK must emit one packetReceivedFromPeer signal. Further cases reset during accepted RTS transmission or CTS wait. DATA and owned Actions must not return to transmission after CTS. The old management exchange must not select another peer's frame after reset. Another peer's DATA retains its lifetime record. + +%extraargs: -r 0..8 + +%file: RetiredData.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class RetiredOriginator : public OriginatorBlockAckAgreementHandler { + public: + void prepare(const Ptr& request) { createAgreement(request); } +}; +class RetiredDataHcf : public Hcf { + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + Ptr retiredHeader; + int64_t retiredId = -1; + int attempts = 0; + int completions = 0; + bool reset = false; + bool targetCompleted = false; + int targetSentSignals = 0; + int lateAckSignals = 0; + int64_t lateAckId = -1; + int64_t otherId = -1; + bool rtsCompleted = false; + bool injectingCts = false; + public: + void begin() { + Enter_Method("begin"); + edca->getEdcaf(AC_VI)->subscribe("packetSentToPeer", this); + subscribe("packetReceivedFromPeer", this); + int scenario = par("scenario"); + if (scenario >= 5) { + delete originatorBlockAckAgreementHandler; + auto originator = new RetiredOriginator(); + originatorBlockAckAgreementHandler = originator; + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + originator->prepare(request); + queueData("deferred"); + processMgmtFrame(new Packet("owned-request", request), request); + return; + } + queueData("first"); + if (par("scenario").intValue() == 2) + queueData("second"); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == packetSentToPeerSignal) { + ASSERT(source == edca->getEdcaf(AC_VI)); + auto packet = check_and_cast(value); + if (packet->getId() == retiredId) + targetSentSignals++; + } + else if (signal == packetReceivedFromPeerSignal) { + if (check_and_cast(value)->getId() == lateAckId) + lateAckSignals++; + } + else + Hcf::receiveSignal(source, signal, value, details); + } + void queueData(const char *name, bool other = false) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(mac->getAddress()); + header->setReceiverAddress(other ? MacAddress("02:00:00:00:00:03") : peer); + header->setAddress3(peer); + header->setTid(5); + auto packet = new Packet(name, header); + if (other) + otherId = packet->getId(); + packet->insertAtBack(makeShared(B(1500))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void injectAck() { + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("ack", header); + if (reset) + lateAckId = packet->getId(); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + ASSERT(packet != nullptr); + auto header = packet->peekAtFront(); + auto data = dynamicPtrCast(header); + int scenario = par("scenario"); + bool obsoleteSubmission = scenario >= 3 && reset && header->getReceiverAddress() == peer && header->getType() != ST_RTS; + bool substitutedSubmission = scenario >= 7 && injectingCts && header->getType() != ST_RTS; + bool accepted = Hcf::transmitFrame(packet, ifs); + if (obsoleteSubmission || substitutedSubmission) { + std::cout << "Post-reset protected frame accepted: " << accepted << std::endl; + ASSERT(!accepted); + return false; + } + if (scenario >= 3 && header->getType() == ST_RTS && accepted && header->getReceiverAddress() == peer) { + ASSERT(!reset); + ASSERT(tx->hasTransmission()); + if (scenario == 3 || scenario == 5 || scenario == 7) + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("resetRts")); + return accepted; + } + if (data && accepted) { + attempts++; + int scenario = par("scenario"); + bool target = scenario == 2 ? attempts == 2 : attempts == 1; + if (target) { + retiredHeader = packet->peekAtFront(); + retiredId = packet->getId(); + ASSERT(retiredHeader->getAckPolicy() == NORMAL_ACK); + if (scenario != 0) { + if (scenario == 2) + ASSERT(ifs > SIMTIME_ZERO); + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("reset")); + } + } + } + return accepted; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto data = dynamicPtrCast(header); + bool isData = data != nullptr; + bool isTarget = data && retiredHeader && data->getSequenceNumber() == retiredHeader->getSequenceNumber(); + bool isRts = header->getType() == ST_RTS; + Hcf::transmissionComplete(packet, header); + if (par("scenario").intValue() >= 3 && isRts) { + rtsCompleted = true; + scheduleAfter(SimTime(60, SIMTIME_US), new cMessage(reset ? "cts" : "resetCtsWait")); + return; + } + if (isData) { + completions++; + int scenario = par("scenario"); + if (isTarget) { + targetCompleted = true; + if (scenario == 0) + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("reset")); + else + scheduleAfter(SimTime(60, SIMTIME_US), new cMessage("lateAck")); + } + else if (scenario == 2 && completions == 1) + scheduleAfter(SimTime(60, SIMTIME_US), new cMessage("firstAck")); + } + } + void handleMessage(cMessage *message) override { + if (message->isName("resetRts") || message->isName("resetCtsWait")) { + bool waiting = message->isName("resetCtsWait"); + delete message; + ASSERT(frameSequenceHandler->isSequenceRunning()); + ASSERT(rtsCompleted == waiting); + ASSERT(tx->hasTransmission() == !waiting); + queueData("other-peer", true); + if (par("scenario").intValue() >= 7) { + auto header = makeShared(); + header->setType(ST_PROBEREQUEST); + header->setReceiverAddress(MacAddress("02:00:00:00:00:03")); + auto packet = new Packet("other-management", header); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + ASSERT(edca->getLifetimeHandler()->findFrame(otherId) != nullptr); + resetPeer(peer); + reset = true; + ASSERT(edca->getLifetimeHandler()->findFrame(otherId) != nullptr); + ASSERT(!hasQueuedData(peer, 5)); + if (waiting) + scheduleAfter(SimTime(60, SIMTIME_US), new cMessage("cts")); + } + else if (message->isName("cts")) { + delete message; + ASSERT(reset && rtsCompleted); + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("late-cts", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + injectingCts = true; + processLowerFrame(packet, header); + injectingCts = false; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("verifyRts")); + } + else if (message->isName("verifyRts")) { + delete message; + ASSERT(!hasQueuedData(peer, 5)); + ASSERT(edca->getLifetimeHandler()->findFrame(otherId) != nullptr); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().count({peer, 5}) == 0); + ASSERT(ownedActions.empty()); + edca->getEdcaf(AC_VI)->unsubscribe("packetSentToPeer", this); + unsubscribe("packetReceivedFromPeer", this); + std::cout << "Block Ack retired-DATA assertions passed.\n"; + endSimulation(); + } + else if (message->isName("reset")) { + delete message; + ASSERT(!reset); + int scenario = par("scenario"); + ASSERT(frameSequenceHandler->isSequenceRunning()); + if (scenario == 0) { + ASSERT(targetCompleted); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::RECEIVE); + } + else { + ASSERT(!targetCompleted); + ASSERT(tx->hasTransmission()); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::TRANSMIT); + } + resetPeer(peer); + reset = true; + ASSERT(edca->getLifetimeHandler()->findFrame(retiredId) == nullptr); + if (scenario != 0) + ASSERT(tx->hasTransmission()); + else + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("lateAck")); + } + else if (message->isName("firstAck")) { + delete message; + injectAck(); + } + else if (message->isName("lateAck")) { + delete message; + ASSERT(reset); + ASSERT(targetCompleted); + injectAck(); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("verify")); + } + else if (message->isName("verify")) { + delete message; + ASSERT(reset && targetCompleted); + ASSERT(attempts == (par("scenario").intValue() == 2 ? 2 : 1)); + ASSERT(!tx->hasTransmission()); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(edca->getEdcaf(AC_VI)->getAckHandler()->getQoSDataAckStatus(retiredHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + edca->getEdcaf(AC_VI)->unsubscribe("packetSentToPeer", this); + std::cout << "Retired target sent signals: " << targetSentSignals << std::endl; + std::cout << "Late ACK received signals: " << lateAckSignals << std::endl; + unsubscribe("packetReceivedFromPeer", this); + ASSERT(targetSentSignals == 1); + ASSERT(lateAckSignals == 1); + std::cout << "Block Ack retired-DATA assertions passed.\n"; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(RetiredDataHcf); +class RetiredDataTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(RetiredDataTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module RetiredDataHcf extends Hcf { parameters: int scenario; @class(::inet::ieee80211::RetiredDataHcf); } +simple RetiredDataTest { parameters: @class(::inet::ieee80211::RetiredDataTest); } +network RetiredDataNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: RetiredDataTest; +} + +%inifile: omnetpp.ini +[General] +network = RetiredDataNetwork +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "RetiredDataHcf" +**.wlan[*].mac.hcf.scenario = ${scenario=0..8} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = ${scenario} >= 3 ? 0B : 2346B +**.wlan[*].mac.hcf.rtsPolicy.ctsTimeout = 1ms +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = 10ms +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack retired-DATA assertions passed. diff --git a/tests/module/Ieee80211BlockAckScheduling_1.test b/tests/module/Ieee80211BlockAckScheduling_1.test new file mode 100644 index 00000000000..ec072ea0dd2 --- /dev/null +++ b/tests/module/Ieee80211BlockAckScheduling_1.test @@ -0,0 +1,579 @@ +%description: +Exercise Basic and Compressed BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. +The fixture supplies accepted agreements and injects responses at the HCF receive boundary. +Compressed cases supply peer HT capabilities; the fixture does not test capability negotiation. +Response events include SIFS and the selected response airtime. +Cases cover short bursts, windows smaller than the threshold, two peers, BAR timeout, +negative bitmap bits, Normal Ack window progress, and late BAR admission. + +%extraargs: -r 0..75 + +%file: Scheduling.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +class SchedulingRecipientAgreements : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer, BlockAckVariant variant, int start = 0) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, SIMTIME_ZERO); + agreement->setVariant(variant); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class SchedulingRecipient : public Hcf { + public: + std::vector delivered; + bool checkedBitmap = false; + bool orderedReceipt = false; + void publishPrimaryChannel() { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + } + void checkOrderedReceipt() { + Enter_Method("checkOrderedReceipt"); + delete recipientBlockAckAgreementHandler; + auto agreements = new SchedulingRecipientAgreements(); + recipientBlockAckAgreementHandler = agreements; + orderedReceipt = true; + publishPrimaryChannel(); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:01"), mib->localHtCapabilities, mib->getHtOperation()); + agreements->accept(MacAddress("02:00:00:00:00:01"), BlockAckVariant::COMPRESSED, 100); + for (int sequence : {100, 102, 101}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(data->getTransmitterAddress()); + data->setTid(5); + data->setAckPolicy(BLOCK_ACK); + data->setSequenceNumber(SequenceNumberCyclic(sequence)); + auto packet = new Packet("ordered", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getSlowestMode()); + processLowerFrame(packet, data); + ASSERT((delivered == (sequence == 101 ? std::vector{100, 101, 102} : std::vector{100}))); + } + auto bar = makeShared(); + bar->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + bar->setReceiverAddress(mac->getAddress()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("ordered-bar", bar); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getSlowestMode()); + processLowerFrame(packet, bar); + ASSERT(checkedBitmap); + } + void prepare() { + Enter_Method("prepare"); + delete recipientBlockAckAgreementHandler; + auto agreements = new SchedulingRecipientAgreements(); + recipientBlockAckAgreementHandler = agreements; + if (std::string(par("blockAckVariant").stringValue()) == "compressed") { + publishPrimaryChannel(); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:01"), mib->localHtCapabilities, mib->getHtOperation()); + } + agreements->accept(MacAddress("02:00:00:00:00:01"), std::string(par("blockAckVariant").stringValue()) == "compressed" ? BlockAckVariant::COMPRESSED : BlockAckVariant::BASIC); + subscribe(blockAckAgreementDeletedSignal, this); + } + void receive(Packet *packet) { + Enter_Method("receive"); + take(packet); + auto header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + if (data->getSequenceNumber() == SequenceNumberCyclic(0) && + (std::string(par("blockAckVariant").stringValue()) == "compressed" || data->getFragmentNumber() != 0)) { + delete packet; + return; + } + } + auto mode = packet->getTag()->getMode(); + packet->addTagIfAbsent()->setMode(mode); + processLowerFrame(packet, header); + } + bool retired() const { return recipientBlockAckAgreementHandler->getAgreements().empty(); } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == blockAckAgreementDeletedSignal) + ASSERT((delivered == std::vector{1, 2})); + else + Hcf::receiveSignal(source, signal, value, details); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr& header, + Packet *, const Ptr&) override { + if (auto response = dynamicPtrCast(header)) { + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(response->getBlockAckBitmap(0).getBit(0)); + ASSERT(!response->getBlockAckBitmap(0).getBit(1)); + if (response->getBlockAckBitmap(1).getBit(0) && response->getBlockAckBitmap(2).getBit(0)) { + checkedBitmap = true; + ASSERT(delivered.empty()); + } + } + if (auto response = dynamicPtrCast(header)) { + if (orderedReceipt) { + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(100)); + for (int bit = 0; bit < 64; bit++) + ASSERT(response->getBlockAckBitmap().getBit(bit) == (bit < 3)); + ASSERT((delivered == std::vector{100, 101, 102})); + checkedBitmap = true; + delete packet; + return; + } + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + if (response->getBlockAckBitmap().getBit(1) && response->getBlockAckBitmap().getBit(2)) { + checkedBitmap = true; + ASSERT(delivered.empty()); + } + } + delete packet; + } +}; +Define_Module(SchedulingRecipient); +class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, int size, Tid tid = 5) { + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTid(tid); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBufferSize(size); + request->setBlockAckPolicy(true); + createAgreement(request); + auto agreement = getAgreement(peer, tid); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + finishProcedure(peer, tid); + } +}; +class SchedulingHcf : public Hcf { + const MacAddress peer{"02:00:00:00:00:02"}; + const MacAddress other{"02:00:00:00:00:03"}; + int dataCount = 0; + int bars = 0; + int delbas = 0; + int refusedBars = 0; + int retries = 0; + int dataInTxop = 0; + bool responsePending = false; + Ptr pendingBar; + const physicallayer::IIeee80211Mode *pendingResponseMode = nullptr; + bool firstBarWasStandalone = false; + int expired = 0; + bool expiredDuringExchange = false; + int collisions = 0; + SchedulingRecipient *recipient = nullptr; + simtime_t responseEnd; + simtime_t txopDeadline; + int scenario() const { return par("scenario"); } + bool compressed() const { return std::string(par("blockAckVariant").stringValue()) == "compressed"; } + b responseLength() const { return compressed() ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK; } + Tid barTid(const Ptr& bar) const { + return compressed() ? check_and_cast(bar.get())->getTidInfo() : + check_and_cast(bar.get())->getTidInfo(); + } + SequenceNumberCyclic barStart(const Ptr& bar) const { + return compressed() ? check_and_cast(bar.get())->getStartingSequenceNumber() : + check_and_cast(bar.get())->getStartingSequenceNumber(); + } + int burst() const { + switch (scenario()) { + case 0: return 1; + case 1: return 4; + case 2: return 6; + case 3: return 9; + case 4: return 4; + case 5: return 1; + case 6: return 1; + case 7: return 2; + case 9: return 3; + case 10: return 1; + case 11: return 8; + case 12: return 1; + case 13: return 1; + case 14: return 1; + default: return 4; + } + } + int window() const { return scenario() == 2 || scenario() == 7 ? 1 : scenario() == 3 ? 4 : 64; } + public: + void begin() { + Enter_Method("begin"); + delete originatorBlockAckAgreementHandler; + auto agreements = new SchedulingAgreements(); + originatorBlockAckAgreementHandler = agreements; + if (compressed()) { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + mib->setPeerHtCapabilities(other, mib->localHtCapabilities, mib->getHtOperation()); + } + agreements->accept(peer, window()); + if (compressed() && scenario() == 0) + check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf"))->checkOrderedReceipt(); + if (scenario() == 9) { + recipient = check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf")); + recipient->prepare(); + subscribe(packetDroppedSignal, this); + } + if (scenario() == 4) + agreements->accept(other, 64); + if (scenario() == 18) + agreements->accept(peer, 64, 4); + rx->setContentionBlocked(true); + for (int i = 0; i < burst(); i++) + queueData(peer, scenario() == 9 ? (i == 0 ? 200 : 10) : scenario() == 7 && i == 0 ? 1100 : 100); + if (scenario() == 12 || scenario() == 14) { + auto frames = edca->getEdcaf(AC_VI)->getInProgressFrames(); + auto frame = frames->getFrameToTransmit(); + auto header = frame->removeAtFront(); + header->setAckPolicy(BLOCK_ACK); + frame->insertAtFront(header); + edca->getEdcaf(AC_VI)->getAckHandler()->processTransmittedDataOrMgmtFrame(header); + originatorBlockAckAgreementHandler->getAgreement(peer, 5)->recordTransmittedData( + frame->getTag()->getAllocationIndex()); + dataCount = 1; + queueData(other, 1100, 6); + } + if (scenario() == 4) { + for (int i = 0; i < 8; i++) + queueData(other, 100); + } + if (scenario() == 18) { + for (int i = 0; i < 8; i++) + queueData(peer, 100, 4); + } + rx->setContentionBlocked(false); + scheduleAfter(SimTime(40, SIMTIME_MS), new cMessage("verify")); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == packetDroppedSignal) { + auto drop = dynamic_cast(details); + if (drop && drop->getReason() == LIFETIME_EXPIRED) { + expired++; + expiredDuringExchange |= frameSequenceHandler->isSequenceRunning() && + originatorBlockAckAgreementHandler->getAgreement(peer, 5) != nullptr; + } + if (source == this) + return; + } + Hcf::receiveSignal(source, signal, value, details); + } + void queueData(MacAddress receiver, int length, Tid tid = 5) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(receiver); + auto packet = new Packet("data", header); + packet->insertAtBack(makeShared(B(length))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void frameSequenceStarted() override { + dataInTxop = 0; + Hcf::frameSequenceStarted(); + } + void channelGranted(IChannelAccess *access) override { + auto collided = edca->getInternallyCollidedEdcafs(); + if (scenario() == 12 || scenario() == 14) + collisions += collided.size(); + Hcf::channelGranted(access); + if (scenario() == 14 && !collided.empty()) { + auto edcaf = edca->getEdcaf(AC_VI); + ASSERT(edcaf->getCw() == 1); + auto frame = edcaf->getInProgressFrames()->getFrames(0); + bool absent = false; + try { + edcaf->getRecoveryProcedure()->getRetryCount(frame, frame->peekAtFront()); + } + catch (const cRuntimeError& error) { + absent = std::string(error.what()).find("retry counter entry doesn't exist") != std::string::npos; + } + ASSERT(absent); + } + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + auto bar = dynamicPtrCast(header); + if (bar) { + auto frames = edca->getChannelOwner()->getInProgressFrames(); + auto candidate = frames->peekFrameToTransmit(); + if (candidate && !frames->containsFrame(candidate)) { + auto sequence = candidate->peekAtFront()->getSequenceNumber(); + auto size = frames->getLength(); + ASSERT(frames->peekFrameToTransmit() == candidate); + ASSERT(frames->getLength() == size); + ASSERT(candidate->peekAtFront()->getSequenceNumber() == sequence); + } + } + bool accepted = Hcf::transmitFrame(packet, ifs); + if (bar && !accepted) { + refusedBars++; + return false; + } + if (!accepted) + return false; + header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + auto txop = edca->getChannelOwner()->getTxopProcedure(); + if (!par("multiple").boolValue() && txop->getLimit() == 0 && data->getAckPolicy() == BLOCK_ACK) + ASSERT(data->getDurationField() == SIMTIME_ZERO); + if (!par("multiple").boolValue() && scenario() == 3) { + ASSERT(dataInTxop == 0); + ASSERT(data->getDurationField() == SIMTIME_ZERO); + } + if (scenario() == 4 && data->getReceiverAddress() == other) + ASSERT(bars >= 1); + if (scenario() == 18 && data->getTid() == 4) + ASSERT(bars >= 1); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + if ((scenario() == 12 || scenario() == 14) && data->getTid() == 6) + ASSERT(!data->getRetry()); + else + ASSERT(agreement != nullptr && agreement->containsSequence(data->getSequenceNumber())); + if (scenario() != 7 && data->getSequenceNumber().get() >= window() && data->getReceiverAddress() == peer) + ASSERT(bars >= 1); + if (data->getRetry()) + retries++; + dataCount++; + dataInTxop++; + } + if (bar) { + bars++; + if (bars == 1) { + firstBarWasStandalone = dataInTxop == 0; + ASSERT(bar->getReceiverAddress() == peer); + ASSERT(barTid(bar) == 5); + if (scenario() != 9) + ASSERT(dataCount == (scenario() == 2 ? 1 : scenario() == 3 ? 4 : scenario() == 7 || scenario() == 12 || scenario() == 14 ? 2 : scenario() == 11 ? 5 : burst())); + } + auto mode = packet->getTag()->getMode(); + auto responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(responseLength()); + auto txop = edca->getChannelOwner()->getTxopProcedure(); + auto end = simTime() + ifs + cost; + if (dataInTxop > 1 && txop->getLimit() > 0) + ASSERT(end <= txop->getStart() + txop->getLimit()); + std::cout << "BAR timing: scenario=" << scenario() << " multiple=" << par("multiple").boolValue() + << " start=" << simTime() << " ifs=" << ifs << " bytes=" << packet->getByteLength() + << " rate=" << mode->getDataMode()->getNetBitrate() << " responseRate=" << responseMode->getDataMode()->getNetBitrate() + << " response=" << responseMode->getDuration(responseLength()) << " end=" << end + << " deadline=" << txop->getStart() + txop->getLimit() << " data=" << dataInTxop << "\n"; + } + if (auto delba = dynamicPtrCast(header)) { + ASSERT(delba->getInitiator()); + ASSERT(!hasQueuedData(delba->getReceiverAddress(), delba->getTid())); + ASSERT(!hasNumberedData(delba->getReceiverAddress(), delba->getTid())); + delbas++; + } + return true; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto bar = dynamicPtrCast(header); + auto data = dynamicPtrCast(header); + if (recipient && (data || bar || dynamicPtrCast(header))) + recipient->receive(packet->dup()); + bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); + if (bar || ack) { + if (!(bar && scenario() == 5 && bars == 1)) { + pendingBar = bar; + responsePending = true; + pendingResponseMode = bar ? + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT) : + rateSelection->computeResponseAckFrameMode(packet, dynamicPtrCast(header), ResponseQueryRole::PREDICT); + auto duration = pendingResponseMode->getDuration(bar ? responseLength() : LENGTH_ACK); + responseEnd = simTime() + modeSet->getSifsTime() + duration; + txopDeadline = edca->getChannelOwner()->getTxopProcedure()->getStart() + edca->getChannelOwner()->getTxopProcedure()->getLimit(); + scheduleAt(responseEnd, new cMessage("response")); + } + } + Hcf::transmissionComplete(packet, header); + } + bool isReceptionInProgress() override { return responsePending || Hcf::isReceptionInProgress(); } + void handleMessage(cMessage *message) override { + if (message->isName("response")) { + delete message; + responsePending = false; + Ptr header; + if (pendingBar) { + if (compressed()) { + auto response = makeShared(); + response->setTransmitterAddress(pendingBar->getReceiverAddress()); + response->setTidInfo(barTid(pendingBar)); + auto responseStart = barStart(pendingBar); + if (bars == 1 && scenario() >= 15 && scenario() <= 17) + responseStart = responseStart + (scenario() == 15 ? -2 : scenario() == 16 ? 1 : 100); + response->setStartingSequenceNumber(responseStart); + BitVector bits; + for (int i = 0; i < 64; i++) + bits.appendBit(scenario() == 9 ? i != 0 : !(bars == 1 && (scenario() == 6 || scenario() == 17))); + response->setBlockAckBitmap(bits); + header = response; + } + else { + auto response = makeShared(); + response->setTransmitterAddress(pendingBar->getReceiverAddress()); + response->setTidInfo(barTid(pendingBar)); + response->setStartingSequenceNumber(barStart(pendingBar)); + for (int i = 0; i < 64; i++) { + BitVector bits; + for (int j = 0; j < 16; j++) + bits.appendBit(scenario() == 9 ? (i != 0 || j == 0) : !(scenario() == 6 && bars == 1)); + response->setBlockAckBitmap(i, bits); + } + header = response; + } + } + else + header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("response", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(pendingResponseMode); + pendingBar = nullptr; + processLowerFrame(packet, header); + } + else if (message->isName("verify")) { + delete message; + std::cout << "Scheduling totals: data=" << dataCount << " bars=" << bars << " delbas=" << delbas << " refused=" << refusedBars << "\n"; + ASSERT(bars > 0); + ASSERT(delbas == (scenario() == 4 || scenario() == 18 ? 2 : 1)); + if (scenario() != 9) + ASSERT(dataCount == burst() + (scenario() == 4 || scenario() == 18 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 || (compressed() && scenario() == 16) ? 1 : compressed() && scenario() == 17 ? 4 : 0)); + else { + ASSERT(recipient->checkedBitmap && recipient->retired()); + ASSERT((recipient->delivered == std::vector{1, 2})); + ASSERT(dataCount > 3); + ASSERT(expired > 0); + if (compressed()) + ASSERT(expiredDuringExchange); + } + ASSERT(originatorBlockAckAgreementHandler->getAgreements().empty()); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().empty()); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + if (scenario() == 5 || scenario() == 6) + ASSERT(retries == 1 && bars == 2); + if (compressed() && scenario() >= 15 && scenario() <= 17) + ASSERT(retries == (scenario() == 15 ? 0 : scenario() == 16 ? 1 : 4) && bars == (scenario() == 15 ? 1 : 2)); + if (scenario() == 8 || scenario() == 10 || scenario() == 13) + ASSERT(firstBarWasStandalone); + if (scenario() == 12 || scenario() == 14) + ASSERT(collisions == 1 && retries == 0); + std::cout << "Block Ack scheduling assertions passed.\n"; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(SchedulingHcf); +class SchedulingTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(SchedulingTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module SchedulingHcf extends Hcf { parameters: int scenario; bool multiple; @class(::inet::ieee80211::SchedulingHcf); } +module SchedulingRecipient extends Hcf { parameters: @class(::inet::ieee80211::SchedulingRecipient); } +simple SchedulingTest { parameters: @class(::inet::ieee80211::SchedulingTest); } +network SchedulingNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + recipient: WirelessHost; + test: SchedulingTest; +} + +%inifile: omnetpp.ini +[General] +network = SchedulingNetwork +ned-path = .;../../../../src +sim-time-limit = 50ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = ${variant} == "basic" ? "a" : "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].radio.channelNumber = 0 +*.host.wlan[*].address = "02:00:00:00:00:01" +*.recipient.wlan[*].address = "02:00:00:00:00:02" +**.wlan[*].mac.qosStation = true +*.recipient.wlan[*].mac.hcf.typename = "SchedulingRecipient" +*.host.wlan[*].mac.hcf.typename = "SchedulingHcf" +**.wlan[*].mac.hcf.scenario = ${scenario=0..18} +**.wlan[*].mac.hcf.multiple = ${multiple=false,true} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = ${variant="basic","compressed"} +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.originatorAckPolicy.blockAckTimeout = 1ms +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 2346B +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 && ${variant} == "basic" ? 128B : 2346B +**.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 10ms : 512ms +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[2].cwMax = ${scenario} == 14 ? 3 : ${scenario} == 12 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[2].recoveryProcedure.shortRetryLimit = ${scenario} == 14 ? 1 : 7 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = ${multiple} ? "multiple" : "single" +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = ${scenario} == 8 ? 900us : ${scenario} == 10 ? 0s : ${scenario} == 11 ? 1160us : ${scenario} == 13 ? 50us : 10ms +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 2000000m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.recipient.mobility.initialX = 1000000m +*.host.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack scheduling assertions passed. diff --git a/tests/module/Ieee80211BlockAckVariant_1.test b/tests/module/Ieee80211BlockAckVariant_1.test index 61da5b30adc..195c298c73a 100644 --- a/tests/module/Ieee80211BlockAckVariant_1.test +++ b/tests/module/Ieee80211BlockAckVariant_1.test @@ -1,8 +1,8 @@ %description: Check profile validation through production HCF initialization. -Retain the default and explicit Basic profile. +Accept the default, explicit Basic, and explicit Compressed profiles. Reject unknown values with support enabled or disabled. -Reject Compressed until its runtime exchange path exists. +Reject delayed policy for Compressed before any ADDBA transmission. %testprog: python3 check-profile.py @@ -18,20 +18,26 @@ import subprocess root = Path.cwd().parents[3] cases = [ - (None, True, None), - ("basic", True, None), - ("invalid", True, 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), - ("invalid", False, 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), - ("compressed", True, 'Compressed Block Ack exchanges are not implemented'), - ("compressed", False, 'Compressed Block Ack exchanges are not implemented'), + (None, True, (), None), + ("basic", True, (), None), + ("invalid", True, (), 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), + ("invalid", False, (), 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), + ("compressed", True, (), None), + ("compressed", False, (), None), ] -for variant, supported, diagnostic in cases: +for policies in [("originator",), ("recipient",), ("originator", "recipient")]: + cases.append(("compressed", True, policies, + "Compressed Block Ack does not support delayedAckPolicySupported")) + cases.append(("basic", True, policies, None)) +for variant, supported, policies, diagnostic in cases: command = [str(root / "bin/inet"), "--debug", "-u", "Cmdenv", "-f", str(Path.cwd() / "profile.ini"), "-c", "General", "-r", "0", "--sim-time-limit=0s", "--record-vector-results=false", "--record-scalar-results=false", f"--result-dir={Path.cwd() / 'results'}", f"--**.wlan[*].mac.hcf.isBlockAckSupported={'true' if supported else 'false'}"] if variant is not None: command.append(f'--**.wlan[*].mac.hcf.blockAckVariant="{variant}"') + for policy in policies: + command.append(f'--**.wlan[*].mac.hcf.{policy}BlockAckAgreementPolicy.delayedAckPolicySupported=true') result = subprocess.run(command, cwd=root / "showcases/wireless/blockack", text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print(result.stdout) diff --git a/tests/module/Ieee80211CompressedBlockAckAdmission_1.test b/tests/module/Ieee80211CompressedBlockAckAdmission_1.test new file mode 100644 index 00000000000..d3c46e46cb0 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckAdmission_1.test @@ -0,0 +1,665 @@ +%description: +Check reversible DATA preparation and Compressed TXOP admission through HCF, EDCA, frame sequences, and Tx. +The fixture supplies accepted agreements and peer capabilities. It injects timed CTS, ACK, and Block Ack responses at HCF. +Service cases check queue ownership, deadlines, sequence assignment, and allocation before refusal and after commitment. +Runtime cases check ordinary fallback, one mode decision, RTS recovery, expiry, capability loss, and mode-set changes. +Ordinary commitment preserves elapsed queueing time and one queue pull; retransmission retains that measurement without another pull. +This fixture does not establish association behavior. The infrastructure exchange fixture supplies that evidence. + +%extraargs: -r 0..23 + +%file: Admission.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class AdmissionService : public OriginatorQosMacDataService { + public: + int preparations = 0; + int commits = 0; + int64_t allocations() const { return nextDataAllocationIndex; } + std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool aggregate) override { + preparations++; + return OriginatorQosMacDataService::prepareFrame(queue, aggregate); + } + Packet *commitPreparedFrame(PreparedQosData& preparation) override { + auto preview = preparation.packet.get(); + auto result = OriginatorQosMacDataService::commitPreparedFrame(preparation); + if (result) { + ASSERT(result == preview); + commits++; + } + return result; + } +}; +Define_Module(AdmissionService); + +class AdmissionRecovery : public QosRecoveryProcedure { + public: + int controls = 0; + int dataFailures = 0; + int numberedRtsFailures = 0; + std::vector qsrcHistory; + int qsrc() const { return stationShortRetryCounter; } + size_t numberedCounters() const { return shortRetryCounter.size() + longRetryCounter.size(); } + void controlFrameTransmissionFailed() override { + controls++; + QosRecoveryProcedure::controlFrameTransmissionFailed(); + qsrcHistory.push_back(stationShortRetryCounter); + } + void dataFrameTransmissionFailed(Packet *packet, const Ptr& header) override { + dataFailures++; + QosRecoveryProcedure::dataFrameTransmissionFailed(packet, header); + } + void rtsFrameTransmissionFailed(const Ptr& header) override { + numberedRtsFailures++; + QosRecoveryProcedure::rtsFrameTransmissionFailed(header); + } +}; +Define_Module(AdmissionRecovery); + +class AdmissionRateControl : public ModeSetListener, public IRateControl { + public: + int queries = 0; + int failedDataFeedback = 0; + const physicallayer::IIeee80211Mode *getRate(const MacAddress&) override { + queries++; + int scenario = getParentModule()->par("scenario"); + int rate = scenario == 22 ? 6 : scenario == 6 || scenario == 19 || scenario == 20 ? (queries % 2 ? 6 : 24) : + scenario == 17 ? (queries <= 2 ? 24 : 6) : queries % 2 ? 24 : 6; + return modeSet->getMode(Mbps(rate)); + } + void frameTransmitted(Packet *packet, int, bool successful, bool) override { + if (!successful && dynamicPtrCast(packet->peekAtFront())) + failedDataFeedback++; + } + void frameReceived(Packet *) override {} +}; +Define_Module(AdmissionRateControl); + +class AdmissionAgreements : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTid(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + request->setAMsduSupported(true); + createAgreement(request); + auto agreement = getAgreement(peer, 5); + agreement->setIsAddbaResponseReceived(true); + agreement->setResponseAMsduSupported(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + finishProcedure(peer, 5); + } +}; + +class AdmissionHcf : public Hcf { + const MacAddress peer{"02:00:00:00:00:02"}; + Ptr pendingRts; + Ptr pendingAck; + bool pendingBasic = false; + Tid pendingTid = -1; + SequenceNumberCyclic pendingStart; + const physicallayer::IIeee80211Mode *responseMode = nullptr; + simtime_t firstDeadline; + bool responsePending = false; + b firstDataLength; + int failuresWanted() const { return scenario() == 7 ? 1 : scenario() == 8 ? 2 : scenario() == 9 ? 1000 : 0; } + public: + std::vector sourceIds; + std::vector sourcePackets; + std::vector sourceOwners; + std::vector deadlines; + int data = 0; + int scopedData = 0; + int rts = 0; + int aggregates = 0; + int refusedSequences = 0; + int mutation = 0; + int queueRemovals = 0; + int aggregationEvents = 0; + int queuePulls = 0; + int ordinaryMeasurements = 0; + simtime_t ordinaryQueueTime; + int internalCollisions = 0; + std::vector queriesAtData; + int scenario() const { return par("scenario"); } + AdmissionService *service() const { return check_and_cast(getSubmodule("originatorMacDataService")); } + AdmissionRecovery *recovery() const { return check_and_cast(edca->getEdcaf(AC_VI)->getRecoveryProcedure()); } + AdmissionRateControl *control() const { return check_and_cast(getSubmodule("rateControl")); } + queueing::IPacketQueue *queue() const { return edca->getEdcaf(AC_VI)->getPendingQueue(); } + EdcaTransmitLifetimeHandler *lifetime() const { return edca->getLifetimeHandler(); } + InProgressFrames *frames() const { return edca->getEdcaf(AC_VI)->getInProgressFrames(); } + void setup() { + Enter_Method("setup"); + rx->setContentionBlocked(true); + Ieee80211RateSetState rates; + rates.supported.known = rates.basic.known = rates.operational.known = true; + rates.supported.legacyRates = rates.basic.legacyRates = rates.operational.legacyRates = {Mbps(6), Mbps(12), Mbps(24)}; + mib->setLocalRateSet(rates); + mib->installBssAndPeerRateSets(rates, peer, rates); + if (scenario() != 16) { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + delete originatorBlockAckAgreementHandler; + auto agreements = new AdmissionAgreements(); + originatorBlockAckAgreementHandler = agreements; + agreements->accept(peer); + } + int firstLength = scenario() == 13 ? 1400 : scenario() == 17 ? 2000 : 700; + if (scenario() == 21) + enqueue(100, 5, false, true); + enqueue(firstLength); + enqueue(scenario() == 17 ? 100 : 700); + if (scenario() == 17) + enqueue(700); + if (scenario() == 18) + enqueue(100, 6, false); + firstDeadline = deadlines.front(); + check_and_cast(queue())->subscribe(packetRemovedSignal, this); + check_and_cast(queue())->subscribe(packetPulledSignal, this); + service()->subscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + } + void enqueue(int bytes, Tid tid = 5, bool record = true, bool group = false) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto receiver = group ? MacAddress("01:00:5e:00:00:01") : peer; + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(receiver); + auto packet = new Packet("admission-data", header); + packet->insertAtBack(makeShared(B(bytes))); + packet->insertAtBack(makeShared()); + auto time = packet->addRegionTag(header->getChunkLength(), B(bytes)); + time->setPacketTotalTimesArraySize(1); + time->setPacketTotalTimes(0, SIMTIME_ZERO); + packet->setArrivalTime(simTime()); + processUpperFrame(packet, header); + if (record) { + sourcePackets.push_back(packet); + sourceIds.push_back(packet->getId()); + deadlines.push_back(lifetime()->getDeadline(packet)); + sourceOwners.push_back(packet->getOwner()); + } + } + void unchanged(int expectedAllocations = 0) const { + ASSERT(queue()->getNumPackets() == int(sourceIds.size())); + ASSERT(frames()->getLength() == 0 && service()->allocations() == expectedAllocations); + ASSERT(service()->getNextSequenceNumber(peer, 5) == SequenceNumberCyclic(0)); + for (int i = 0; i < int(sourceIds.size()); i++) { + auto packet = queue()->getPacket(i); + ASSERT(packet == sourcePackets[i] && packet->getId() == sourceIds[i]); + ASSERT(packet->getOwner() == sourceOwners[i]); + ASSERT(packet->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(lifetime()->findFrame(sourceIds[i]) == packet && lifetime()->getDeadline(packet) == deadlines[i]); + } + } + void run() { Enter_Method("run"); rx->setContentionBlocked(false); } + void stop() { Enter_Method("stop"); rx->setContentionBlocked(true); } + void serviceCheck() { + Enter_Method("serviceCheck"); + auto preparation = service()->prepareFrame(queue(), scenario() != 1); + ASSERT(preparation && preparation->packet && preparation->sourceIds.size() == (scenario() == 1 ? 1 : 2)); + ASSERT(preparation->aggregate == (scenario() != 1)); + unchanged(); + ASSERT(queueRemovals == 0 && aggregationEvents == 0); + auto preview = preparation->packet.get(); + ASSERT(lifetime()->findFrame(preview->getId()) == nullptr); + ASSERT(service()->isPreparationValid(*preparation, simTime())); + if (scenario() == 0) { + preparation.reset(); + unchanged(); + ASSERT(queueRemovals == 0 && aggregationEvents == 0); + } + else if (scenario() == 1 || scenario() == 2) { + auto committed = service()->commitPreparedFrame(*preparation); + ASSERT(committed == preview && !preparation->packet); + ASSERT(service()->allocations() == 1 && service()->getNextSequenceNumber(peer, 5) == SequenceNumberCyclic(1)); + ASSERT(committed->getTag()->getAllocationIndex() == 0); + ASSERT(lifetime()->findFrame(sourceIds.front()) == nullptr); + ASSERT(lifetime()->getDeadline(committed) == (scenario() == 1 ? firstDeadline : simTime() + SimTime(100, SIMTIME_MS))); + ASSERT(queue()->getNumPackets() == (scenario() == 1 ? 1 : 0)); + ASSERT(queueRemovals == (scenario() == 1 ? 1 : 2)); + ASSERT(aggregationEvents == (scenario() == 1 ? 0 : 1)); + if (scenario() == 1) { + ASSERT(queue()->getPacket(0) == sourcePackets[1]); + ASSERT(lifetime()->getDeadline(sourcePackets[1]) == deadlines[1]); + } + lifetime()->forgetFrame(committed->getId()); + take(committed); + delete committed; + } + else if (scenario() == 3) { + auto removed = queue()->dequeuePacket(); + lifetime()->forgetFrame(removed->getId()); + take(removed); + delete removed; + ASSERT(!service()->isPreparationValid(*preparation, simTime())); + ASSERT(service()->commitPreparedFrame(*preparation) == nullptr); + ASSERT(service()->allocations() == 0 && queue()->getNumPackets() == 1); + } + else if (scenario() == 4) { + ASSERT(!service()->isPreparationValid(*preparation, firstDeadline)); + unchanged(); + } + else { + service()->setEligibility([](Packet *) { return true; }, [](Packet *) { return false; }); + ASSERT(!service()->isPreparationValid(*preparation, simTime())); + ASSERT(service()->commitPreparedFrame(*preparation) == nullptr); + unchanged(); + } + } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LOCAL) { + // Replace fixed submodules before their parent caches pointers. + getSubmodule("originatorMacDataService")->deleteModule(); + auto service = cModuleType::get("AdmissionService")->create("originatorMacDataService", this); + service->finalizeParameters(); + service->buildInside(); + auto access = getSubmodule("edca"); + for (int i = 0; i < 4; i++) { + auto edcaf = access->getSubmodule("edcaf", i); + edcaf->getSubmodule("recoveryProcedure")->deleteModule(); + auto recovery = cModuleType::get("AdmissionRecovery")->create("recoveryProcedure", edcaf); + recovery->finalizeParameters(); + recovery->buildInside(); + } + } + Hcf::initialize(stage); + } + void channelGranted(IChannelAccess *access) override { + auto collided = edca->getInternallyCollidedEdcafs(); + Hcf::channelGranted(access); + if (scenario() == 18 && !collided.empty()) { + internalCollisions += collided.size(); + ASSERT(frames()->getLength() == 1 && queue()->getNumPackets() == 1); + auto committed = frames()->getFrames(0); + auto header = committed->peekAtFront(); + ASSERT(!header->getAMsduPresent() && !header->getRetry()); + ASSERT(queue()->getPacket(0) == sourcePackets[1]); + ASSERT(lifetime()->getDeadline(sourcePackets[1]) == deadlines[1]); + ASSERT(recovery()->dataFailures == 1 && recovery()->getRetryCount(committed, header) == 1); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == packetRemovedSignal || signal == packetPulledSignal) { + queueRemovals++; + if (signal == packetPulledSignal) + queuePulls++; + } + else if (signal == IOriginatorMacDataService::packetAggregatedSignal) + aggregationEvents++; + else + Hcf::receiveSignal(source, signal, value, details); + } + bool isReceptionInProgress() override { return responsePending || Hcf::isReceptionInProgress(); } + void frameSequenceFinished() override { + if (data == 0 && rts > 0 && scenario() != 9 && mutation == 0) { + if (scenario() != 18) + unchanged(); + ASSERT(recovery()->numberedCounters() == 0); + } + if (scenario() == 13 && data == 1) + refusedSequences++; + Hcf::frameSequenceFinished(); + if ((scenario() >= 22 ? data >= 2 : scenario() == 18 ? scopedData > 0 : data > 0) || mutation > 0) + rx->setContentionBlocked(true); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + auto context = frameSequenceHandler->getContext(); + if (auto protectedRts = dynamicPtrCast(header)) { + auto state = context->getQoSContext()->preparedData.get(); + if (state) { + if (state->preparation) { + ASSERT(state->frame == state->preparation->packet.get()); + ASSERT(!frames()->containsFrame(state->frame)); + if (scenario() != 18) + unchanged(); + } + else + ASSERT(frames()->containsFrame(state->frame)); + } + else + ASSERT(scenario() == 15 || scenario() == 16 || scenario() == 18); + pendingRts = protectedRts; + } + bool accepted = Hcf::transmitFrame(packet, ifs); + if (!accepted) + return false; + header = packet->peekAtFront(); + auto mode = packet->getTag()->getMode(); + auto end = simTime() + ifs + mode->getDuration(packet->getDataLength()); + if (dynamicPtrCast(header)) { + pendingRts = staticPtrCast(header); + rts++; + if (scenario() == 19 || scenario() == 20) { + auto state = context->getQoSContext()->preparedData.get(); + ASSERT(state && !state->frame->peekAtFront()->getAMsduPresent()); + auto required = modeSet->getSifsTime() + state->ctsMode->getDuration(LENGTH_CTS) + + modeSet->getSifsTime() + state->dataMode->getDuration(state->frame->getDataLength()); + if (state->ackMode) + required += modeSet->getSifsTime() + state->ackMode->getDuration(LENGTH_ACK); + ASSERT(pendingRts->getDurationField() >= required); + } + if (rts == 1 && (scenario() == 10 || scenario() == 11 || scenario() == 12)) + scheduleAt(end + SimTime(1, SIMTIME_PS), new cMessage("mutation")); + if (rts > failuresWanted()) { + responsePending = true; + responseMode = rateSelection->computeResponseCtsFrameMode(packet, pendingRts, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_CTS), new cMessage("cts")); + } + } + else if (auto ordinary = dynamicPtrCast(header)) { + data++; + scopedData += ordinary->getTid() == 5 && !ordinary->getReceiverAddress().isMulticast(); + aggregates += ordinary->getAMsduPresent(); + queriesAtData.push_back(control()->queries); + if (scenario() != 15 && scenario() != 16 && ordinary->getTid() == 5 && !ordinary->getReceiverAddress().isMulticast()) { + auto state = context->getQoSContext()->preparedData.get(); + ASSERT(state && frames()->containsFrame(packet) && state->dataMode == mode); + if (!ordinary->getAMsduPresent() && (scenario() == 6 || scenario() == 19 || scenario() == 20 || scenario() == 22)) { + auto offset = ordinary->getChunkLength(); + auto time = packet->findRegionTag(offset, packet->getDataLength() - offset - B(4)); + if (!ordinary->getRetry()) { + ASSERT(time && time->getPacketTotalTimes(0) == simTime() - SimTime(1, SIMTIME_MS)); + ASSERT(queuePulls >= 1); + ordinaryQueueTime = time->getPacketTotalTimes(0); + ordinaryMeasurements++; + } + else if (scenario() == 22) + ASSERT(time && time->getPacketTotalTimes(0) == ordinaryQueueTime && queuePulls == 1); + } + } + if (scenario() == 20) + ASSERT(ordinary->getAckPolicy() == NORMAL_ACK && ordinary->getDurationField() >= modeSet->getSifsTime() + + rateSelection->computeResponseAckFrameMode(packet, ordinary, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK)); + if (scenario() >= 22 && data == 2) { + ASSERT(ordinary->getRetry()); + ASSERT(packet->getDataLength() == firstDataLength); + ASSERT(mode->getDuration(packet->getDataLength()) > SimTime(1, SIMTIME_MS)); + ASSERT(ordinary->getAMsduPresent() == (scenario() == 23)); + } + if (scenario() >= 22 && data == 1) { + firstDataLength = packet->getDataLength(); + ASSERT(!ordinary->getRetry()); + return true; // No ACK forces a retry of the same committed MPDU. + } + if (ordinary->getAckPolicy() == NORMAL_ACK) { + responsePending = true; + pendingAck = ordinary; + responseMode = rateSelection->computeResponseAckFrameMode(packet, ordinary, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_ACK), new cMessage("ack")); + } + } + else if (auto management = dynamicPtrCast(header)) { + responsePending = true; + pendingAck = management; + responseMode = rateSelection->computeResponseAckFrameMode(packet, management, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_ACK), new cMessage("ack")); + } + else if (auto bar = dynamicPtrCast(header)) { + responsePending = true; + pendingBasic = dynamicPtrCast(bar) != nullptr; + pendingTid = pendingBasic ? staticPtrCast(bar)->getTidInfo() : + staticPtrCast(bar)->getTidInfo(); + pendingStart = pendingBasic ? staticPtrCast(bar)->getStartingSequenceNumber() : + staticPtrCast(bar)->getStartingSequenceNumber(); + responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); + scheduleAt(end + modeSet->getSifsTime() + responseMode->getDuration(pendingBasic ? LENGTH_BASIC_BLOCKACK : LENGTH_COMPRESSED_BLOCKACK), new cMessage("ba")); + } + return true; + } + void receive(Packet *packet, const Ptr& header) { + packet->addTag()->setMode(responseMode); + if (!frameSequenceHandler->isSequenceRunning()) { + delete packet; + return; + } + processLowerFrame(packet, header); + } + void handleMessage(cMessage *message) override { + if (message->isName("mutation")) { + delete message; + mutation++; + int queries = control()->queries; + if (scenario() == 10) + mib->removePeerHtCapabilities(peer); + else if (scenario() == 11) { + auto rates = mib->getBssRateSet(); + rates.basic.legacyRates.erase(Mbps(12)); + mib->setBssRateSet(rates); + } + else { + auto radio = check_and_cast(getModuleByPath("^.^.radio")); + radio->setMode(modeSet->getMode(Mbps(24))); + auto replacement = physicallayer::Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setModeSet(replacement); + getContainingNicModule(this)->emit(modesetChangedSignal, replacement); + } + ASSERT(control()->queries == queries); + if (scenario() != 11) + rx->setContentionBlocked(true); + return; + } + if (message->isName("cts")) { + delete message; + responsePending = false; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + header->setDurationField(std::max(SIMTIME_ZERO, pendingRts->getDurationField() - modeSet->getSifsTime() - responseMode->getDuration(LENGTH_CTS))); + receive(new Packet("synthetic-cts", header), header); + return; + } + if (message->isName("ack")) { + delete message; + responsePending = false; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + receive(new Packet("synthetic-ack", header), header); + return; + } + if (message->isName("ba")) { + delete message; + responsePending = false; + Ptr header; + if (pendingBasic) { + auto basic = makeShared(); + basic->setTidInfo(pendingTid); + basic->setStartingSequenceNumber(pendingStart); + for (int i = 0; i < 64; i++) basic->setBlockAckBitmap(i, BitVector("1000000000000000")); + header = basic; + } + else { + auto compressed = makeShared(); + compressed->setTidInfo(pendingTid); + compressed->setStartingSequenceNumber(pendingStart); + compressed->setBlockAckBitmap(BitVector(std::string(64, '1').c_str())); + header = compressed; + } + header->setReceiverAddress(mac->getAddress()); + header->setTransmitterAddress(peer); + receive(new Packet("synthetic-ba", header), header); + return; + } + Hcf::handleMessage(message); + } +}; +Define_Module(AdmissionHcf); + +class AdmissionTest : public cSimpleModule { + public: + AdmissionTest() : cSimpleModule(131072) {} + protected: + void activity() override { + wait(SimTime(1, SIMTIME_MS)); + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + hcf->setup(); + int scenario = par("scenario"); + if (scenario < 6) + hcf->serviceCheck(); + else { + hcf->run(); + wait(SimTime(20, SIMTIME_MS)); + hcf->stop(); + if (scenario == 6 || scenario == 19 || scenario == 20 || scenario == 22) + ASSERT(hcf->ordinaryMeasurements == 1 && hcf->queuePulls == 1); + auto recovery = hcf->recovery(); + ASSERT(recovery->dataFailures == (scenario == 18 || scenario >= 22 ? 1 : 0)); + ASSERT(hcf->control()->failedDataFeedback == (scenario >= 22 ? 1 : 0)); + if (scenario == 9) { + ASSERT(hcf->rts > 0 && hcf->data == 0 && hcf->queue()->isEmpty()); + ASSERT(hcf->service()->allocations() == 0); + int queries = hcf->control()->queries; + wait(SimTime(1, SIMTIME_MS)); + ASSERT(hcf->control()->queries == queries); + } + else if (scenario == 10 || scenario == 12) { + ASSERT(hcf->data == 0 && hcf->mutation == 1); + hcf->unchanged(); + ASSERT(hcf->control()->queries == 1 && recovery->numberedCounters() == 0); + } + else { + ASSERT(hcf->data > 0); + if (scenario == 6 || scenario == 19 || scenario == 20) { + ASSERT(hcf->data == 1 && hcf->aggregates == 0 && hcf->queriesAtData.front() == 1); + ASSERT(hcf->queue()->getNumPackets() == 1 && hcf->service()->allocations() == 1); + ASSERT(hcf->lifetime()->getDeadline(hcf->queue()->getPacket(0)) == hcf->deadlines[1]); + } + if (scenario == 7 || scenario == 8) { + ASSERT(recovery->controls == (scenario == 7 ? 1 : 2)); + ASSERT(recovery->numberedRtsFailures == 0); + ASSERT(hcf->queriesAtData.front() == (scenario == 7 ? 2 : 3)); + ASSERT(recovery->qsrcHistory[0] == 1); + if (scenario == 8) ASSERT(recovery->qsrcHistory[1] == 0); + } + else + ASSERT(recovery->controls == 0); + if (scenario == 11) + ASSERT(hcf->queriesAtData.front() == 1 && hcf->mutation == 1); + if (scenario == 13) { + ASSERT(hcf->data == 1 && hcf->refusedSequences == 1); + ASSERT(hcf->queue()->getNumPackets() == 1 && hcf->service()->allocations() == 1); + ASSERT(hcf->queue()->getPacket(0) == hcf->sourcePackets[1]); + ASSERT(hcf->lifetime()->getDeadline(hcf->queue()->getPacket(0)) == hcf->deadlines[1]); + } + if (scenario == 14) + ASSERT(hcf->data == 1 && hcf->aggregates == 1 && hcf->queriesAtData.front() == 1); + if (scenario == 15 || scenario == 16) + ASSERT(hcf->service()->preparations == 0); + if (scenario == 17) { + ASSERT(hcf->data == 2 && hcf->aggregates == 0); + ASSERT(hcf->queriesAtData[0] == 1 && hcf->queriesAtData[1] == 2); + ASSERT(hcf->queue()->getNumPackets() == 1 && hcf->service()->allocations() == 2); + } + if (scenario == 18) { + ASSERT(hcf->internalCollisions == 1 && hcf->scopedData == 1 && hcf->aggregates == 0); + ASSERT(hcf->queue()->getNumPackets() == 1); + ASSERT(hcf->lifetime()->getDeadline(hcf->queue()->getPacket(0)) == hcf->deadlines[1]); + } + if (scenario == 21) { + ASSERT(hcf->data == 1 && hcf->scopedData == 0 && hcf->service()->commits == 0); + ASSERT(hcf->service()->preparations > 0); + hcf->unchanged(1); + } + if (scenario >= 22) { + ASSERT(hcf->data == 2 && hcf->aggregates == (scenario == 23 ? 2 : 0)); + ASSERT(hcf->service()->commits == 1 && hcf->service()->allocations() == 1); + ASSERT(hcf->queue()->getNumPackets() == (scenario == 22 ? 1 : 0)); + } + } + } + std::cout << "Compressed admission scenario " << scenario << " passed.\n"; + endSimulation(); + } +}; +Define_Module(AdmissionTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.originator.OriginatorQosMacDataService; +import inet.linklayer.ieee80211.mac.originator.QosRecoveryProcedure; +import inet.linklayer.ieee80211.mac.contract.IRateControl; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module AdmissionService extends OriginatorQosMacDataService { parameters: @class(::inet::ieee80211::AdmissionService); } +simple AdmissionRecovery extends QosRecoveryProcedure { parameters: @class(::inet::ieee80211::AdmissionRecovery); } +simple AdmissionRateControl like IRateControl { parameters: @class(::inet::ieee80211::AdmissionRateControl); } +module AdmissionHcf extends Hcf { parameters: int scenario; @class(::inet::ieee80211::AdmissionHcf); } +simple AdmissionTest { parameters: int scenario; @class(::inet::ieee80211::AdmissionTest); @display("i=block/cogwheel"); } +network AdmissionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: AdhocHost; + test: AdmissionTest; +} + +%inifile: omnetpp.ini +[General] +network = AdmissionNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.scenario = ${scenario=0..23} +*.host.wlan[*].opMode = "n(mixed-2.4Ghz)" +*.host.wlan[*].mac.qosStation = true +*.host.wlan[*].mac.hcf.typename = "AdmissionHcf" +*.host.wlan[*].mac.hcf.scenario = ${scenario} +*.host.wlan[*].mac.hcf.isBlockAckSupported = true +*.host.wlan[*].mac.hcf.blockAckVariant = ${scenario} == 15 ? "basic" : "compressed" +*.host.wlan[*].mac.hcf.rateControl.typename = "AdmissionRateControl" +*.host.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = -1bps +*.host.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps +*.host.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = ${scenario} == 20 || ${scenario} >= 22 ? 0B : 10000B +*.host.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 +*.host.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 1600 +*.host.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +*.host.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = 1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 18 ? 0 : -1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].cwMax = ${scenario} == 18 ? 0 : -1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].aifsn = ${scenario} == 18 ? 2 : -1 +*.host.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 250us : 100ms +*.host.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 14 ? 0s : 1ms +*.host.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = ${scenario} == 13 || ${scenario} == 17 || ${scenario} == 19 || ${scenario} == 20 || ${scenario} == 21 ? "multiple" : "single" +*.host.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 500B +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +Compressed admission scenario 23 passed. diff --git a/tests/module/Ieee80211CompressedBlockAckExchange_1.test b/tests/module/Ieee80211CompressedBlockAckExchange_1.test new file mode 100644 index 00000000000..2988432ef82 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckExchange_1.test @@ -0,0 +1,427 @@ +%description: +Use real association and management rate publication for two Compressed Block Ack hops. +The STA sends thirty UDP payloads through the AP to the other STA. +Observe accepted ADDBA dialogs, matched Compressed BAR/BA, positive receipt bits, and final agreement retirement. +After setup, remove one BSS basic rate through the MIB API. Require the agreement and HT generation to remain unchanged. +Record actual frame modes, lengths, airtime, IFS, and TXOP bounds for both protection modes at 6 and 24 Mbps. +The medium uses zero propagation time. Compare local receipt completion with the exact TXOP endpoint without a timing allowance. +The fixture enforces DATA and ACK bounds under active Compressed agreements, with the permitted initial MSDU and same-size retry exceptions. +The fixture records overruns outside active agreements separately. StrictAllTiming retains the original aggregate assertion for inherited failures. +Repeat each case with a non-HT link. Require payload delivery without ADDBA or BAR. +Repeat each case with A-MSDU aggregation enabled. Require actual A-MSDUs under accepted agreements at 24 Mbps. +The LowestResponse configuration supplies two extra runs with a 6 Mbps BAR and BA, one per protection mode. + +%extraargs: -r 0..15 + +%file: Exchange.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ExchangeHcf : public Hcf { + struct DataScope { + bool activeCompressed; + AckPolicy policy; + }; + std::map dataScopes; + struct PendingBar { + SequenceNumberCyclic start; + simtime_t bound; + int dataManagementCount; + bool aggregate; + }; + std::map, int> dialogs; + std::map, PendingBar> pendingBars; + simtime_t observedTxopStart = -1; + int dataManagementCount = 0; + bool aggregate = false; + simtime_t controlReceivedAt = -1; + public: + int requests = 0; + int acceptedResponses = 0; + int bars = 0; + int positiveResponses = 0; + int activeData = 0; + int activeAggregates = 0; + int retired = 0; + int initialOverruns = 0; + int generalDataOverruns = 0; + int generalAckOverruns = 0; + int initialAckOverruns = 0; + int initialBlockAckOverruns = 0; + int outsideAgreementDataOverruns = 0; + int outsideAgreementAckOverruns = 0; + int permittedRetryOverruns = 0; + bool refreshedRates = false; + bool clean() const { + std::cout << "Exchange cleanup: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " originator=" << originatorBlockAckAgreementHandler->getAgreements().size() + << " recipient=" << recipientBlockAckAgreementHandler->getAgreements().size() + << " pendingBars=" << pendingBars.size() + << " inactivityTimer=" << inactivityTimer->isScheduled() << " addbaTimer=" << addbaResponseTimer->isScheduled() << "\n"; + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) { + std::cout << "Exchange procedure: peer=" << entry.first.first << " tid=" << int(entry.first.second) + << " state=" << int(entry.second.state) << " deadline=" << entry.second.responseDeadline << "\n"; + ASSERT(entry.second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::WAITING_FOR_SUCCESSOR); + ASSERT(!entry.second.requestAttempted && entry.second.responseDeadline == SIMTIME_MAX); + } + for (auto category : {AC_BK, AC_BE, AC_VI, AC_VO}) { + std::cout << "Exchange remaining: ac=" << category + << " queued=" << edca->getEdcaf(category)->getPendingQueue()->getNumPackets() + << " committed=" << edca->getEdcaf(category)->getInProgressFrames()->getLength() << "\n"; + ASSERT(edca->getEdcaf(category)->getPendingQueue()->isEmpty()); + ASSERT(edca->getEdcaf(category)->getInProgressFrames()->getLength() == 0); + } + ASSERT(!inactivityTimer->isScheduled() && !addbaResponseTimer->isScheduled()); + return originatorBlockAckAgreementHandler->getAgreements().empty() && + recipientBlockAckAgreementHandler->getAgreements().empty() && pendingBars.empty(); + } + void checkPublishedState(MacAddress peer) { + ASSERT(mib->getLocalRateSet().operational.known); + ASSERT(mib->getBssRateSet().basic.known); + ASSERT(!mib->getBssRateSet().basic.legacyRates.empty()); + auto rates = mib->findPeerRateSet(peer); + ASSERT(rates && rates->operational.known && !rates->operational.legacyRates.empty()); + auto ht = mib->findPeerHtState(peer); + ASSERT(mib->isHtOperationSupported() && ht && ht->valid); + } + protected: + void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, + Packet *receivedPacket, const Ptr& receivedHeader) override { + controlReceivedAt = simTime(); + Hcf::transmitControlResponseFrame(responsePacket, responseHeader, receivedPacket, receivedHeader); + } + void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) override { + auto mode = packet->getTag()->getMode(); + auto airtime = mode->getDuration(packet->getDataLength()); + ASSERT(simTime() - airtime - controlReceivedAt == modeSet->getSifsTime()); + if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + std::cout << "Exchange response: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " start=" << simTime() - airtime << " end=" << simTime() << " type=" << header->getType() + << " bytes=" << packet->getByteLength() << " bitrate=" << mode->getDataMode()->getNetBitrate() + << " airtime=" << airtime << " ifs=" << modeSet->getSifsTime() << "\n"; + Hcf::recipientProcessTransmittedControlResponseFrame(packet, header); + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (auto data = dynamicPtrCast(header)) { + auto index = packet->getTag()->getAllocationIndex(); + ASSERT(dataScopes.count(index)); + const auto& scope = dataScopes.at(index); + std::cout << "Exchange scope: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " at=" << simTime() << " allocation=" << index << " activeCompressed=" << scope.activeCompressed + << " ackPolicy=" << scope.policy << " aggregate=" << data->getAMsduPresent() << " retry=" << data->getRetry() << "\n"; + auto owner = edca->getChannelOwner(); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + auto bound = txop->getStart() + txop->getLimit(); + if (simTime() > bound) { + if (par("strictAllTiming").boolValue()) + ASSERT(!data->getAMsduPresent()); + if (scope.activeCompressed) { + ASSERT(dataManagementCount == 1); + ASSERT(!data->getAMsduPresent() || data->getRetry()); + permittedRetryOverruns += data->getRetry(); + } + else + outsideAgreementDataOverruns++; + if (dataManagementCount == 1) + initialOverruns++; + else + generalDataOverruns++; + } + } + Hcf::transmissionComplete(packet, header); + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + auto data = dynamicPtrCast(transmitted->peekAtFront()); + if (data) { + auto owner = edca->getChannelOwner(); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + if (simTime() > txop->getStart() + txop->getLimit()) { + auto index = transmitted->getTag()->getAllocationIndex(); + const auto& scope = dataScopes.at(index); + if (par("strictAllTiming").boolValue()) + ASSERT(!data->getAMsduPresent()); + if (scope.activeCompressed) { + ASSERT(dataManagementCount == 1); + ASSERT(!data->getAMsduPresent() || data->getRetry()); + } + else + outsideAgreementAckOverruns++; + if (dataManagementCount > 1) + generalAckOverruns++; + else + initialAckOverruns++; + } + } + Hcf::originatorProcessReceivedFrame(packet, transmitted); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + bool activeCompressed = false; + if (auto data = dynamicPtrCast(header)) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + activeCompressed = std::string(par("blockAckVariant").stringValue()) == "compressed" && + agreement && agreement->getIsAddbaResponseReceived(); + } + bool sent = Hcf::transmitFrame(packet, ifs); + if (!sent) + return false; + header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) + dataScopes.insert_or_assign(packet->getTag()->getAllocationIndex(), + DataScope{activeCompressed, data->getAckPolicy()}); + auto owner = edca->getChannelOwner(); + if (owner) { + auto txop = owner->getTxopProcedure(); + if (txop->getStart() != observedTxopStart) { + observedTxopStart = txop->getStart(); + dataManagementCount = 0; + aggregate = false; + } + if (dynamicPtrCast(header)) + dataManagementCount++; + } + if (auto request = dynamicPtrCast(header)) { + checkPublishedState(request->getReceiverAddress()); + dialogs[{request->getReceiverAddress(), request->getTid()}] = request->getDialogToken(); + requests++; + } + if (auto bar = dynamicPtrCast(header)) { + ASSERT(bar->getCompressedBitmap() && !bar->getMultiTid()); + ASSERT(packet->getDataLength() == LENGTH_BLOCKACKREQ); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + pendingBars.insert_or_assign({bar->getReceiverAddress(), bar->getTidInfo()}, + PendingBar{bar->getStartingSequenceNumber(), txop->getStart() + txop->getLimit(), dataManagementCount, aggregate}); + bars++; + } + ASSERT(!dynamicPtrCast(header)); + if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + if (auto data = dynamicPtrCast(header)) { + aggregate |= data->getAMsduPresent(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived()) { + ASSERT(data->getFragmentNumber() == 0 && !data->getMoreFragments()); + activeData++; + activeAggregates += data->getAMsduPresent(); + } + } + if (dynamicPtrCast(header)) + retired++; + if (header->getType() == ST_DATA_WITH_QOS || header->getType() == ST_RTS || + header->getType() == ST_CTS || header->getType() == ST_BLOCKACK_REQ || header->getType() == ST_BLOCKACK) { + auto mode = packet->getTag()->getMode(); + std::cout << "Exchange frame: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " at=" << simTime() << " type=" << header->getType() << " bytes=" << packet->getByteLength() + << " bitrate=" << mode->getDataMode()->getNetBitrate() << " ifs=" << ifs + << " airtime=" << mode->getDuration(packet->getDataLength()); + if (owner) { + auto txop = owner->getTxopProcedure(); + std::cout << " txopStart=" << txop->getStart() << " txopLimit=" << txop->getLimit() + << " protection=" << txop->getProtectionMechanism() << " dataManagementCount=" << dataManagementCount; + } + std::cout << "\n"; + } + return true; + } + void processLowerFrame(Packet *packet, const Ptr& header) override { + if (auto response = dynamicPtrCast(header); response && response->getReceiverAddress() == mib->address) { + auto key = std::make_pair(response->getTransmitterAddress(), int(response->getTid())); + ASSERT(dialogs.count(key) && dialogs.at(key) == response->getDialogToken()); + ASSERT(response->getStatusCode() == 0 && response->getBlockAckPolicy()); + acceptedResponses++; + } + if (auto response = dynamicPtrCast(header); response && response->getReceiverAddress() == mib->address) { + auto key = std::make_pair(response->getTransmitterAddress(), int(response->getTidInfo())); + ASSERT(pendingBars.count(key)); + auto pending = pendingBars.at(key); + ASSERT(response->getStartingSequenceNumber() == pending.start); + ASSERT(response->getCompressedBitmap() && !response->getMultiTid()); + ASSERT(response->getBlockAckBitmap().getSize() == 64); + bool positive = false; + for (unsigned int i = 0; i < response->getBlockAckBitmap().getSize(); i++) + positive |= response->getBlockAckBitmap().getBit(i); + ASSERT(positive); + std::cout << "Exchange completion: at=" << simTime() << " bound=" << pending.bound + << " dataManagementCount=" << pending.dataManagementCount << "\n"; + // One initial unaggregated MSDU can overrun. A later exchange must finish within the TXOP. + if (pending.dataManagementCount != 1 || pending.aggregate) + ASSERT(simTime() <= pending.bound); + else if (simTime() > pending.bound) + initialBlockAckOverruns++; + positiveResponses++; + pendingBars.erase(key); + } + ASSERT(!dynamicPtrCast(header)); + Hcf::processLowerFrame(packet, header); + if (auto response = dynamicPtrCast(header); + response && response->getReceiverAddress() == mib->address && !refreshedRates) { + auto peer = response->getTransmitterAddress(); + auto tid = response->getTid(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, tid); + ASSERT(agreement && agreement->getIsAddbaResponseReceived()); + auto generation = mib->findPeerHtState(peer)->generation; + auto rates = mib->getBssRateSet(); + ASSERT(rates.basic.legacyRates.size() > 1); + rates.basic.legacyRates.erase(std::prev(rates.basic.legacyRates.end())); + // Association supplies the initial rates. This rate-only update exercises the production subscriber. + mib->setBssRateSet(rates); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, tid) == agreement); + ASSERT(mib->findPeerHtState(peer)->generation == generation); + refreshedRates = true; + } + } +}; +Define_Module(ExchangeHcf); + +class ExchangeTest : public cSimpleModule, public cListener { + int received = 0; + int64_t bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + auto packet = check_and_cast(value); + received++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.sta2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(1200, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(received == 30 && bytes == 21000); + for (const char *path : {"^.sta1.wlan[0].mac.hcf", "^.ap.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + std::cout << "Exchange summary: node=" << path << " accepted=" << hcf->acceptedResponses + << " bars=" << hcf->bars << " positiveResponses=" << hcf->positiveResponses + << " activeData=" << hcf->activeData << " retired=" << hcf->retired + << " activeAggregates=" << hcf->activeAggregates << " initialOverruns=" << hcf->initialOverruns + << " generalDataOverruns=" << hcf->generalDataOverruns << " generalAckOverruns=" << hcf->generalAckOverruns + << " outsideAgreementDataOverruns=" << hcf->outsideAgreementDataOverruns + << " outsideAgreementAckOverruns=" << hcf->outsideAgreementAckOverruns + << " permittedRetryOverruns=" << hcf->permittedRetryOverruns + << " initialAckOverruns=" << hcf->initialAckOverruns << " initialBlockAckOverruns=" << hcf->initialBlockAckOverruns << "\n"; + if (par("ht").boolValue()) { + ASSERT(hcf->requests > 0 && hcf->acceptedResponses > 0 && hcf->bars > 0); + ASSERT(hcf->positiveResponses > 0 && hcf->positiveResponses <= hcf->bars && hcf->activeData > 0 && hcf->retired > 0); + ASSERT(hcf->refreshedRates); + if (par("aggregate").boolValue() && par("rate").intValue() == 24) + ASSERT(hcf->activeAggregates > 0); + } + else + ASSERT(hcf->requests == 0 && hcf->acceptedResponses == 0 && hcf->bars == 0 && hcf->activeData == 0); + ASSERT(hcf->clean()); + } + ASSERT(check_and_cast(getModuleByPath("^.sta2.wlan[0].mac.hcf"))->clean()); + std::cout << "Compressed infrastructure exchange assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(ExchangeTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ExchangeHcf extends Hcf { parameters: bool strictAllTiming = default(false); @class(::inet::ieee80211::ExchangeHcf); } +simple ExchangeTest { parameters: bool ht; bool aggregate; int rate; @class(::inet::ieee80211::ExchangeTest); } +network ExchangeNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta1: WirelessHost; + sta2: WirelessHost; + test: ExchangeTest; +} + +%inifile: omnetpp.ini +[General] +network = ExchangeNetwork +ned-path = .;../../../../src +sim-time-limit = 1.3s +seed-set = 0 +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.sta1.numApps = 1 +*.sta1.app[0].typename = "UdpBasicApp" +*.sta1.app[0].destAddresses = "sta2" +*.sta1.app[0].destPort = 1000 +*.sta1.app[0].messageLength = 700B +*.sta1.app[0].startTime = 1s +*.sta1.app[0].stopTime = 1.00295s +*.sta1.app[0].sendInterval = 100us +*.sta2.numApps = 1 +*.sta2.app[0].typename = "UdpSink" +*.sta2.app[0].localPort = 1000 +*.sta*.ipv4.arp.typename = "GlobalArp" +*.test.ht = ${ht=true,false} +*.test.aggregate = ${aggregationPolicy="","BasicMsduAggregationPolicy"} != "" +*.test.rate = ${rate=6,24} +**.wlan[*].opMode = ${ht} ? "n(mixed-2.4Ghz)" : "g(mixed)" +**.wlan[*].bitrate = ${rate}Mbps +**.bandName = "2.4 GHz" +**.wlan[*].radio.antenna.numAntennas = 2 +**.wlan[*].classifier.typename = "QosClassifier" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ExchangeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.originatorAckPolicy.blockAckReqThreshold = 5 +**.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.originatorMacDataService.msduAggregationPolicy.typename = ${aggregationPolicy} +**.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 1600 +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +**.rtsPolicy.rtsThreshold = 500B +**.txopProcedure.protectionMechanism = ${protection="single","multiple"} +**.txopProcedure.txopLimit = 1ms +**.wlan[*].radio.receiver.energyDetection = -105dBm +**.wlan[*].radio.receiver.sensitivity = -105dBm +**.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sta1.mobility.initialX = 0m +*.ap.mobility.initialX = 5m +*.sta2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +[Config StrictAllTiming] +**.wlan[*].mac.hcf.strictAllTiming = true + +[Config LowestResponse] +constraint = $ht == true && $aggregationPolicy == "" && $rate == 6 +*.test.ht = true +*.test.aggregate = false +*.test.rate = 6 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps + +%contains: stdout +Compressed infrastructure exchange assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckInput_1.test b/tests/module/Ieee80211CompressedBlockAckInput_1.test new file mode 100644 index 00000000000..39a6506ce2d --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckInput_1.test @@ -0,0 +1,326 @@ +%description: +Check malformed input and profile conflicts through initialized HCF. +Both local profiles receive valid addressed BAR from the opposite profile. +HCF must stop before receipt, reorder, inactivity, recovery, or response updates. +Malformed BAR and unaddressed BAR must not cause a profile conflict. +An active Compressed agreement rejects fragmented DATA before ACK and data-service callbacks. +A valid Compressed BAR without an agreement requests DELBA. A malformed BAR does not. +The fixture supplies an accepted agreement. It does not test ADDBA negotiation or wireless reception. +The separate Compressed exchange fixture tests negotiation and frame exchange. + +%extraargs: -r 0..1 + +%file: CompressedInput.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class InputAgreement : public RecipientBlockAckAgreement { + public: + int refreshes = 0; + using RecipientBlockAckAgreement::RecipientBlockAckAgreement; + void calculateExpirationTime() override { + refreshes++; + RecipientBlockAckAgreement::calculateExpirationTime(); + } +}; +class InputAgreements : public RecipientBlockAckAgreementHandler { + public: + int dataUpdates = 0; + InputAgreement *accept(MacAddress peer, BlockAckVariant variant) { + auto agreement = new InputAgreement(peer, 5, SequenceNumberCyclic(100), 64, SimTime(5, SIMTIME_MS)); + agreement->setVariant(variant); + agreement->addbaResposneSent(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } + void qosFrameReceived(const Ptr& header, IBlockAckAgreementHandlerCallback *callback) override { + dataUpdates++; + RecipientBlockAckAgreementHandler::qosFrameReceived(header, callback); + } +}; +class InputAckSpy : public IRecipientAckProcedure { + public: + IRecipientAckProcedure *delegate; + int calls = 0; + explicit InputAckSpy(IRecipientAckProcedure *delegate) : delegate(delegate) {} + ~InputAckSpy() override { delete delegate; } + void processReceivedFrame(Packet *packet, const Ptr& header, + IRecipientAckPolicy *policy, IProcedureCallback *callback) override { + calls++; + delegate->processReceivedFrame(packet, header, policy, callback); + } + void processTransmittedAck(const Ptr& header) override { + delegate->processTransmittedAck(header); + } +}; +class InputDataSpy : public IRecipientQosMacDataService { + public: + IRecipientQosMacDataService *delegate = nullptr; + int dataCalls = 0; + int controlCalls = 0; + int retireCalls = 0; + std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool reset) override { + retireCalls++; + return delegate->retireAgreement(agreement, reset); + } + void clearPeer(MacAddress peer) override { delegate->clearPeer(peer); } + std::vector dataFrameReceived(Packet *packet, const Ptr& header, + IRecipientBlockAckAgreementHandler *agreements, bool interframe = false, + const BlockAckReceiveContext *context = nullptr) override { + dataCalls++; + return delegate->dataFrameReceived(packet, header, agreements, interframe, context); + } + std::vector controlFrameReceived(Packet *packet, const Ptr& header, + IRecipientBlockAckAgreementHandler *agreements) override { + controlCalls++; + return delegate->controlFrameReceived(packet, header, agreements); + } + std::vector managementFrameReceived(Packet *packet, const Ptr& header) override { + return delegate->managementFrameReceived(packet, header); + } +}; +class CompressedInputHcf : public Hcf { + protected: + InputDataSpy dataSpy; + InputAckSpy *ackSpy = nullptr; + InputAgreements *agreements = nullptr; + InputAgreement *accepted = nullptr; + BlockAckVariant localVariant = BlockAckVariant::BASIC; + int responses = 0; + int deliveries = 0; + int delbas = 0; + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress absent = MacAddress("02:00:00:00:00:04"); + std::vector receiptSnapshot; + simtime_t expirationSnapshot; + + std::vector receipts() { + std::vector result; + auto record = accepted->getBlockAckRecord(); + for (int sequence = 100; sequence < 164; sequence++) { + if (localVariant == BlockAckVariant::COMPRESSED) + result.push_back(record->hasCompleteReceipt(SequenceNumberCyclic(sequence))); + else + for (int fragment = 0; fragment < 16; fragment++) + result.push_back(record->getAckState(SequenceNumberCyclic(sequence), fragment)); + } + return result; + } + void unchanged() { + ASSERT(agreements->getAgreement(5, peer) == accepted); + ASSERT(!agreements->isTeardownPending(peer, 5)); + ASSERT(accepted->refreshes == 0); + ASSERT(accepted->getExpirationTime() == expirationSnapshot); + ASSERT(accepted->getHighestReceivedSequence() == SequenceNumberCyclic(100)); + ASSERT(receipts() == receiptSnapshot); + if (localVariant == BlockAckVariant::COMPRESSED) + ASSERT(accepted->getBlockAckRecord()->getWindowStart() == SequenceNumberCyclic(100)); + ASSERT(inactivityTimer->isScheduled()); + ASSERT(inactivityTimer->getArrivalTime() == expirationSnapshot); + ASSERT(agreements->dataUpdates == 0); + ASSERT(dataSpy.dataCalls == 0 && dataSpy.controlCalls == 0 && dataSpy.retireCalls == 0); + ASSERT(ackSpy->calls == 0); + ASSERT(responses == 0 && deliveries == 0 && delbas == 0); + } + Ptr makeBar(bool compressed, MacAddress transmitter, bool addressed, + int fragment = 0, bool unsupported = false) { + Ptr bar; + if (unsupported) + bar = makeShared(); + else if (compressed) { + auto concrete = makeShared(); + concrete->setTidInfo(5); + concrete->setFragmentNumber(fragment); + concrete->setStartingSequenceNumber(SequenceNumberCyclic(101)); + bar = concrete; + } + else { + auto concrete = makeShared(); + concrete->setTidInfo(5); + concrete->setFragmentNumber(fragment); + concrete->setStartingSequenceNumber(SequenceNumberCyclic(101)); + bar = concrete; + } + bar->setTransmitterAddress(transmitter); + bar->setReceiverAddress(addressed ? mac->getAddress() : MacAddress("02:00:00:00:00:09")); + return bar; + } + void injectBar(bool compressed, MacAddress transmitter, bool addressed, bool conflict, + int fragment = 0, int extraBytes = 0, bool unsupported = false) { + auto bar = makeBar(compressed, transmitter, addressed, fragment, unsupported); + auto packet = new Packet("bar", bar); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + if (extraBytes) + packet->insertAtBack(makeShared(B(extraBytes))); + packet->insertAtBack(makeShared()); + bool caught = false; + try { + processLowerFrame(packet, bar); + } + catch (const cRuntimeError& error) { + // processLowerFrame takes the Packet before the exception. This wrapper owns its disposal. + take(packet); + delete packet; + std::string message = error.what(); + ASSERT(message.find("Block Ack profile conflict at ") == 0); + ASSERT(message.find("peer=" + peer.str() + " TID=5") != std::string::npos); + ASSERT(message.find(compressed ? "local=basic received=compressed" : "local=compressed received=basic") != std::string::npos); + caught = true; + } + ASSERT(caught == conflict); + } + void injectFragment(int fragment, bool more, AckPolicy policy) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(peer); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(peer); + data->setTid(5); + data->setAckPolicy(policy); + data->setSequenceNumber(SequenceNumberCyclic(101)); + data->setFragmentNumber(fragment); + data->setMoreFragments(more); + auto packet = new Packet("fragment", data); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, data); + } + void sendUp(const std::vector& frames) override { + deliveries += frames.size(); + for (auto frame : frames) { + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + responses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + auto delba = dynamicPtrCast(header); + ASSERT(delba && delba->getReceiverAddress() == absent && delba->getTid() == 5); + ASSERT(!delba->getInitiator() && delba->getReasonCode() == 38); + delbas++; + delete packet; + } + public: + void runChecks() { + Enter_Method("runChecks"); + localVariant = std::string(par("blockAckVariant").stringValue()) == "compressed" ? BlockAckVariant::COMPRESSED : BlockAckVariant::BASIC; + delete recipientBlockAckAgreementHandler; + agreements = new InputAgreements(); + recipientBlockAckAgreementHandler = agreements; + accepted = agreements->accept(peer, localVariant); + ackSpy = new InputAckSpy(recipientAckProcedure); + recipientAckProcedure = ackSpy; + dataSpy.delegate = recipientDataService; + recipientDataService = &dataSpy; + auto seed = makeShared(); + seed->setAckPolicy(BLOCK_ACK); + seed->setSequenceNumber(SequenceNumberCyclic(100)); + accepted->recordReceived(seed); + if (localVariant == BlockAckVariant::COMPRESSED) + accepted->getBlockAckRecord()->compressedFrameReceived(seed); + else + accepted->blockAckPolicyFrameReceived(seed); + receiptSnapshot = receipts(); + expirationSnapshot = accepted->getExpirationTime(); + scheduleInactivityTimer(); + unchanged(); + + bool oppositeCompressed = localVariant == BlockAckVariant::BASIC; + injectBar(oppositeCompressed, peer, true, true); + unchanged(); + injectBar(oppositeCompressed, peer, false, false); + unchanged(); + for (bool compressed : {false, true}) { + injectBar(compressed, peer, true, false, 1); + unchanged(); + injectBar(compressed, peer, true, false, 0, 1); + unchanged(); + injectBar(compressed, peer, true, false, 0, 0, true); + unchanged(); + injectBar(compressed, absent, true, false, 1); + unchanged(); + injectBar(compressed, absent, true, false, 0, 1); + unchanged(); + } + if (localVariant == BlockAckVariant::COMPRESSED) { + for (AckPolicy policy : {NORMAL_ACK, BLOCK_ACK}) { + injectFragment(0, true, policy); + unchanged(); + injectFragment(1, false, policy); + unchanged(); + } + } + injectBar(true, absent, true, false); + ASSERT(delbas == 1 && responses == 0 && deliveries == 0); + ASSERT(agreements->getAgreement(5, absent) == nullptr); + ASSERT(agreements->isTeardownPending(absent, 5)); + ASSERT(agreements->getAgreement(5, peer) == accepted); + ASSERT(receipts() == receiptSnapshot && accepted->refreshes == 0); + std::cout << "Compressed Block Ack input assertions passed.\n"; + } +}; +Define_Module(CompressedInputHcf); +class CompressedInputTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->runChecks(); + endSimulation(); + } +}; +Define_Module(CompressedInputTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module CompressedInputHcf extends Hcf { parameters: @class(::inet::ieee80211::CompressedInputHcf); } +simple CompressedInputTest { parameters: @class(::inet::ieee80211::CompressedInputTest); } +network CompressedInputNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: CompressedInputTest; +} + +%inifile: omnetpp.ini +[General] +network = CompressedInputNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "CompressedInputHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = ${"basic", "compressed"} +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Compressed Block Ack input assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckInterframe_1.test b/tests/module/Ieee80211CompressedBlockAckInterframe_1.test new file mode 100644 index 00000000000..6d9a76be69d --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckInterframe_1.test @@ -0,0 +1,542 @@ +%description: +Check Compressed interframe behavior through physical reception and HCF ACK completion. +The fixture supplies an accepted recipient agreement with four positions. It does not test negotiation. +A raw radio driver transmits HT DATA and receives real HCF responses. +The driver supplies no interframe relation flag to HCF or the data service. +Check SIFS, the exact tolerance boundary, a missing prior receipt, wrap, and unrelated traffic. +Check RIFS with an external HT waveform that omits the six-microsecond signal extension. +The external transmitter changes physical and analog duration together. It retains the actual HT mode and PHY length. +Check a default-extension RIFS control and a gap outside the RIFS tolerance. +Repeat each case with zero and five-microsecond propagation time. +After cache expiry, require the saved physical snapshot to retain the exact interval for a later transmission. + +%extraargs: -r 0..27 + +%file: Interframe.cc +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" +#include "inet/mobility/contract/IMobility.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmitterAnalogModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +using namespace physicallayer; + +class InterframeTransmitter : public Ieee80211Transmitter { + public: + const ITransmission *createTransmission(const IRadio *radio, const Packet *packet, simtime_t start) const override { + if (!packet->isName("first-no-extension")) + return Ieee80211Transmitter::createTransmission(radio, packet, start); + auto selected = computeTransmissionMode(packet); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + ASSERT(selected->getHtMcsIndex() == 0); + ASSERT(selected->getSignalExtensionTime() == SimTime(6, SIMTIME_US)); + auto duration = selected->getDuration(B(phy->getLengthField())) - SimTime(6, SIMTIME_US); + auto preamble = selected->getPreambleMode()->getDuration(); + auto header = selected->getHeaderMode()->getDuration(); + auto data = duration - preamble - header; + auto mobility = radio->getAntenna()->getMobility(); + auto position = mobility->getCurrentPosition(); + auto orientation = mobility->getCurrentAngularPosition(); + auto analog = getAnalogModel()->createAnalogModel(preamble, header, data, centerFrequency, + selected->getDataMode()->getBandwidth(), computeTransmissionPower(packet)); + return new Ieee80211Transmission(radio, packet, start, start + duration, preamble, header, data, + position, position, orientation, orientation, nullptr, nullptr, nullptr, nullptr, + analog, selected, computeTransmissionChannel(packet)); + } +}; +Define_Module(InterframeTransmitter); + +class InterframeReordering : public BlockAckReordering { + public: + ReceiveBuffer *find(MacAddress peer) const { + auto it = receiveBuffers.find({5, peer}); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; +class InterframeRecipient : public RecipientQosMacDataService { + protected: + void initialize() override { + RecipientQosMacDataService::initialize(); + delete blockAckReordering; + blockAckReordering = new InterframeReordering(); + } + public: + ReceiveBuffer *buffer(MacAddress peer) const { + return static_cast(blockAckReordering)->find(peer); + } +}; +Define_Module(InterframeRecipient); + +class InterframeJammer : public cSimpleModule { + public: + void start() { + Enter_Method("start"); + scheduleAfter(SimTime(1, SIMTIME_PS), new cMessage("busy")); + } + protected: + void handleMessage(cMessage *message) override { + delete message; + auto radio = check_and_cast(getParentModule()->getSubmodule("radio")); + radio->setRadioMode(IRadio::RADIO_MODE_TRANSMITTER); + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:08")); + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet("busy-only", header); + auto fcs = makeShared(); + fcs->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(fcs); + auto catalog = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const IIeee80211Mode *mode = nullptr; + for (int i = 0; i < catalog->getNumModes(); i++) { + auto candidate = catalog->getMode(i); + if (candidate->getHtMcsIndex() == 8 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + mode = candidate; + } + ASSERT(mode && mode->getDataMode()->getNumberOfSpatialStreams() == 2); + packet->addTag()->setMode(mode); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + send(packet, "out"); + } +}; +Define_Module(InterframeJammer); + +class InterframeAgreements : public RecipientBlockAckAgreementHandler { + public: + RecipientBlockAckAgreement *accept(MacAddress peer, int start) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 4, SIMTIME_ZERO); + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } +}; +class InterframeHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + InterframeRecipient *recipient = nullptr; + InterframeAgreements *agreements = nullptr; + RecipientBlockAckAgreement *accepted = nullptr; + std::vector delivered; + int ackCompletions = 0; + int ctsCompletions = 0; + int baCompletions = 0; + int physicalStarts = 0; + int busyStates = 0; + MacAddress address() const { return mac->getAddress(); } + void prepare(int start) { + Enter_Method("prepare"); + delete recipientBlockAckAgreementHandler; + agreements = new InterframeAgreements(); + recipientBlockAckAgreementHandler = agreements; + accepted = agreements->accept(peer, start); + recipient = check_and_cast(getSimulation()->getSystemModule()->getSubmodule("recipientProbe")); + recipientDataService = recipient; + } + void retireAndAcceptAgain(int start) { + Enter_Method("retireAndAcceptAgain"); + retireAgreement(peer, 5, false, true); + accepted = agreements->accept(peer, start); + // Supply prior receipt state independently of the previous agreement's physical ACK context. + auto seed = makeShared(); + seed->setSequenceNumber(SequenceNumberCyclic(start)); + accepted->recordReceived(seed); + accepted->getBlockAckRecord()->compressedFrameReceived(seed); + } + void check(int start, int scenario, bool report = true) { + Enter_Method("check"); + int first = (start + (scenario == 2 || scenario == 12 ? 1 : 0)) % 4096; + int last = (start + 4) % 4096; + int recordStart = (start + 1) % 4096; + bool related = scenario == 0 || scenario == 3 || scenario == 4 || scenario == 6 || scenario == 7; + auto record = accepted->getBlockAckRecord(); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(recordStart)); + unsigned bitmap = 0; + for (int i = 0; i < 64; i++) + if (record->getCompressedAckState(SequenceNumberCyclic((recordStart + i) % 4096))) + bitmap |= 1u << i; + ASSERT(bitmap == (scenario == 2 || scenario == 12 ? 9u : 8u)); + auto buffer = recipient->buffer(peer); + ASSERT(buffer); + int expectedB = related ? (last + 1) % 4096 : scenario == 2 || scenario == 12 ? (start + 2) % 4096 : recordStart; + ASSERT(buffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(expectedB)); + ASSERT(buffer->getLength() == (related ? 0 : 1)); + std::vector expected = related ? std::vector{first, last} : std::vector{first}; + ASSERT(delivered == expected); + ASSERT(ackCompletions == (scenario >= 7 && scenario <= 9 ? 1 : 2)); + ASSERT(ctsCompletions == (scenario == 5 ? 1 : 0)); + ASSERT(baCompletions == (scenario == 12 ? 1 : 0)); + if (scenario == 13 && report) { + ASSERT(physicalStarts == 2); + ASSERT(busyStates > 0); + } + if (report) + std::cout << "Interframe scenario=" << scenario << " R=" << recordStart << " bitmap=" << bitmap + << " B=" << expectedB << " ACK=" << ackCompletions << " CTS=" << ctsCompletions << "\n"; + } + void checkNoPending() { + Enter_Method("checkNoPending"); + auto watch = dynamic_cast(findObject("numPendingInterframeReceptions", false)); + ASSERT(watch && watch->str() == "0"); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (header->getType() == ST_ACK) + ackCompletions++; + if (header->getType() == ST_CTS) + ctsCompletions++; + if (header->getType() == ST_BLOCKACK) + baCompletions++; + Hcf::transmissionComplete(packet, header); + if (header->getType() == ST_ACK && ackCompletions == 1 && par("busyProbe").boolValue()) + check_and_cast(getSimulation()->getSystemModule()->getModuleByPath("jammerNode.jammer"))->start(); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (source == getParentModule()->getParentModule()->getSubmodule("radio") && signal == receptionStartedSignal) + physicalStarts++; + Hcf::receiveSignal(source, signal, object, details); + } + void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override { + if (source == getParentModule()->getParentModule()->getSubmodule("radio") && + signal == IRadio::receptionStateChangedSignal && value == IRadio::RECEPTION_STATE_BUSY) + busyStates++; + Hcf::receiveSignal(source, signal, value, details); + } +}; +Define_Module(InterframeHcf); + +class InterframeDriver : public cSimpleModule, public cListener { + protected: + InterframeHcf *receiver = nullptr; + Ieee80211Radio *radio = nullptr; + const IRadioMedium *medium = nullptr; + const IIeee80211Mode *ht = nullptr; + const Ieee80211ModeSet *catalog = nullptr; + int scenario = -1; + int start = 100; + int receivedAcks = 0; + bool finalSent = false; + TransmissionTimingSnapshot saved; + simtime_t initialAckArrival = -1; + bool snapshotVerified = false; + bool completed = false; + int malformedIndex = 0; + + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("start")); + } + void finish() override { ASSERT(completed); } + void scheduleFinal(simtime_t gap) { + scheduleAfter(gap, new cMessage("final")); + } + void sendFrame(const Ptr& header, const char *name, bool payload = false) { + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet(name, header); + if (payload) + packet->insertAtBack(makeShared(B(10))); + auto fcs = makeShared(); + fcs->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(fcs); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + packet->addTag()->setMode(header->getType() == ST_RTS ? catalog->getMode(bps(6000000)) : ht); + send(packet, "out"); + } + void data(int sequence, const char *name, bool addressed = true, AckPolicy policy = NORMAL_ACK) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(addressed ? receiver->address() : MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(receiver->peer); + header->setAddress3(receiver->peer); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setAckPolicy(policy); + sendFrame(header, name, true); + } + void sendMalformed() { + Ptr bar; + int kind = malformedIndex % 3; + if (kind == 2) + bar = makeShared(); + else { + auto compressed = makeShared(); + compressed->setStartingSequenceNumber(SequenceNumberCyclic((start + 2) % 4096)); + compressed->setTidInfo(5); + compressed->setFragmentNumber(kind == 0 ? 1 : 0); + bar = compressed; + } + bar->setTransmitterAddress(receiver->peer); + bar->setReceiverAddress(receiver->address()); + sendFrame(bar, "malformed-bar", kind == 1); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + auto transmission = check_and_cast(object); + auto name = std::string(transmission->getPacket()->getName()); + if (signal == transmissionStartedSignal && name == "snapshot-probe") { + ASSERT(saved.transmissionId >= 0); + ASSERT(medium->getTransmission(saved.transmissionId) == nullptr); + ASSERT(medium->getTransmissionTimingSnapshot(saved.transmissionId).transmissionId == -1); + auto gap = medium->computePrecedingSignalGap(transmission->getId(), saved); + ASSERT(gap.has_value()); + ASSERT(*gap == transmission->getStartTime() - saved.arrivalEndTimes.at(radio->getId())); + snapshotVerified = true; + } + if (signal == transmissionEndedSignal && name == "unrelated") + scheduleFinal(catalog->getSifsTime() + (scenario == 11 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10 + 1) : SIMTIME_ZERO)); + if (signal == transmissionEndedSignal && (name == "first-no-extension" || name == "first-rifs-default")) { + auto extension = transmission->getSignalExtensionDuration(); + ASSERT(extension.has_value()); + ASSERT(*extension == (scenario == 8 ? SimTime(6, SIMTIME_US) : SIMTIME_ZERO)); + scheduleFinal(SimTime(2, SIMTIME_US) + (scenario == 9 ? SimTime(200001, SIMTIME_PS) : SIMTIME_ZERO)); + } + if (signal == transmissionEndedSignal && name == "snapshot-probe") + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("finish")); + if (signal == transmissionEndedSignal && name == "malformed-bar") { + auto propagation = getSimulation()->getSystemModule()->getSubmodule("radioMedium")->getSubmodule("propagation"); + scheduleAfter(SimTime(propagation->par("propagationTime").doubleValue()) + SimTime(1, SIMTIME_PS), + new cMessage("malformed-check")); + } + } + void handleMessage(cMessage *message) override { + if (!message->isSelfMessage()) { + auto packet = check_and_cast(message); + ASSERT(!packet->hasBitError()); + if (packet->isName("busy-only")) { + delete packet; + return; + } + auto header = packet->peekAtFront(); + if (header->getType() == ST_BLOCKACK) { + ASSERT(scenario == 12 && receivedAcks == 0); + scheduleAfter(catalog->getSifsTime(), new cMessage("first")); + } + else if (header->getType() == ST_CTS) { + ASSERT(scenario == 5 && receivedAcks == 1); + scheduleFinal(catalog->getSifsTime()); + } + else { + ASSERT(header->getType() == ST_ACK); + receivedAcks++; + if (receivedAcks == 1 && (scenario < 7 || scenario >= 10)) { + auto timing = packet->getTag(); + saved = medium->getTransmissionTimingSnapshot(timing->getTransmissionId()); + initialAckArrival = timing->getEndTime(); + ASSERT(initialAckArrival == simTime()); + if (scenario == 10) + receiver->retireAndAcceptAgain(start); + if (scenario == 4 || scenario == 11) + scheduleAfter(catalog->getSifsTime(), new cMessage("unrelated")); + else if (scenario == 5) + scheduleAfter(catalog->getSifsTime(), new cMessage("rts")); + else { + auto offset = scenario == 1 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10 + 1) : + scenario == 6 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10) : SIMTIME_ZERO; + scheduleFinal(catalog->getSifsTime() + offset); + } + } + else { + ASSERT(finalSent); + if (saved.transmissionId < 0) + saved = medium->getTransmissionTimingSnapshot(packet->getTag()->getTransmissionId()); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("snapshot")); + } + } + delete packet; + return; + } + std::string name = message->getName(); + delete message; + if (name == "start") { + receiver = check_and_cast(getSimulation()->getSystemModule()->getModuleByPath("receiver.wlan[0].mac.hcf")); + radio = check_and_cast(getParentModule()->getSubmodule("radio")); + medium = radio->getMedium(); + ASSERT(medium->getTransmission(0) == nullptr); + ASSERT(medium->getTransmissionTimingSnapshot(0).transmissionId == -1); + ASSERT(!medium->computePrecedingSignalGap(0, TransmissionTimingSnapshot()).has_value()); + radio->setRadioMode(IRadio::RADIO_MODE_TRANSCEIVER); + radio->subscribe(transmissionStartedSignal, this); + radio->subscribe(transmissionEndedSignal, this); + scenario = par("scenario"); + start = scenario == 3 ? 4094 : 100; + catalog = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto candidate = catalog->getMode(i); + if (candidate->getHtMcsIndex() == 0 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + ht = candidate; + } + ASSERT(ht); + receiver->prepare(start); + bool rifs = scenario >= 7 && scenario <= 9; + if (scenario == 12) { + auto bar = makeShared(); + bar->setTransmitterAddress(receiver->peer); + bar->setReceiverAddress(receiver->address()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(101)); + sendFrame(bar, "initial-bar"); + } + else + data((start + (scenario == 2 ? 1 : 0)) % 4096, + rifs ? scenario == 8 ? "first-rifs-default" : "first-no-extension" : "first", + true, rifs ? BLOCK_ACK : NORMAL_ACK); + } + else if (name == "first") + data(101, "first"); + else if (name == "final") { + finalSent = true; + data((start + 4) % 4096, "final"); + } + else if (name == "unrelated") + data(200, "unrelated", false); + else if (name == "rts") { + auto header = makeShared(); + header->setTransmitterAddress(receiver->peer); + header->setReceiverAddress(receiver->address()); + sendFrame(header, "rts"); + } + else if (name == "snapshot") { + receiver->check(start, scenario); + data(300, "snapshot-probe", false); + } + else if (name == "finish") { + ASSERT(snapshotVerified); + ASSERT(receivedAcks == (scenario >= 7 && scenario <= 9 ? 1 : 2)); + sendMalformed(); + } + else if (name == "malformed-check") { + receiver->checkNoPending(); + receiver->check(start, scenario, false); + malformedIndex++; + if (malformedIndex < 6) { + sendMalformed(); + return; + } + std::cout << "Compressed Block Ack interframe assertions passed.\n"; + completed = true; + endSimulation(); + } + } +}; +Define_Module(InterframeDriver); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +import inet.mobility.static.StationaryMobility; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Transmitter; +module InterframeTransmitter extends Ieee80211Transmitter { parameters: @class(::inet::ieee80211::InterframeTransmitter); } +module InterframeRecipient extends RecipientQosMacDataService { parameters: @class(::inet::ieee80211::InterframeRecipient); } +module InterframeHcf extends Hcf { parameters: bool busyProbe; @class(::inet::ieee80211::InterframeHcf); } +simple InterframeJammer { + parameters: @class(::inet::ieee80211::InterframeJammer); + gates: output out; +} +simple InterframeDriver { + parameters: int scenario; @class(::inet::ieee80211::InterframeDriver); + gates: input in; output out; +} +module DriverNode { + parameters: @networkNode; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + driver: InterframeDriver; + connections allowunconnected: + driver.out --> radio.upperLayerIn; + radio.upperLayerOut --> driver.in; +} +module JammerNode { + parameters: @networkNode; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + jammer: InterframeJammer; + connections allowunconnected: + jammer.out --> radio.upperLayerIn; +} +network InterframeNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + receiver: WirelessHost; + driverNode: DriverNode; + jammerNode: JammerNode; + recipientProbe: InterframeRecipient; +} + +%inifile: omnetpp.ini +[General] +network = InterframeNetwork +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.driverNode.driver.scenario = ${scenario=0..13} +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = (${scenario} == 13 ? 100us : 0s) + ${propagation=0s,5us} +*.radioMedium.mediumLimitCache.maxTransmissionDuration = 1ms +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "InterframeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +*.receiver.wlan[0].mac.hcf.busyProbe = ${scenario} == 13 +*.receiver.wlan[0].radio.bandwidth = 20MHz +*.receiver.wlan[0].radio.antenna.numAntennas = 1 +*.receiver.wlan[0].radio.receiver.sensitivity = -70dBm +*.receiver.wlan[0].radio.receiver.energyDetection = -90dBm +*.jammerNode.radio.opMode = "n(mixed-2.4Ghz)" +*.jammerNode.radio.bandwidth = 20MHz +*.jammerNode.radio.transmitter.bitrate = 6.5Mbps +*.jammerNode.radio.transmitter.power = -40dBm +*.jammerNode.radio.antenna.numAntennas = 2 +*.jammerNode.radio.antenna.mobilityModule = "^.^.mobility" +*.driverNode.radio.opMode = "n(mixed-2.4Ghz)" +*.driverNode.radio.bandwidth = 20MHz +*.driverNode.radio.transmitter.typename = "InterframeTransmitter" +*.driverNode.radio.transmitter.bitrate = 6.5Mbps +*.driverNode.radio.antenna.mobilityModule = "^.^.mobility" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 10m +**.mobility.constraintAreaMaxY = 10m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +*.receiver.mobility.initialX = 0m +*.driverNode.mobility.initialX = 1m +*.jammerNode.mobility.initialX = 1m + +%contains: stdout +Compressed Block Ack interframe assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckMigration_1.test b/tests/module/Ieee80211CompressedBlockAckMigration_1.test new file mode 100644 index 00000000000..72db8ff5672 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckMigration_1.test @@ -0,0 +1,179 @@ +%description: +An external configuration enables Block Ack without an explicit profile. +Legacy g and a links, and an HT ad hoc link without peer HT records, must deliver twelve UDP payloads with Normal Ack. +Require a queued successor without ADDBA, BAR, or BA. Check profile and capability diagnostics with support enabled and disabled. + +%extraargs: -r 0..5 + +%postrun-command: python3 check-diagnostics.py + +%file: Migration.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet::ieee80211 { +class MigrationHcf : public Hcf { + public: + int normalData = 0; + int requests = 0; + int bars = 0; + int responses = 0; + bool queuedSuccessor = false; + bool compressed() const { return std::string(par("blockAckVariant").stringValue()) == "compressed"; } + bool clean() const { + return (!originatorBlockAckAgreementHandler || (originatorBlockAckAgreementHandler->getAgreements().empty() && + originatorBlockAckAgreementHandler->getProcedures().empty())) && + (!recipientBlockAckAgreementHandler || recipientBlockAckAgreementHandler->getAgreements().empty()); + } + protected: + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (!accepted) + return false; + requests += dynamicPtrCast(header) != nullptr; + bars += dynamicPtrCast(header) != nullptr; + responses += dynamicPtrCast(header) != nullptr; + if (auto data = dynamicPtrCast(header)) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + ASSERT(!mib->findPeerHtState(data->getReceiverAddress())); + normalData++; + } + return true; + } + void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& header, AccessCategory ac) override { + queuedSuccessor |= edca->getEdcaf(ac)->getPendingQueue()->getNumPackets() > 0; + Hcf::originatorProcessTransmittedDataFrame(packet, header, ac); + } +}; +Define_Module(MigrationHcf); + +class MigrationTest : public cSimpleModule, public cListener { + int received = 0; + int bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + auto packet = check_and_cast(value); + received++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.host2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(99, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(received == 12 && bytes == 8400); + for (const char *path : {"^.host1.wlan[0].mac.hcf", "^.host2.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + ASSERT(hcf->compressed() && hcf->clean()); + ASSERT(hcf->requests == 0 && hcf->bars == 0 && hcf->responses == 0); + if (hcf->normalData > 0) + ASSERT(hcf->normalData >= 12 && hcf->queuedSuccessor); + } + ASSERT(check_and_cast(getModuleByPath("^.host1.wlan[0].mac.hcf"))->normalData >= 12); + std::cout << "Compressed default migration assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(MigrationTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.AdhocHost; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module MigrationHcf extends Hcf { parameters: @class(::inet::ieee80211::MigrationHcf); } +simple MigrationTest { parameters: string opMode; bool supported; @class(::inet::ieee80211::MigrationTest); } +network MigrationNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: AdhocHost; + host2: AdhocHost; + test: MigrationTest; +} + +%inifile: omnetpp.ini +[General] +network = MigrationNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = false +cmdenv-event-banners = false +**.mac.hcf.originatorBlockAckAgreementPolicy.cmdenv-log-level = debug +**.mac.hcf.cmdenv-log-level = info +**.cmdenv-log-level = off +record-vector-results = false +record-scalar-results = false +*.test.opMode = ${opMode="g(mixed)","a","n(mixed-2.4Ghz)"} +*.test.supported = ${supported=true,false} +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicApp" +*.host1.app[0].destAddresses = "host2" +*.host1.app[0].destPort = 1000 +*.host1.app[0].messageLength = 700B +*.host1.app[0].startTime = 10ms +*.host1.app[0].stopTime = 11.15ms +*.host1.app[0].sendInterval = 100us +*.host2.numApps = 1 +*.host2.app[0].typename = "UdpSink" +*.host2.app[0].localPort = 1000 +*.host*.ipv4.arp.typename = "GlobalArp" +*.host*.wlan[*].opMode = ${opMode} +*.host*.wlan[*].radio.bandName = ${opMode} == "a" ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +*.host*.wlan[*].radio.channelNumber = 0 +*.host*.wlan[*].bitrate = 6Mbps +*.host*.wlan[*].classifier.typename = "QosClassifier" +*.host*.wlan[*].mac.qosStation = true +*.host*.wlan[*].mac.hcf.typename = "MigrationHcf" +*.host*.wlan[*].mac.hcf.isBlockAckSupported = ${supported} +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +*.host*.wlan[*].radio.receiver.energyDetection = -105dBm +*.host*.wlan[*].radio.receiver.sensitivity = -105dBm +*.host*.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.host1.mobility.initialX = 0m +*.host2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%file: check-diagnostics.py +from pathlib import Path +import re + +text = Path("test.out").read_text() +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", text)[1:] +assert len(runs) == 6, len(runs) +warning = "Compressed Block Ack has no usable local HT support" +profile = "Block Ack profile: compressed" +for run in runs: + ht = '$opMode="n(mixed-2.4Ghz)"' in run + supported = "$supported=true" in run + assert "Compressed default migration assertions passed." in run + assert run.count(profile) == (2 if supported else 0), run + assert run.count(warning) == (2 if supported and not ht else 0), run + if supported: + reason = "peer HT state is absent" if ht else "local HT state is absent" + assert f"Compressed Block Ack setup refused: {reason}; DATA uses Normal Ack." in run, run + else: + assert "Compressed Block Ack setup refused:" not in run, run +print("Compressed default migration diagnostics passed.") + +%contains: stdout +Compressed default migration assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckOriginator_1.test b/tests/module/Ieee80211CompressedBlockAckOriginator_1.test new file mode 100644 index 00000000000..10bac06324e --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckOriginator_1.test @@ -0,0 +1,175 @@ +%description: +Check Compressed responses through the HCF control callback and real EDCAF state. +The fixture supplies accepted agreements and numbered frames with known ACK states. +The fixture supplies peer HT capabilities; it does not test capability negotiation. +An earlier response acknowledges a sent frame outside the current BAR wait. +Later and disjoint responses release every current BAR wait for retry. +Unsent frames, frames outside the active window, other peers, and other TIDs retain their state. +Cases use ordinary sequence numbers and wrap across 4095. +Scheduling_1 separately checks shifted responses through an active frame sequence. + +%extraargs: -r 0..5 + +%file: Originator.cc +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +class OriginatorAgreements : public OriginatorBlockAckAgreementHandler { + public: + OriginatorBlockAckAgreement *accept(MacAddress peer, int start) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } +}; +class OriginatorHcf : public Hcf { + public: + void check() { + Enter_Method("check"); + rx->setContentionBlocked(true); + const MacAddress peer("02:00:00:00:00:02"); + const MacAddress other("02:00:00:00:00:03"); + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + int start = par("start"); + int offset = par("offset"); + delete originatorBlockAckAgreementHandler; + auto agreements = new OriginatorAgreements(); + originatorBlockAckAgreementHandler = agreements; + auto agreement = agreements->accept(peer, start); + auto edcaf = edca->getEdcaf(AC_VI); + auto frames = edcaf->getInProgressFrames(); + auto ack = edcaf->getAckHandler(); + auto add = [&](MacAddress receiver, Tid tid, int position, bool sent) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setTid(tid); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(start) + position); + auto packet = new Packet("supplied-data", header); + packet->insertAtBack(makeShared()); + frames->adoptPreparedFrame(packet); + if (sent) + ack->processTransmittedDataOrMgmtFrame(header); + return packet; + }; + auto earlier = add(peer, 5, 2, true); + auto earlierHeader = earlier->peekAtFront(); + auto requested = add(peer, 5, 4, true); + auto requestedHeader = requested->peekAtFront(); + auto unsent = add(peer, 5, 5, false); + auto outside = add(peer, 5, 64, true); + auto otherTid = add(peer, 6, 2, true); + auto otherPeer = add(other, 5, 2, true); + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + ack->processTransmittedBlockAckReq(request, agreement); + ASSERT(ack->getQoSDataAckStatus(earlierHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(ack->getQoSDataAckStatus(requestedHeader) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setReceiverAddress(mac->getAddress()); + response->setTidInfo(5); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + offset); + BitVector bits; + for (int bit = 0; bit < 64; bit++) + bits.appendBit((offset == 0 && (bit == 2 || bit == 5)) || (offset == 6 && bit == 58) || offset == 100); + response->setBlockAckBitmap(bits); + auto packet = new Packet("supplied-response", response); + packet->insertAtBack(makeShared()); + originatorProcessReceivedControlFrame(packet, response, nullptr, request, AC_VI); + delete packet; + ASSERT(frames->containsFrame(earlier) == (offset != 0)); + if (offset != 0) + ASSERT(ack->getQoSDataAckStatus(earlierHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(frames->containsFrame(requested)); + ASSERT(ack->isEligibleToTransmit(requestedHeader)); + ASSERT(frames->containsFrame(unsent)); + ASSERT(ack->getQoSDataAckStatus(unsent->peekAtFront()) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + for (auto untouched : {outside, otherTid, otherPeer}) { + ASSERT(frames->containsFrame(untouched)); + ASSERT(ack->getQoSDataAckStatus(untouched->peekAtFront()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + } + ASSERT(frames->getLength() == (offset == 0 ? 5 : 6)); + std::cout << "Compressed HCF originator assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(OriginatorHcf); +class OriginatorTest : public cSimpleModule { + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("check")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->check(); + } +}; +Define_Module(OriginatorTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module OriginatorHcf extends Hcf { + parameters: + int start; + int offset; + @class(::inet::ieee80211::OriginatorHcf); +} +simple OriginatorTest { parameters: @class(::inet::ieee80211::OriginatorTest); } +network OriginatorNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: OriginatorTest; +} + +%inifile: omnetpp.ini +[General] +network = OriginatorNetwork +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "OriginatorHcf" +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.start = ${start=100,4094} +**.wlan[*].mac.hcf.offset = ${offset=0,6,100} +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Compressed HCF originator assertions passed. diff --git a/tests/module/Ieee80211HcfHeader_1.test b/tests/module/Ieee80211HcfHeader_1.test index 81703858adf..595c2b1b6b7 100644 --- a/tests/module/Ieee80211HcfHeader_1.test +++ b/tests/module/Ieee80211HcfHeader_1.test @@ -20,7 +20,7 @@ class HeaderAckPolicy : public OriginatorQosAckPolicy { selections++; return BLOCK_ACK; } - bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *) const override { return false; } + bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return false; } }; Define_Module(HeaderAckPolicy); class HeaderAgreementHandler : public OriginatorBlockAckAgreementHandler { diff --git a/tests/module/Ieee80211HcfMultipleProtection_1.test b/tests/module/Ieee80211HcfMultipleProtection_1.test index ea7991de3ee..a2ea0cc1346 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_1.test +++ b/tests/module/Ieee80211HcfMultipleProtection_1.test @@ -109,6 +109,12 @@ class ProtectionRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } }; class ProtectionHcf : public Hcf { diff --git a/tests/module/Ieee80211HcfMultipleProtection_2.test b/tests/module/Ieee80211HcfMultipleProtection_2.test index 748c19d83ef..bd2ef75b3d2 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_2.test +++ b/tests/module/Ieee80211HcfMultipleProtection_2.test @@ -2,8 +2,11 @@ Exercise the existing immediate Basic Block Ack exchange through HCF. Check zero-limit RTS estimates with an absent agreement, a Block Ack agreement, AARF query effects, and a deterministic rate decrease. Check response conversion at Tx. +Retain all seven existing scenarios. Add Compressed exchanges with positive and zero TXOP limits. +Repeat negative and fractional Duration/ID conversion with the 32-byte Compressed response. +The Compressed cases supply valid HT peer state and accepted agreements where the exchange requires them. -%extraargs: -r 0..6 +%extraargs: -r 0..9 %file: ProtectionRates.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -16,6 +19,8 @@ AARF query effects, and a deterministic rate decrease. Check response conversion #include "inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class RateProtectionRx : public Rx { @@ -41,14 +46,17 @@ class SeededOriginator : public OriginatorBlockAckAgreementHandler { }; class SeededRecipient : public RecipientBlockAckAgreementHandler { public: - void seed(const MacAddress& peer) { + void seed(const MacAddress& peer, bool compressed = false) { auto request = makeShared(); request->setTransmitterAddress(peer); request->setTid(5); request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setBufferSize(64); request->setBlockAckTimeoutValue(0); - addAgreement(request)->addbaResposneSent(); + auto agreement = addAgreement(request); + if (compressed) + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); } }; @@ -88,6 +96,8 @@ class RateProtectionMac : public Ieee80211Mac { std::vector ends; std::vector durations; std::vector rates; + std::vector lengths; + std::vector compressedFrames; bool suppressed = false; void sendDownFrame(Packet *packet) override { auto header = packet->peekAtFront(); @@ -97,6 +107,9 @@ class RateProtectionMac : public Ieee80211Mac { ends.push_back(simTime() + mode->getDuration(packet->getDataLength())); durations.push_back(header->getDurationField()); rates.push_back(mode->getDataMode()->getNetBitrate()); + lengths.push_back(packet->getDataLength()); + compressedFrames.push_back(bool(dynamicPtrCast(header)) || + bool(dynamicPtrCast(header))); Ieee80211Mac::sendDownFrame(packet); } protected: @@ -116,6 +129,7 @@ Define_Module(RateProtectionMac); class RateProtectionHcf : public Hcf { public: int acknowledgments = 0; + int managementAcknowledgments = 0; int blockAcks = 0; int failures = 0; void seedOriginator() { @@ -125,11 +139,21 @@ class RateProtectionHcf : public Hcf { handler->seed(MacAddress("02:00:00:00:00:02")); originatorBlockAckAgreementHandler = handler; } - void seedRecipient() { + void seedHtPeer(MacAddress peer) { + Enter_Method("seedHtPeer"); + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + auto radio = check_and_cast(getParentModule()->getParentModule()->getSubmodule("radio")); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + ASSERT(mib->findPeerHtState(peer) && mib->findPeerHtState(peer)->valid); + } + void seedRecipient(bool compressed = false) { Enter_Method("seedRecipient"); delete recipientBlockAckAgreementHandler; auto handler = new SeededRecipient(); - handler->seed(MacAddress("02:00:00:00:00:01")); + handler->seed(MacAddress("02:00:00:00:00:01"), compressed); recipientBlockAckAgreementHandler = handler; } void inject(int payload = 66, bool fragment = false) { @@ -147,28 +171,52 @@ class RateProtectionHcf : public Hcf { packet->insertAtBack(makeShared()); processUpperFrame(packet, header); } - void sendSyntheticBlockAck(simtime_t calculatedField) { + void sendSyntheticBlockAck(simtime_t calculatedField, bool compressed = false) { Enter_Method("sendSyntheticBlockAck"); - auto request = makeShared(); + Ptr request = compressed ? Ptr(makeShared()) : + Ptr(makeShared()); + if (compressed) { + auto bar = dynamicPtrCast(request); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + } request->setTransmitterAddress(MacAddress("02:00:00:00:00:07")); request->setReceiverAddress(mac->getAddress()); auto received = new Packet("synthetic-bar", request); auto mode = modeSet->getMode(bps(6000000)); setFrameMode(received, request, mode); - auto updatedRequest = received->removeAtFront(); - updatedRequest->setDurationField(calculatedField + modeSet->getSifsTime() + mode->getDuration(LENGTH_BASIC_BLOCKACK)); + auto updatedRequest = received->removeAtFront(); + updatedRequest->setDurationField(calculatedField + modeSet->getSifsTime() + + mode->getDuration(compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK)); received->insertAtFront(updatedRequest); request = updatedRequest; - auto response = makeShared(); + Ptr response; + if (compressed) { + auto ba = makeShared(); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(0)); + ba->setBlockAckBitmap(BitVector(0u, 64)); + response = ba; + } + else + response = makeShared(); response->setReceiverAddress(request->getTransmitterAddress()); - response->setDurationField(recipientAckPolicy->computeBasicBlockAckDurationField(received, request)); + response->setDurationField(compressed ? recipientAckPolicy->computeCompressedBlockAckDurationField(received, + dynamicPtrCast(request)) : + recipientAckPolicy->computeBasicBlockAckDurationField(received, + dynamicPtrCast(request))); auto packet = new Packet("synthetic-block-ack", response); Hcf::transmitControlResponseFrame(packet, response, received, request); delete received; } void originatorProcessReceivedFrame(Packet *packet, Packet *request) override { auto type = packet->peekAtFront()->getType(); - if (type == ST_ACK) acknowledgments++; + if (type == ST_ACK) { + if (dynamicPtrCast(request->peekAtFront())) + acknowledgments++; + else + managementAcknowledgments++; + } if (type == ST_BLOCKACK) blockAcks++; Hcf::originatorProcessReceivedFrame(packet, request); } @@ -193,29 +241,51 @@ class RateProtectionTest : public cSimpleModule { auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); auto senderMac = check_and_cast(getModuleByPath("^.sender.wlan[0].mac")); auto receiverMac = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac")); - int scenario = senderMac->par("scenario"); + int configuredScenario = senderMac->par("scenario"); + bool compressed = configuredScenario >= 7; + int scenario = configuredScenario == 7 ? 0 : configuredScenario == 8 ? 1 : configuredScenario == 9 ? 5 : configuredScenario; + if (compressed) { + sender->seedHtPeer(MacAddress("02:00:00:00:00:02")); + receiver->seedHtPeer(MacAddress("02:00:00:00:00:01")); + } if (scenario == 0 || scenario == 1) { sender->seedOriginator(); - receiver->seedRecipient(); + receiver->seedRecipient(compressed); } if (scenario == 2 || scenario == 3) check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf.rateControl"))->seedPeer(); if (scenario == 5) { - receiver->sendSyntheticBlockAck(SimTime(-10, SIMTIME_US)); + if (compressed) + receiver->seedHtPeer(MacAddress("02:00:00:00:00:07")); + receiver->sendSyntheticBlockAck(SimTime(-10, SIMTIME_US), compressed); wait(SimTime(300, SIMTIME_US)); - receiver->sendSyntheticBlockAck(SimTime(31.2e-6)); + receiver->sendSyntheticBlockAck(SimTime(31.2e-6), compressed); } else { sender->inject(66, scenario == 6); if (scenario == 0 || scenario == 1) - sender->inject(); // The existing BAR branch needs a next selected frame. + sender->inject(); } wait(SimTime(5, SIMTIME_MS)); if (scenario == 0 || scenario == 1) { ASSERT(sender->blockAcks >= 1); ASSERT(sender->acknowledgments == 0); + ASSERT(sender->managementAcknowledgments == 1); ASSERT(std::find(senderMac->types.begin(), senderMac->types.end(), ST_BLOCKACK_REQ) != senderMac->types.end()); ASSERT(receiverMac->types[0] == ST_CTS); + if (compressed) { + for (size_t i = 0; i < senderMac->types.size(); i++) + if (senderMac->types[i] == ST_BLOCKACK_REQ) { + ASSERT(senderMac->compressedFrames[i]); + ASSERT(senderMac->lengths[i] == B(24)); + } + for (size_t i = 0; i < receiverMac->types.size(); i++) + if (receiverMac->types[i] == ST_BLOCKACK) { + ASSERT(receiverMac->compressedFrames[i]); + ASSERT(receiverMac->lengths[i] == B(32)); + ASSERT(receiverMac->rates[i] == bps(6000000)); + } + } auto firstRtsEndpoint = senderMac->ends[0] + senderMac->durations[0]; if (scenario == 1) { ASSERT(firstRtsEndpoint - senderMac->starts[0] == SimTime(280, SIMTIME_US)); @@ -265,6 +335,12 @@ class RateProtectionTest : public cSimpleModule { ASSERT(receiverMac->types.size() == 2); ASSERT(receiverMac->durations[0] == 0); ASSERT(receiverMac->durations[1] == SimTime(32, SIMTIME_US)); + if (compressed) + for (size_t i = 0; i < 2; i++) { + ASSERT(receiverMac->compressedFrames[i]); + ASSERT(receiverMac->lengths[i] == B(32)); + ASSERT(receiverMac->rates[i] == bps(6000000)); + } } if (scenario == 6) { ASSERT(senderMac->types.size() == 2); @@ -272,7 +348,10 @@ class RateProtectionTest : public cSimpleModule { ASSERT(senderMac->ends[0] + senderMac->durations[0] - senderMac->starts[0] == SimTime(340, SIMTIME_US)); ASSERT(sender->acknowledgments == 1); } - std::cout << "Basic Block Ack, response conversion, and rate estimates passed.\n"; + if (compressed) + std::cout << "Compressed Block Ack protection and response duration checks passed.\n"; + else + std::cout << "Basic Block Ack, response conversion, and rate estimates passed.\n"; } }; Define_Module(RateProtectionTest); @@ -334,20 +413,22 @@ record-scalar-results = false *.radioMedium.propagation.propagationTime = 0s **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${scenario} >= 7 ? "ac" : "a" +**.bandName = ${scenario} >= 7 ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +**.centerFrequency = ${scenario} >= 7 ? 5.18GHz : 2.412GHz **.wlan[*].bitrate = 6Mbps **.wlan[*].mac.typename = "RateProtectionMac" -**.wlan[*].mac.scenario = ${scenario=0..6} +**.wlan[*].mac.scenario = ${scenario=0..9} **.wlan[*].mac.qosStation = true **.wlan[*].mac.rx.typename = "RateProtectionRx" **.wlan[*].mac.hcf.typename = "RateProtectionHcf" -**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == 0 || ${scenario} == 1 || ${scenario} == 5 -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == 0 || ${scenario} == 1 || ${scenario} == 5 || ${scenario} >= 7 +**.wlan[*].mac.hcf.blockAckVariant = ${scenario} >= 7 ? "compressed" : "basic" **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 1 **.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 4096B -*.sender.wlan[0].mac.hcf.rateControl.typename = ${rateControl="","","TraceAarf","TraceAarf","DecreaseRateControl","","" ! scenario} +*.sender.wlan[0].mac.hcf.rateControl.typename = ${rateControl="","","TraceAarf","TraceAarf","DecreaseRateControl","","","","","" ! scenario} *.sender.wlan[0].mac.hcf.rateControl.initialRate = 6Mbps *.sender.wlan[0].mac.hcf.rateControl.interval = 100us **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = ${scenario} >= 2 && ${scenario} <= 4 ? -1bps : 6Mbps @@ -358,7 +439,7 @@ record-scalar-results = false **.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 0B **.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 **.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 -**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 0 ? 1024us : 0s +**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 0 || ${scenario} == 7 ? 1024us : 0s *.sender.wlan[0].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = "multiple" *.sender.wlan[0].address = "02:00:00:00:00:01" *.receiver.wlan[0].address = "02:00:00:00:00:02" @@ -379,3 +460,6 @@ record-scalar-results = false %contains: stdout Basic Block Ack, response conversion, and rate estimates passed. + +%contains: stdout +Compressed Block Ack protection and response duration checks passed. diff --git a/tests/module/Ieee80211HcfSingleProtection_1.test b/tests/module/Ieee80211HcfSingleProtection_1.test index bcf69e606b9..fe64ee85701 100644 --- a/tests/module/Ieee80211HcfSingleProtection_1.test +++ b/tests/module/Ieee80211HcfSingleProtection_1.test @@ -91,6 +91,12 @@ class SingleRateSelection : public IQosRateSelection { const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } }; class SingleRateControl : public IRateControl { public: 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/module/Ieee80211PreparedQosData_1.test b/tests/module/Ieee80211PreparedQosData_1.test new file mode 100644 index 00000000000..6c7bc79a9b6 --- /dev/null +++ b/tests/module/Ieee80211PreparedQosData_1.test @@ -0,0 +1,294 @@ +%description: +Check reversible QoS DATA preparation through the production data service and PacketQueue. +Ten runs combine ordinary and aggregate previews with cancellation, commitment, stale sources, and stale sequence state. +Preparation must preserve source identities, ownership, deadlines, and sequence counters. +Commitment must return the same preview pointer and consume one sequence number and allocation index. +Ordinary commitment preserves queue service signals, elapsed queueing time, and queue events for head and selected non-head frames. +Refusal must preserve the preview and the queue state that follows the explicit invalidation. +The fixture uses no HCF runtime path. Its Edca subclass supplies only the production access-category mapping for aggregate lifetime registration. + +%extraargs: -r 0..9 + +%file: PreparedData.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/PacketEventTag.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/queueing/queue/PacketQueue.h" + +using namespace inet; +using namespace inet::ieee80211; + +// Aggregate commitment uses classifyFrame(). This fixture does not exercise channel access. +class PreparedEdca : public Edca { + protected: + void initialize(int) override {} +}; +Define_Module(PreparedEdca); + +class PreparedService : public OriginatorQosMacDataService { + std::unique_ptr ownedLifetime; + protected: + void initialize() override { + OriginatorQosMacDataService::initialize(); + auto lifetime = SimTime(1, SIMTIME_S); + ownedLifetime = std::make_unique(lifetime, lifetime, lifetime, lifetime); + setLifetimeHandler(ownedLifetime.get(), {}); + setEligibility([](Packet *) { return true; }, [](Packet *) { return true; }); + } + public: + EdcaTransmitLifetimeHandler *lifetime() const { return ownedLifetime.get(); } + int64_t allocations() const { return nextDataAllocationIndex; } +}; +Define_Module(PreparedService); + +class PreparedDataTest : public cSimpleModule, public cListener { + const MacAddress peer{"02:00:00:00:00:02"}; + PreparedService *service = nullptr; + queueing::PacketQueue *queue = nullptr; + queueing::PacketQueue *otherQueue = nullptr; + std::vector sources; + std::vector sourceIds; + std::vector sourceOwners; + std::vector deadlines; + std::unique_ptr preparation; + Packet *preview = nullptr; + int removals = 0; + int pulls = 0; + int aggregates = 0; + + bool aggregate() const { return par("aggregate") && operation() != "selectedCommit"; } + std::string operation() const { return par("operation").stdstringValue(); } + + Packet *enqueue(queueing::PacketQueue *destination, const char *name) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(peer); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(peer); + header->setTid(0); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(100))); + packet->insertAtBack(makeShared()); + auto time = packet->addRegionTag(header->getChunkLength(), B(100)); + time->setPacketTotalTimesArraySize(1); + time->setPacketTotalTimes(0, SIMTIME_ZERO); + packet->addRegionTag(header->getChunkLength(), B(100)); + packet->setArrivalTime(simTime()); + destination->enqueuePacket(packet); + service->lifetime()->registerFrame(packet, AC_BE); + return packet; + } + + void assertSources(int first, int expectedSequence = 0, int expectedAllocations = 0) const { + ASSERT(queue->getNumPackets() == int(sources.size()) - first); + ASSERT(service->getNextSequenceNumber(peer, 0) == SequenceNumberCyclic(expectedSequence)); + ASSERT(service->allocations() == expectedAllocations); + int index = 0; + for (int i = 0; i < int(sources.size()); i++) { + if (first && (operation() == "selectedCommit" ? i == 1 : i < first)) + continue; + auto packet = queue->getPacket(index++); + ASSERT(packet == sources[i] && packet->getId() == sourceIds[i]); + ASSERT(packet->getOwner() == sourceOwners[i]); + ASSERT(packet->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(!packet->findTag()); + ASSERT(service->lifetime()->findFrame(sourceIds[i]) == packet); + ASSERT(service->lifetime()->getDeadline(packet) == deadlines[i]); + ASSERT(packet->findRegionTag(packet->peekAtFront()->getChunkLength(), B(100))->getPacketTotalTimes(0) == SIMTIME_ZERO); + } + } + + void deleteCommitted(Packet *packet) { + service->lifetime()->forgetFrame(packet->getId()); + take(packet); + delete packet; + } + + void cancelPreview() { + ASSERT(preparation->packet.get() == preview && preview->getOwner() == preparation.get()); + auto live = cOwnedObject::getLiveObjectCount(); + preparation.reset(); + ASSERT(cOwnedObject::getLiveObjectCount() == live - 1); + preview = nullptr; + } + + void prepare() { + for (const char *name : {"first-msdu", "second-msdu"}) { + auto packet = enqueue(queue, name); + sources.push_back(packet); + sourceIds.push_back(packet->getId()); + sourceOwners.push_back(packet->getOwner()); + deadlines.push_back(service->lifetime()->getDeadline(packet)); + } + if (operation() == "selectedCommit") + service->setEligibility([](Packet *packet) { return !packet->isName("first-msdu"); }, [](Packet *) { return true; }); + preparation = service->prepareFrame(queue, aggregate()); + ASSERT(preparation && preparation->packet); + preview = preparation->packet.get(); + ASSERT(preview != sources[0] && preview != sources[1]); + ASSERT(preview->getOwner() == preparation.get()); + ASSERT(preparation->queue == queue && preparation->aggregate == aggregate()); + ASSERT(preparation->sourceIds.size() == (aggregate() ? 2 : 1)); + for (size_t i = 0; i < preparation->sourceIds.size(); i++) + ASSERT(preparation->sourceIds[i] == sourceIds[operation() == "selectedCommit" ? 1 : i]); + auto header = preview->peekAtFront(); + ASSERT(header->getAMsduPresent() == aggregate()); + ASSERT(header->getSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(!preview->findTag()); + ASSERT(service->lifetime()->findFrame(preview->getId()) == nullptr); + ASSERT(service->isPreparationValid(*preparation, simTime())); + ASSERT(removals == 0 && pulls == 0 && aggregates == 0); + assertSources(0); + scheduleAt(SimTime(3, SIMTIME_MS), new cMessage("check")); + } + + void check() { + assertSources(0); + ASSERT(service->isPreparationValid(*preparation, simTime())); + if (operation() == "commit" || operation() == "selectedCommit") { + auto committed = service->commitPreparedFrame(*preparation); + ASSERT(committed == preview && !preparation->packet); + ASSERT(committed->getOwner() == service); + ASSERT(committed->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(committed->getTag()->getAllocationIndex() == 0); + ASSERT(!service->isPreparationValid(*preparation, simTime())); + int consumed = aggregate() ? 2 : 1; + for (int i = 0; i < consumed; i++) + ASSERT(service->lifetime()->findFrame(sourceIds[operation() == "selectedCommit" ? 1 : i]) == nullptr); + auto expectedDeadline = aggregate() ? simTime() + SimTime(1, SIMTIME_S) : deadlines[operation() == "selectedCommit" ? 1 : 0]; + ASSERT(service->lifetime()->getDeadline(committed) == expectedDeadline); + ASSERT(removals == (aggregate() ? consumed : 0) && pulls == (aggregate() ? 0 : 1) && aggregates == (aggregate() ? 1 : 0)); + if (!aggregate()) { + auto offset = committed->peekAtFront()->getChunkLength(); + auto time = committed->findRegionTag(offset, B(100)); + ASSERT(time && time->getPacketTotalTimes(0) == SimTime(2, SIMTIME_MS)); + auto events = committed->findRegionTag(offset, B(100)); + ASSERT(events && events->getPacketEventsArraySize() == 1); + ASSERT(events->getPacketEvents(0)->getKind() == PEK_QUEUED); + ASSERT(events->getPacketEvents(0)->getPacketDuration() == SimTime(2, SIMTIME_MS)); + } + assertSources(consumed, 1, 1); + deleteCommitted(committed); + preparation.reset(); + preview = nullptr; + } + else if (operation() == "staleSource") { + auto removed = queue->getPacket(0); + queue->removePacket(removed); + service->lifetime()->forgetFrame(removed->getId()); + take(removed); + delete removed; + ASSERT(!service->isPreparationValid(*preparation, simTime())); + ASSERT(service->commitPreparedFrame(*preparation) == nullptr); + assertSources(1); + ASSERT(removals == 1 && pulls == 0 && aggregates == 0); + cancelPreview(); + assertSources(1); + } + else if (operation() == "staleSequence") { + enqueue(otherQueue, "independent-msdu"); + auto other = service->prepareFrame(otherQueue, false); + ASSERT(other && service->isPreparationValid(*other, simTime())); + auto otherPreview = other->packet.get(); + auto committed = service->commitPreparedFrame(*other); + ASSERT(committed == otherPreview && otherQueue->isEmpty()); + ASSERT(!service->isPreparationValid(*preparation, simTime())); + ASSERT(service->commitPreparedFrame(*preparation) == nullptr); + assertSources(0, 1, 1); + ASSERT(removals == 0 && pulls == 0 && aggregates == 0); + deleteCommitted(committed); + cancelPreview(); + assertSources(0, 1, 1); + } + else { + ASSERT(operation() == "cancel"); + cancelPreview(); + assertSources(0); + ASSERT(removals == 0 && pulls == 0 && aggregates == 0); + } + while (!queue->isEmpty()) { + auto packet = queue->getPacket(0); + queue->removePacket(packet); + deleteCommitted(packet); + } + ASSERT(otherQueue->isEmpty() && service->lifetime()->getNumFrames() == 0); + std::cout << "Prepared QoS DATA assertions passed: aggregate=" << aggregate() + << " operation=" << operation() << ".\n"; + endSimulation(); + } + + protected: + void initialize() override { + service = check_and_cast(getModuleByPath("^.service")); + queue = check_and_cast(getModuleByPath("^.queue")); + otherQueue = check_and_cast(getModuleByPath("^.otherQueue")); + queue->subscribe(packetRemovedSignal, this); + queue->subscribe(packetPulledSignal, this); + service->subscribe(IOriginatorMacDataService::packetAggregatedSignal, this); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("prepare")); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { + if (signal == packetRemovedSignal) removals++; + if (signal == packetPulledSignal) pulls++; + if (signal == IOriginatorMacDataService::packetAggregatedSignal) aggregates++; + } + void handleMessage(cMessage *message) override { + bool first = message->isName("prepare"); + delete message; + if (first) + prepare(); + else + check(); + } +}; +Define_Module(PreparedDataTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.originator.OriginatorQosMacDataService; +import inet.queueing.queue.PacketQueue; + +simple PreparedEdca extends SimpleModule { parameters: @class(::PreparedEdca); } +module PreparedService extends OriginatorQosMacDataService { parameters: @class(::PreparedService); } +simple PreparedDataTest { + parameters: + bool aggregate; + string operation; + @class(::PreparedDataTest); +} +network PreparedDataNetwork { + submodules: + edca: PreparedEdca; + service: PreparedService; + queue: PacketQueue; + otherQueue: PacketQueue; + test: PreparedDataTest; + connections allowunconnected: +} + +%inifile: omnetpp.ini +[General] +network = PreparedDataNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.aggregate = ${aggregate=false,true} +*.test.operation = ${operation="cancel","commit","staleSource","staleSequence","selectedCommit"} +*.service.msduAggregationPolicy.subframeNumThreshold = 2 +*.service.msduAggregationPolicy.maxAMsduSize = 1000 +*.service.mpduAggregationPolicy.typename = "" +*.service.fragmentationPolicy.typename = "" + +%contains: stdout +Prepared QoS DATA assertions passed: diff --git a/tests/module/Ieee80211RecipientNav_1.test b/tests/module/Ieee80211RecipientNav_1.test index c4812c86883..bb8a4008e40 100644 --- a/tests/module/Ieee80211RecipientNav_1.test +++ b/tests/module/Ieee80211RecipientNav_1.test @@ -5,8 +5,10 @@ Retain received NAV refusal and DCF local NAV updates for holder frames. Require zero HCF local NAV updates for holder frames. Check a Basic Block Ack response after an ADDBA exchange through production HCF. Require its positive Duration/ID field without a local NAV update. +Repeat the response checks with Compressed and valid supplied HT peer state. +The Compressed case uses a 32-byte response MPDU and retains the same NAV invariant. -%extraargs: -r 0..2 +%extraargs: -r 0..3 %file: RecipientNav.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -14,6 +16,8 @@ Require its positive Duration/ID field without a local NAV update. #include "inet/linklayer/ieee80211/mac/Rx.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class RecipientNavRx : public Rx { @@ -32,6 +36,9 @@ class RecipientNavMac : public Ieee80211Mac { std::vector durations; simtime_t addbaResponseEnd = -1; Ptr basicBlockAck; + Ptr compressedBlockAck; + units::values::b blockAckLength; + const physicallayer::IIeee80211Mode *blockAckMode = nullptr; void sendDownFrame(Packet *packet) override { auto header = packet->peekAtFront(); types.push_back(header->getType()); @@ -43,6 +50,11 @@ class RecipientNavMac : public Ieee80211Mac { } if (auto response = dynamicPtrCast(header)) basicBlockAck = response; + if (auto response = dynamicPtrCast(header)) { + compressedBlockAck = response; + blockAckLength = packet->getDataLength(); + blockAckMode = packet->getTag()->getMode(); + } Ieee80211Mac::sendDownFrame(packet); } void injectBlockAckSetup() { @@ -58,12 +70,33 @@ class RecipientNavMac : public Ieee80211Mac { request->setStartingSequenceNumber(SequenceNumberCyclic(0)); injectBlockAckFrame(request); } - void injectBlockAckRequest() { + void prepareCompressedPeer() { + Enter_Method("prepareCompressedPeer"); + const MacAddress peer("02:00:00:00:00:02"); + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + auto radio = check_and_cast(getParentModule()->getSubmodule("radio")); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + ASSERT(mib->findPeerHtState(peer) && mib->findPeerHtState(peer)->valid); + } + void injectBlockAckRequest(bool compressed = false) { Enter_Method("injectBlockAckRequest"); - auto request = makeShared(); + Ptr request; + if (compressed) { + auto bar = makeShared(); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request = bar; + } + else { + auto bar = makeShared(); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request = bar; + } request->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); - request->setTidInfo(5); - request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setDurationField(SimTime(500, SIMTIME_US)); injectBlockAckFrame(request); } @@ -77,7 +110,7 @@ class RecipientNavMac : public Ieee80211Mac { header->setReceiverAddress(getAddress()); auto packet = new Packet("received-block-ack-exchange", header); packet->addTag()->setMode( - physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000))); + physicallayer::Ieee80211ModeSet::getModeSet(mib->isHtOperationSupported() ? "n(mixed-2.4Ghz)" : "a")->getMode(bps(6000000))); auto trailer = makeShared(); trailer->setFcsMode(FCS_DECLARED_CORRECT); packet->insertAtBack(trailer); @@ -140,8 +173,10 @@ class RecipientNavTest : public cSimpleModule { public: RecipientNavTest() : cSimpleModule(65536) {} protected: - void checkBasicBlockAck(RecipientNavMac *mac, RecipientNavRx *rx) { - auto mode = physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000)); + void checkBlockAck(RecipientNavMac *mac, RecipientNavRx *rx, bool compressed) { + auto mode = physicallayer::Ieee80211ModeSet::getModeSet(compressed ? "n(mixed-2.4Ghz)" : "a")->getMode(bps(6000000)); + if (compressed) + mac->prepareCompressedPeer(); mac->injectBlockAckSetup(); for (int i = 0; i < 100 && mac->addbaResponseEnd < SIMTIME_ZERO; i++) wait(SimTime(10, SIMTIME_US)); @@ -154,27 +189,40 @@ class RecipientNavTest : public cSimpleModule { ASSERT(rx->isMediumFree()); auto updatesBeforeResponse = rx->localUpdates; auto framesBeforeResponse = mac->types.size(); - mac->injectBlockAckRequest(); + mac->injectBlockAckRequest(compressed); wait(SimTime(350, SIMTIME_US)); ASSERT(mac->types.size() == framesBeforeResponse + 1); - ASSERT(mac->basicBlockAck != nullptr); - ASSERT(mac->basicBlockAck->getReceiverAddress() == MacAddress("02:00:00:00:00:02")); - ASSERT(mac->basicBlockAck->getTidInfo() == 5); - ASSERT(mac->basicBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + Ptr response; + if (compressed) { + ASSERT(mac->compressedBlockAck != nullptr && mac->basicBlockAck == nullptr); + ASSERT(mac->compressedBlockAck->getTidInfo() == 5); + ASSERT(mac->compressedBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(mac->compressedBlockAck->getBlockAckBitmap().getSize() == 64); + ASSERT(mac->blockAckLength == B(32)); + ASSERT(mac->blockAckMode == mode && mac->blockAckMode->getHtMcsIndex() < 0); + response = mac->compressedBlockAck; + } + else { + ASSERT(mac->basicBlockAck != nullptr); + ASSERT(mac->basicBlockAck->getTidInfo() == 5); + ASSERT(mac->basicBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + response = mac->basicBlockAck; + } + ASSERT(response->getReceiverAddress() == MacAddress("02:00:00:00:00:02")); // IEEE Std 802.11-2024, 9.2.5.6: subtract SIFS and response airtime from the request duration. - auto expectedDuration = SimTime(500, SIMTIME_US) - mode->getSifsTime() - mode->getDuration(LENGTH_BASIC_BLOCKACK); + auto expectedDuration = SimTime(500, SIMTIME_US) - mode->getSifsTime() - mode->getDuration(compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); ASSERT(expectedDuration > SIMTIME_ZERO); - ASSERT(mac->basicBlockAck->getDurationField() == expectedDuration); + ASSERT(response->getDurationField() == expectedDuration); ASSERT(rx->localUpdates == updatesBeforeResponse); ASSERT(rx->isMediumFree()); - std::cout << "Basic Block Ack recipient NAV checks passed.\n"; + std::cout << (compressed ? "Compressed" : "Basic") << " Block Ack recipient NAV checks passed.\n"; } void activity() override { wait(SimTime(1, SIMTIME_MS)); auto mac = check_and_cast(getModuleByPath("^.host.wlan[0].mac")); auto rx = check_and_cast(mac->getSubmodule("rx")); - if (par("basicBlockAckOnly").boolValue()) { - checkBasicBlockAck(mac, rx); + if (par("basicBlockAckOnly").boolValue() || par("compressedBlockAckOnly").boolValue()) { + checkBlockAck(mac, rx, par("compressedBlockAckOnly").boolValue()); std::cout << "Recipient NAV production checks passed.\n"; return; } @@ -231,6 +279,7 @@ simple RecipientNavRx extends Rx { simple RecipientNavTest extends SimpleModule { parameters: bool basicBlockAckOnly = default(false); + bool compressedBlockAckOnly = default(false); @class(::inet::ieee80211::RecipientNavTest); } network RecipientNavNetwork { @@ -251,14 +300,15 @@ record-vector-results = false record-scalar-results = false **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${scenario} == "compressedBlockAck" ? "n(mixed-2.4Ghz)" : "a" **.wlan[*].bitrate = 6Mbps **.wlan[*].mac.typename = "RecipientNavMac" -**.wlan[*].mac.qosStation = ${scenario="hcf","dcf","blockAck"} != "dcf" +**.wlan[*].mac.qosStation = ${scenario="hcf","dcf","blockAck","compressedBlockAck"} != "dcf" **.wlan[*].mac.rx.typename = "RecipientNavRx" -**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == "blockAck" -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == "blockAck" || ${scenario} == "compressedBlockAck" +**.wlan[*].mac.hcf.blockAckVariant = ${scenario} == "compressedBlockAck" ? "compressed" : "basic" *.test.basicBlockAckOnly = ${scenario} == "blockAck" +*.test.compressedBlockAckOnly = ${scenario} == "compressedBlockAck" **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps @@ -287,3 +337,6 @@ Recipient NAV production checks passed. %contains: stdout Basic Block Ack recipient NAV checks passed. + +%contains: stdout +Compressed Block Ack recipient NAV checks passed. diff --git a/tests/module/Ieee80211SingleProtectionGroup_1.test b/tests/module/Ieee80211SingleProtectionGroup_1.test index c5ff47f33c0..76d8e3e744e 100644 --- a/tests/module/Ieee80211SingleProtectionGroup_1.test +++ b/tests/module/Ieee80211SingleProtectionGroup_1.test @@ -28,6 +28,8 @@ class GroupRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txop) override { return delegate->computeMode(packet, header, txop); } const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + 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 452f1bc2091..5a740809038 100644 --- a/tests/module/Ieee80211Txnav_1.test +++ b/tests/module/Ieee80211Txnav_1.test @@ -32,6 +32,8 @@ class TxnavSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseAckFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } + 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 72514a2b82b..72d90237935 100644 --- a/tests/module/Ieee80211VhtHtSupport_1.test +++ b/tests/module/Ieee80211VhtHtSupport_1.test @@ -68,6 +68,12 @@ class VhtHtSelection : public IQosRateSelection { const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } + const IIeee80211Mode *computeCompressedBlockAckResponseMode(const IIeee80211Mode *mode) override { + return delegate->computeCompressedBlockAckResponseMode(mode); + } }; class VhtHtRx : public Rx { public: diff --git a/tests/module/PacketSelectiveExtraction_1.test b/tests/module/PacketSelectiveExtraction_1.test new file mode 100644 index 00000000000..1391d286fd5 --- /dev/null +++ b/tests/module/PacketSelectiveExtraction_1.test @@ -0,0 +1,213 @@ +%description: +Check selected extraction through simple and compound queues. +Extraction must preserve ownership, residence metadata, and one pull event per queue layer. +The predicate query must preserve storage and configured scheduler order. + +%extraargs: -r 0..5 + +%file: SelectiveExtraction.cc +#include "inet/common/PacketEventTag.h" +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/queueing/contract/IPacketQueue.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" +#include "inet/queueing/scheduler/PriorityScheduler.h" + +namespace inet::queueing { +class SelectiveScheduler : public PriorityScheduler { + public: + int getProcessedCount() const { return numProcessedPackets; } +}; +Define_Module(SelectiveScheduler); + +class SelectiveExtractionTest : public cSimpleModule, public cListener { + protected: + IPacketQueue *queue = nullptr; + cModule *queueModule = nullptr; + Packet *first = nullptr; + Packet *second = nullptr; + std::map pulls; + int removals = 0; + + Packet *makePacket(const char *name, bool multicast) { + auto header = makeShared(); + header->setType(ieee80211::ST_DATA_WITH_QOS); + header->addChunkLength(ieee80211::QOSCONTROL_PART_LENGTH); + header->setTid(5); + header->setReceiverAddress(MacAddress(multicast ? "01:00:00:00:00:01" : "02:00:00:00:00:02")); + auto packet = new Packet(name, header); + packet->setArrivalTime(simTime()); + packet->insertAtBack(makeShared(B(20))); + auto timeTag = packet->addRegionTag(); + timeTag->setPacketTotalTimesArraySize(1); + timeTag->setPacketTotalTimes(0, SIMTIME_ZERO); + packet->addRegionTag(); + return packet; + } + + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("enqueue")); + } + + void handleMessage(cMessage *message) override { + bool enqueue = !strcmp(message->getName(), "enqueue"); + delete message; + if (enqueue) { + queueModule = getModuleByPath(par("compound").boolValue() ? "^.compound" : "^.simpleQueue"); + queue = check_and_cast(queueModule); + queueModule->subscribe(packetPulledSignal, this); + queueModule->subscribe(packetRemovedSignal, this); + first = makePacket("first", true); + second = makePacket("second", false); + queue->enqueuePacket(first); + queue->enqueuePacket(second); + scheduleAt(SimTime(11, SIMTIME_MS), new cMessage("extract")); + return; + } + auto selective = check_and_cast(queue); + auto unsupported = check_and_cast(getModuleByPath("^.unsupportedScheduler")); + bool unsupportedQuery = false; + try { + unsupported->peekEligiblePacket([](Packet *) { return true; }); + } + catch (const UnsupportedPacketSelection&) { + unsupportedQuery = true; + } + ASSERT(unsupportedQuery); + auto selected = par("head").boolValue() ? first : second; + auto predicate = [selected](Packet *packet) { return packet == selected; }; + ASSERT(selective->peekEligiblePacket(predicate) == selected); + ASSERT(queue->getNumPackets() == 2); + ASSERT(pulls.empty() && removals == 0); + auto outsider = makePacket("outsider", false); + bool unsupportedExtraction = false; + try { + unsupported->extractPacket(outsider); + } + catch (const UnsupportedPacketSelection&) { + unsupportedExtraction = true; + } + ASSERT(unsupportedExtraction); + bool rejected = false; + try { + selective->extractPacket(outsider); + } + catch (const cRuntimeError&) { + rejected = true; + } + ASSERT(rejected && queue->getNumPackets() == 2); + ASSERT(pulls.empty() && removals == 0); + delete outsider; + if (par("compound").boolValue()) { + auto any = selective->peekEligiblePacket([](Packet *) { return true; }); + ASSERT(any == (par("reverse").boolValue() ? second : first)); + } + auto extracted = selective->extractPacket(selected); + ASSERT(extracted == selected); + take(extracted); + ASSERT(extracted->getOwner() == this); + ASSERT(queue->getNumPackets() == 1); + ASSERT(queue->getPacket(0) == (selected == first ? second : first)); + ASSERT(removals == 0); + auto timeTag = extracted->findRegionTag(); + ASSERT(timeTag != nullptr); + ASSERT(timeTag->getPacketTotalTimes(0) == SimTime(10, SIMTIME_MS)); + auto eventTag = extracted->findRegionTag(); + ASSERT(eventTag != nullptr && eventTag->getPacketEventsArraySize() == 1); + ASSERT(eventTag->getPacketEvents(0)->getKind() == PEK_QUEUED); + ASSERT(eventTag->getPacketEvents(0)->getPacketDuration() == SimTime(10, SIMTIME_MS)); + auto retained = queue->getPacket(0); + ASSERT(retained->findRegionTag()->getPacketTotalTimes(0) == SIMTIME_ZERO); + ASSERT(retained->findRegionTag()->getPacketEventsArraySize() == 0); + if (par("compound").boolValue()) { + ASSERT(pulls.size() == 3); + auto scheduler = check_and_cast(queueModule->getSubmodule("prioritizer")); + ASSERT(scheduler->getProcessedCount() == 1); + } + else + ASSERT(pulls.size() == 1); + for (auto entry : pulls) + ASSERT(entry.second == 1); + delete extracted; + queueModule->unsubscribe(packetPulledSignal, this); + queueModule->unsubscribe(packetRemovedSignal, this); + std::cout << "Selected extraction assertions passed.\n"; + } + + void receiveSignal(cComponent *source, simsignal_t signal, cObject *, cObject *) override { + if (signal == packetPulledSignal) + pulls[source->getFullPath()]++; + else if (signal == packetRemovedSignal) + removals++; + } +}; +Define_Module(SelectiveExtractionTest); +} + +%file: test.ned +import inet.queueing.classifier.PacketClassifier; +import inet.queueing.queue.CompoundPacketQueueBase; +import inet.queueing.queue.PacketQueue; +import inet.queueing.scheduler.PriorityScheduler; +import inet.queueing.source.PassivePacketSource; +simple SelectiveScheduler extends PriorityScheduler { parameters: @class(::inet::queueing::SelectiveScheduler); } +module SelectiveCompoundQueue extends CompoundPacketQueueBase { + submodules: + classifier: PacketClassifier { + parameters: classifierClass = "inet::ieee80211::MgmtOverMulticastOverUnicastClassifier"; + } + managementQueue: PacketQueue; + multicastQueue: PacketQueue; + unicastQueue: PacketQueue; + prioritizer: SelectiveScheduler; + connections: + in --> classifier.in; + classifier.out++ --> managementQueue.in; + classifier.out++ --> multicastQueue.in; + classifier.out++ --> unicastQueue.in; + managementQueue.out --> prioritizer.in++; + multicastQueue.out --> prioritizer.in++; + unicastQueue.out --> prioritizer.in++; + prioritizer.out --> out; +} +simple SelectiveExtractionTest { + parameters: + bool compound; + bool head; + bool reverse; + @class(::inet::queueing::SelectiveExtractionTest); +} +network SelectiveExtractionNetwork { + submodules: + simpleQueue: PacketQueue; + compound: SelectiveCompoundQueue; + unsupportedProvider: PassivePacketSource { + parameters: packetLength = 20B; + } + unsupportedQueue: PacketQueue; + unsupportedScheduler: PriorityScheduler; + test: SelectiveExtractionTest; + connections allowunconnected: + unsupportedProvider.out --> unsupportedScheduler.in++; + unsupportedQueue.out --> unsupportedScheduler.in++; +} + +%inifile: omnetpp.ini +[General] +network = SelectiveExtractionNetwork +ned-path = .;../../../../src +sim-time-limit = 12ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.compound = ${compound=false,true,true} +*.test.reverse = ${reverse=false,false,true ! compound} +*.test.head = ${head=false,true} +*.compound.classifier.classifierClass = "inet::ieee80211::MgmtOverMulticastOverUnicastClassifier" +*.compound.prioritizer.reverseOrder = ${reverse} + +%contains: stdout +Selected extraction assertions passed. diff --git a/tests/protocol/wifi/11ac/Ac_BlockAck.test b/tests/protocol/wifi/11ac/Ac_BlockAck.test index eeaa886371b..e6407799617 100644 --- a/tests/protocol/wifi/11ac/Ac_BlockAck.test +++ b/tests/protocol/wifi/11ac/Ac_BlockAck.test @@ -1,13 +1,22 @@ %description: 802.11ac Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] +Check explicit Basic, explicit Compressed, and the NED default with concrete BAR/BA types and positive receipt bits. +The third run uses the unquoted INI keyword default. OMNeT++ accepts the parameter's NED default for this keyword. + +%extraargs: -r 0..2 + +%postrun-command: python3 check-default.py %file: Ac_BlockAck.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { +using namespace inet::ieee80211; + // Block Ack session on 11ac: STA sends ADDBA Request (type 13), // then BlockAckReq (type 24), AP replies with BlockAck (type 25). Define_ProtocolTest(wifi_11ac_block_ack) @@ -18,21 +27,63 @@ Define_ProtocolTest(wifi_11ac_block_ack) .describe("ADDBA action frame from sta1").within(2.5)) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) .describe("BlockAckReq from sta1").within(1.0)) .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (int i = 0; i < 64; i++) + for (unsigned int j = 0; j < ba->getBlockAckBitmap(i).getSize(); j++) + if (ba->getBlockAckBitmap(i).getBit(j)) return true; + return false; + }) .describe("BlockAck received at sta1").within(0.02)); } +Define_ProtocolTest(wifi_11ac_compressed_exchange) +{ + return ProtocolTest("wifi_11ac_compressed_exchange") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 13") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return response && response->getStatusCode() == 0 && response->getBlockAckPolicy(); + }) + .describe("successful immediate ADDBA response").within(2.5)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) + .describe("Compressed BAR from sta1").within(1.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (unsigned int i = 0; i < ba->getBlockAckBitmap().getSize(); i++) + if (ba->getBlockAckBitmap().getBit(i)) return true; + return false; + }) + .describe("Compressed Block Ack with positive receipt evidence").within(0.02)); +} + } // namespace protocoltest } // namespace inet %inifile: test.ini [General] -*.tester.testName = "wifi_11ac_block_ack" +*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange","wifi_11ac_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed",default ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -48,5 +99,22 @@ include ../../ini/_base.ini **.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 *.sta*.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +%file: check-default.py +from pathlib import Path +import re + +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", Path("test.out").read_text())[1:] +assert len(runs) == 3, len(runs) +for run, profile in zip(runs, ["basic", "compressed", "default"]): + assert f'$profile={profile if profile == "default" else chr(34) + profile + chr(34)}' in run + resolved = "basic" if profile == "basic" else "compressed" + assert run.count(f"Block Ack profile: {resolved}") == 3 + test = "wifi_11ac_block_ack" if profile == "basic" else "wifi_11ac_compressed_exchange" + assert f"PROTOCOLTEST {test}: PASS" in run +print("Basic, explicit Compressed, and NED default exchanges passed.") + %contains: stdout PROTOCOLTEST wifi_11ac_block_ack: PASS + +%contains: stdout +PROTOCOLTEST wifi_11ac_compressed_exchange: PASS diff --git a/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test b/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test new file mode 100644 index 00000000000..dd86abc74e5 --- /dev/null +++ b/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test @@ -0,0 +1,186 @@ +%description: +Check runtime Compressed fallback under a controlled peer HT capability gap. +Simplified management supplies static association and valid HT state after initialization. +Before DATA, the fixture removes peer HT records at both ends of both wireless hops. +The fixture retains association, local HT support, ac operation, and Compressed Block Ack support. +During traffic, require delivered Normal Ack QoS DATA without ADDBA, BAR, or Block Ack. +Check absent peer HT records during traffic and at the final audit. +This fixture does not establish an on-air association exchange or a limitation of all ac stations. + +%file: Ac_BlockAckFallback.cc +#include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +namespace inet::protocoltest { +using namespace inet::ieee80211; + +class FallbackHcf : public Hcf { + public: + bool hasNoAgreements() const { + return originatorBlockAckAgreementHandler->getAgreements().empty() && + recipientBlockAckAgreementHandler->getAgreements().empty(); + } +}; +Define_Module(FallbackHcf); + +class BlockAckFallbackAudit : public cSimpleModule, public cListener { + protected: + Ieee80211Mib *ap = nullptr; + Ieee80211Mib *sta1 = nullptr; + Ieee80211Mib *sta2 = nullptr; + std::vector hcfs; + std::map macs; + bool removed = false; + bool completed = false; + int delivered = 0; + int sentFirstHop = 0; + int receivedFirstHop = 0; + int sentSecondHop = 0; + int receivedSecondHop = 0; + simsignal_t sentSignal; + simsignal_t receivedSignal; + simsignal_t appSignal; + + void checkAssociation() const { + for (auto sta : {sta1, sta2}) { + ASSERT(sta->bssStationData.isAssociated); + ASSERT(sta->bssData.bssid == ap->address); + auto it = ap->bssAccessPointData.stations.find(sta->address); + ASSERT(it != ap->bssAccessPointData.stations.end() && it->second == Ieee80211Mib::ASSOCIATED); + } + for (auto mib : {ap, sta1, sta2}) + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + for (auto hcf : hcfs) { + ASSERT(hcf->par("isBlockAckSupported").boolValue()); + ASSERT(std::string(hcf->par("blockAckVariant").stringValue()) == "compressed"); + ASSERT(hcf->hasNoAgreements()); + } + } + void checkGap() const { + checkAssociation(); + ASSERT(ap->findPeerHtState(sta1->address) == nullptr); + ASSERT(ap->findPeerHtState(sta2->address) == nullptr); + ASSERT(sta1->findPeerHtState(ap->address) == nullptr); + ASSERT(sta2->findPeerHtState(ap->address) == nullptr); + } + void initialize() override { + sentSignal = registerSignal("packetSentToLower"); + receivedSignal = registerSignal("packetReceivedFromLower"); + appSignal = registerSignal("packetReceived"); + scheduleAt(SIMTIME_ZERO, new cMessage("remove")); + scheduleAt(SimTime(1100, SIMTIME_MS), new cMessage("audit")); + } + void handleMessage(cMessage *message) override { + bool remove = message->isName("remove"); + delete message; + if (remove) { + ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + sta1 = check_and_cast(getModuleByPath("^.sta1.wlan[0].mib")); + sta2 = check_and_cast(getModuleByPath("^.sta2.wlan[0].mib")); + for (const char *name : {"ap", "sta1", "sta2"}) { + auto wlan = getParentModule()->getSubmodule(name)->getSubmodule("wlan", 0); + ASSERT(std::string(wlan->par("opMode").stringValue()) == "ac"); + auto mac = check_and_cast(wlan->getSubmodule("mac")); + macs[name] = mac; + hcfs.push_back(check_and_cast(mac->getSubmodule("hcf"))); + mac->subscribe(sentSignal, this); + mac->subscribe(receivedSignal, this); + } + checkAssociation(); + for (auto sta : {sta1, sta2}) { + auto atAp = ap->findPeerHtState(sta->address); + auto atSta = sta->findPeerHtState(ap->address); + ASSERT(atAp && atAp->valid && atSta && atSta->valid); + } + for (auto sta : {sta1, sta2}) { + ap->removePeerHtCapabilities(sta->address); + sta->removePeerHtCapabilities(ap->address); + } + getModuleByPath("^.sta2.app[0]")->subscribe(appSignal, this); + removed = true; + checkGap(); + } + else { + ASSERT(removed); + checkGap(); + ASSERT(delivered >= 5); + ASSERT(sentFirstHop >= 5 && receivedFirstHop >= 5); + ASSERT(sentSecondHop >= 5 && receivedSecondHop >= 5); + completed = true; + std::cout << "Absent peer HT fallback: Normal Ack delivered=" << delivered + << " firstHop=" << receivedFirstHop << " secondHop=" << receivedSecondHop << "\n"; + emit(registerSignal("fallbackAuditPassed"), intval_t(1)); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *) override { + ASSERT(removed); + checkGap(); + auto packet = check_and_cast(object); + if (signal == appSignal) { + delivered++; + return; + } + auto header = packet->peekAtFront(); + ASSERT(!dynamicPtrCast(header)); + ASSERT(!dynamicPtrCast(header)); + ASSERT(header->getType() != ST_BLOCKACK_REQ && header->getType() != ST_BLOCKACK); + auto data = dynamicPtrCast(header); + if (!data) + return; + ASSERT(data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == NORMAL_ACK); + bool firstHop = data->getTransmitterAddress() == sta1->address && data->getReceiverAddress() == ap->address; + bool secondHop = data->getTransmitterAddress() == ap->address && data->getReceiverAddress() == sta2->address; + if (signal == sentSignal) { + sentFirstHop += firstHop && source == macs.at("sta1"); + sentSecondHop += secondHop && source == macs.at("ap"); + } + else if (signal == receivedSignal) { + receivedFirstHop += firstHop && source == macs.at("ap"); + receivedSecondHop += secondHop && source == macs.at("sta2"); + } + } + void finish() override { ASSERT(completed); } +}; +Define_Module(BlockAckFallbackAudit); + +Define_ProtocolTest(wifi_11ac_blockack_fallback) +{ + return ProtocolTest("wifi_11ac_blockack_fallback") + .once(on("audit").signal("fallbackAuditPassed") + .describe("Normal Ack delivery with absent peer HT state on both hops").within(1.2)); +} +} + +%file: FallbackNetwork.ned +import inet.protocoltest.wifi.WifiInfraNetwork; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +module FallbackHcf extends Hcf { parameters: @class(::inet::protocoltest::FallbackHcf); } +simple BlockAckFallbackAudit { parameters: @class(::inet::protocoltest::BlockAckFallbackAudit); } +network FallbackNetwork extends WifiInfraNetwork { + submodules: + audit: BlockAckFallbackAudit; +} + +%inifile: test.ini +[General] +network = FallbackNetwork +*.tester.testName = "wifi_11ac_blockack_fallback" +*.ap.wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified" +*.sta*.wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified" +*.sta*.wlan[*].agent.typename = "" +*.sta*.wlan[*].mgmt.accessPointAddress = "ap" +**.wlan[*].mac.hcf.typename = "FallbackHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +*.sta1.app[0].sendInterval = 0.1ms +*.sta1.app[0].stopTime = 1.01s +include ../../ini/_ac.ini +include ../../ini/_base.ini +**.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B +**.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 100 +**.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 + +%contains: stdout +PROTOCOLTEST wifi_11ac_blockack_fallback: PASS diff --git a/tests/protocol/wifi/11n/N_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..2cc0843219b --- /dev/null +++ b/tests/protocol/wifi/11n/N_AmpduImplicitBa.test @@ -0,0 +1,153 @@ +%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/MpduAggregationUtils.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 computeAmpduRangeSuccessRate(const ISnir *, const IIeee80211Mode *, B length) const override { + 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; + auto layout = inspectMpduAggregate(packet, B(0), B(packet->getDataLength())); + bool retry = false; + int members = 0; + for (const auto& range : layout.ranges) { + if (range.kind != MpduRangeKind::MPDU) + continue; + auto data = packet->peekDataAt(range.offset); + ASSERT(data->getAckPolicy() == NORMAL_ACK && data->getFragmentNumber() == 0 && !data->getMoreFragments()); + retry |= data->getRetry(); + members++; + } + return layout.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/11n/N_BlockAck.test b/tests/protocol/wifi/11n/N_BlockAck.test index d5ab66019fe..0ae29c49cad 100644 --- a/tests/protocol/wifi/11n/N_BlockAck.test +++ b/tests/protocol/wifi/11n/N_BlockAck.test @@ -1,13 +1,22 @@ %description: 802.11n Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] +Check explicit Basic, explicit Compressed, and the NED default with concrete BAR/BA types and positive receipt bits. +The third run uses the unquoted INI keyword default. OMNeT++ accepts the parameter's NED default for this keyword. + +%extraargs: -r 0..2 + +%postrun-command: python3 check-default.py %file: N_BlockAck.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { +using namespace inet::ieee80211; + // Block Ack session flow: STA sends ADDBA Request (action frame, type 13), // AP responds with ADDBA Response (type 13), then after enough frames are // queued, STA sends BlockAckReq (type 24) and AP sends BlockAck (type 25). @@ -19,21 +28,63 @@ Define_ProtocolTest(wifi_11n_block_ack) .describe("ADDBA action frame from sta1").within(2.5)) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) .describe("BlockAckReq from sta1").within(1.0)) .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (int i = 0; i < 64; i++) + for (unsigned int j = 0; j < ba->getBlockAckBitmap(i).getSize(); j++) + if (ba->getBlockAckBitmap(i).getBit(j)) return true; + return false; + }) .describe("BlockAck received at sta1").within(0.02)); } +Define_ProtocolTest(wifi_11n_compressed_exchange) +{ + return ProtocolTest("wifi_11n_compressed_exchange") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 13") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return response && response->getStatusCode() == 0 && response->getBlockAckPolicy(); + }) + .describe("successful immediate ADDBA response").within(2.5)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) + .describe("Compressed BAR from sta1").within(1.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (unsigned int i = 0; i < ba->getBlockAckBitmap().getSize(); i++) + if (ba->getBlockAckBitmap().getBit(i)) return true; + return false; + }) + .describe("Compressed Block Ack with positive receipt evidence").within(0.02)); +} + } // namespace protocoltest } // namespace inet %inifile: test.ini [General] -*.tester.testName = "wifi_11n_block_ack" +*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange","wifi_11n_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed",default ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -49,5 +100,22 @@ include ../../ini/_base.ini **.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 *.sta*.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +%file: check-default.py +from pathlib import Path +import re + +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", Path("test.out").read_text())[1:] +assert len(runs) == 3, len(runs) +for run, profile in zip(runs, ["basic", "compressed", "default"]): + assert f'$profile={profile if profile == "default" else chr(34) + profile + chr(34)}' in run + resolved = "basic" if profile == "basic" else "compressed" + assert run.count(f"Block Ack profile: {resolved}") == 3 + test = "wifi_11n_block_ack" if profile == "basic" else "wifi_11n_compressed_exchange" + assert f"PROTOCOLTEST {test}: PASS" in run +print("Basic, explicit Compressed, and NED default exchanges passed.") + %contains: stdout PROTOCOLTEST wifi_11n_block_ack: PASS + +%contains: stdout +PROTOCOLTEST wifi_11n_compressed_exchange: PASS diff --git a/tests/protocol/wifi/11n/N_CompressedBa.test b/tests/protocol/wifi/11n/N_CompressedBa.test index 8d7910bb180..ffc72bf4215 100644 --- a/tests/protocol/wifi/11n/N_CompressedBa.test +++ b/tests/protocol/wifi/11n/N_CompressedBa.test @@ -1,5 +1,5 @@ %description: -802.11n Block Ack received at sta1 with non-empty bitmap (acknowledges at least one frame). [Required] +802.11n Compressed Block Ack received at sta1 with a positive receipt bit. [Required] %file: N_CompressedBa.cc @@ -11,10 +11,6 @@ namespace protocoltest { using namespace inet::ieee80211; -// INET sends Ieee80211BasicBlockAck (not CompressedBlockAck) for all block ack sessions. -// The Basic Block Ack carries a 64x16-bit bitmap acknowledging MPDUs. -// We assert that a BasicBlockAck is received and its bitmap has at least one bit set, -// confirming the AP has positively acknowledged at least one received data MPDU. Define_ProtocolTest(wifi_11n_compressed_ba) { return ProtocolTest("wifi_11n_compressed_ba") @@ -22,18 +18,14 @@ Define_ProtocolTest(wifi_11n_compressed_ba) .filterPacket("ieee80211mac.type == 25") .filterEvent([](const MatchContext& c) { if (!c.event.packet) return false; - auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); if (!ba) return false; - // At least one bit set means at least one MPDU was acknowledged. - for (int i = 0; i < 64; i++) { - const auto& bv = ba->getBlockAckBitmap(i); - for (unsigned int j = 0; j < bv.getSize(); j++) { - if (bv.getBit(j)) return true; - } - } + const auto& bitmap = ba->getBlockAckBitmap(); + for (unsigned int i = 0; i < bitmap.getSize(); i++) + if (bitmap.getBit(i)) return true; return false; }) - .describe("a BasicBlockAck with at least one acknowledged MPDU").within(2.5)); + .describe("a Compressed Block Ack with a positive receipt bit").within(2.5)); } } // namespace protocoltest @@ -44,7 +36,7 @@ Define_ProtocolTest(wifi_11n_compressed_ba) [General] *.tester.testName = "wifi_11n_compressed_ba" *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = "compressed" *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms diff --git a/tests/serializer/REMAINING_GAPS.md b/tests/serializer/REMAINING_GAPS.md index c112225ab8d..78180de2e35 100644 --- a/tests/serializer/REMAINING_GAPS.md +++ b/tests/serializer/REMAINING_GAPS.md @@ -432,6 +432,10 @@ anyway. ## Untested serializers (registered, but no real-capture coverage) +The Block Ack producers retain Basic BAR and Block Ack with Type 0. Their `reserved` fields clear the two bits that overlap Type. The Compressed producers use Type 2 and leave ACK-policy and `reserved` fields at zero. These Compressed fields have no field-preserving wire contract. The serializer writes zero for reserved bits B0 and B5–B11 and ignores their received values. + +`Ieee80211BlockAckWire_1.test` checks independent octets for both variants and both frame types. It also checks unsupported Types, malformed Compressed fragment subfields, and response lengths. Basic response headers contain 148 bytes; Compressed response headers contain 28 bytes. The four-byte FCS gives MPDU lengths of 152 and 32 bytes. These checks do not establish a complete frame exchange or complete historical Basic conformance. + The tests cover different things. `serializer_chunk_roundtrip` **constructs** an instance of every registered chunk type — from hand-written typed fillers in `tests/serializer/lib/fillers/`, one or more per type, a variant per branch the serializer takes diff --git a/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc b/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc index 158b12c5620..163bfaa38c3 100644 --- a/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc +++ b/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc @@ -432,13 +432,8 @@ void addFillers_ieee80211(std::vector& fillers) setChunkLength(c, LENGTH_DELBA); }}); - // --- Block Ack request / response, Basic and Compressed variants: multiTid and - // compressedBitmap are the serializer's variant discriminator (see - // Ieee80211MacHeaderSerializer's ST_BLOCKACK_REQ/ST_BLOCKACK cases), pinned per - // subclass by the .msg -- kept at that pinned pair. fragmentNumber is the Fragment - // Number subfield of the Block Ack (Req) Starting Sequence Control, which 802.11 - // always sets to 0 in a Block Ack context (no fragmentation) -- kept at the .msg - // default in all four subtypes. + // The concrete classes fix the Type bits. Basic reserved values must retain Type 0. + // Compressed reserved fields write zero. All producers retain fragmentNumber = 0. fillers.push_back({"inet::ieee80211::Ieee80211BasicBlockAckReq", "", [](Chunk *c) { auto h = check_and_cast(c); FillValues v; @@ -451,7 +446,7 @@ void addFillers_ieee80211(std::vector& fillers) h->setMultiTid(false); // pinned by the class: selects the (non-multi-TID) Basic/Compressed branch h->setCompressedBitmap(false); // pinned by the class: selects the Basic (non-compressed) branch h->setBarAckPolicy(true); - h->setReserved(v.uint(9)); + h->setReserved(v.uint(9) & ~3u); h->setTidInfo((int)v.uint(4)); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); }}); @@ -466,8 +461,8 @@ void addFillers_ieee80211(std::vector& fillers) h->setType(ST_BLOCKACK_REQ); h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(true); // pinned by the class: selects the Compressed branch - h->setBarAckPolicy(true); - h->setReserved(v.uint(9)); + h->setBarAckPolicy(false); + h->setReserved(0); h->setTidInfo((int)v.uint(4)); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); }}); @@ -483,7 +478,7 @@ void addFillers_ieee80211(std::vector& fillers) h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(false); // pinned by the class: selects the Basic bitmap branch h->setBlockAckPolicy(true); - h->setReserved(v.uint(9)); + h->setReserved(v.uint(9) & ~3u); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); // The Block Ack bitmap: 64 elements of 2 bytes (16 bits) each; never leave any at // the default empty BitVector -- getBytes()[0]/[1] would read out of bounds. @@ -502,15 +497,11 @@ void addFillers_ieee80211(std::vector& fillers) h->setType(ST_BLOCKACK); h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(true); // pinned by the class: selects the Compressed bitmap branch - h->setBlockAckPolicy(true); - h->setReserved(v.uint(9)); + h->setBlockAckPolicy(false); + h->setReserved(0); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); h->setBlockAckBitmap(BitVector(std::vector{v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8()})); h->setTidInfo((int)v.uint(4)); - // the .msg leaves chunkLength unset ("TODO"); the wire layout is fixed at - // 16 (header) + 2 (BA control) + 2 (starting-sequence control) + 8 (compressed - // bitmap) = 28 bytes. - setChunkLength(c, B(28)); }}); // --- management frame bodies. Each is content-length-dependent (SSID, rates, the diff --git a/tests/serializer/serializer_chunk_roundtrip.test b/tests/serializer/serializer_chunk_roundtrip.test index 18fa3f2e235..3ab712fe87c 100644 --- a/tests/serializer/serializer_chunk_roundtrip.test +++ b/tests/serializer/serializer_chunk_roundtrip.test @@ -420,11 +420,10 @@ inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgBackoffTime inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgErrorRate inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgWaitTime inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.estAvailBW -// -- 802.11 Block Ack: multiTid/compressedBitmap are the pair of bits that select -// the concrete Basic/Compressed/MultiTid variant, so each concrete class pins them; -// the Starting Sequence Control fragment-number subfield is always 0 (802.11 does -// not fragment the frames a Block Ack covers), and ADDBA's blockAckAction is the -// action code its own class fixes +// The concrete Block Ack classes fix their Type bits. These producers use fragmentNumber = 0. +// Compressed ACK-policy and reserved fields have no field-preserving wire contract. +// The serializer writes zero for these fields and ignores received reserved bits. +// ADDBA fixes its action code through its concrete class. inet::ieee80211::Ieee80211AddbaRequest::_fragmentNumber inet::ieee80211::Ieee80211AddbaRequest::blockAckAction inet::ieee80211::Ieee80211BasicBlockAck::compressedBitmap @@ -434,11 +433,15 @@ inet::ieee80211::Ieee80211BasicBlockAckReq::compressedBitmap inet::ieee80211::Ieee80211BasicBlockAckReq::fragmentNumber inet::ieee80211::Ieee80211BasicBlockAckReq::multiTid inet::ieee80211::Ieee80211CompressedBlockAck::compressedBitmap +inet::ieee80211::Ieee80211CompressedBlockAck::blockAckPolicy inet::ieee80211::Ieee80211CompressedBlockAck::fragmentNumber inet::ieee80211::Ieee80211CompressedBlockAck::multiTid +inet::ieee80211::Ieee80211CompressedBlockAck::reserved +inet::ieee80211::Ieee80211CompressedBlockAckReq::barAckPolicy inet::ieee80211::Ieee80211CompressedBlockAckReq::compressedBitmap inet::ieee80211::Ieee80211CompressedBlockAckReq::fragmentNumber inet::ieee80211::Ieee80211CompressedBlockAckReq::multiTid +inet::ieee80211::Ieee80211CompressedBlockAckReq::reserved // -- MRP TLV length bytes: the serializer derives them from what it writes, so // the model field is not the value on the wire inet::MrpEnd::valueLength diff --git a/tests/unit/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 diff --git a/tests/unit/Ieee80211AmpduConstruction_1.test b/tests/unit/Ieee80211AmpduConstruction_1.test new file mode 100644 index 00000000000..fb8bccc4dbf --- /dev/null +++ b/tests/unit/Ieee80211AmpduConstruction_1.test @@ -0,0 +1,188 @@ +%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/MpduAggregationUtils.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)); + auto layout = inspectMpduAggregate(&independent, B(0), expectedLength); + ASSERT(layout.complete && layout.ranges.size() == size_t(4 + extraDelimiters)); + ASSERT(layout.ranges.back().offset == B(distance + 4)); + ASSERT(layout.ranges.back().length == B(33)); + 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(inspectMpduAggregate(&fresh, B(0), B(35)).complete); +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(inspectMpduAggregate(legacy, B(0), B(71)).complete); +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 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/Ieee80211AmpduWire_1.test b/tests/unit/Ieee80211AmpduWire_1.test new file mode 100644 index 00000000000..3a7d3f3266f --- /dev/null +++ b/tests/unit/Ieee80211AmpduWire_1.test @@ -0,0 +1,191 @@ +%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. +Bounded delimiter inspection covers lengths, alignment, truncation, and recovery. +Recovery preserves matching ranges across typed and byte representations, including a nonzero PSDU offset. + +%includes: +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregationUtils.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 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)); +} + +static void appendMember(Packet& packet, int length) +{ + auto delimiter = makeShared(); + delimiter->setLength(length); + packet.insertAtBack(delimiter); + if (length) + packet.insertAtBack(makeShared(std::vector(length, 0))); +} + +%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}); +for (int length : {0, 1, 2, 3, 4, 4095}) { + Packet packet("layout"); + appendMember(packet, 0); + appendMember(packet, length); + if (length) { + int padding = (4 - length % 4) % 4; + if (padding) + packet.insertAtBack(makeShared(std::vector(padding, 0))); + appendMember(packet, 20); + } + auto layout = inspectMpduAggregate(&packet, B(0), B(packet.getDataLength())); + ASSERT(layout.complete); + ASSERT(layout.ranges.size() == (length ? 5 : 2)); + ASSERT(layout.ranges.front().kind == MpduRangeKind::DELIMITER); + for (const auto& range : layout.ranges) + ASSERT(range.offset + range.length <= packet.getDataLength()); + auto bytes = packet.peekAllAsBytes()->getBytes(); + Packet independent("wire", makeShared(bytes)); + ASSERT(inspectMpduAggregate(&independent, B(0), B(bytes.size())).complete); + if (length) { + bytes.pop_back(); + Packet truncated("truncated", makeShared(bytes)); + ASSERT(!inspectMpduAggregate(&truncated, B(0), B(bytes.size())).complete); + } +} +Packet recovery("recovery", makeShared(std::vector{0, 0, 0, 0})); +appendMember(recovery, 20); +auto recovered = inspectMpduAggregate(&recovery, B(0), B(recovery.getDataLength())); +ASSERT(!recovered.complete && recovered.ranges.size() == 3); +ASSERT(!recovered.ranges[0].valid && recovered.ranges[1].offset == B(4)); +// An invalid length forces delimiter recovery through a typed MAC header. +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 typedLayout = inspectMpduAggregate(&typed, B(prefixLength), B(128)); + auto byteLayout = inspectMpduAggregate(&independent, B(prefixLength), B(128)); + ASSERT(!typedLayout.complete && !byteLayout.complete); + ASSERT(typedLayout.ranges.size() == byteLayout.ranges.size()); + for (size_t i = 0; i < typedLayout.ranges.size(); i++) { + const auto& actual = typedLayout.ranges[i]; + const auto& expected = byteLayout.ranges[i]; + ASSERT(actual.kind == expected.kind && actual.offset == expected.offset && + actual.length == expected.length && actual.valid == expected.valid); + } + const auto& last = typedLayout.ranges.back(); + ASSERT(last.kind == MpduRangeKind::MPDU && last.valid && last.offset == B(108) && last.length == B(20)); +} +// Reinterpretation must not hide an existing typed delimiter's error flag. +auto incorrectDelimiter = makeShared(); +incorrectDelimiter->setLength(0); +incorrectDelimiter->markIncorrect(); +Packet incorrect("incorrect-delimiter", incorrectDelimiter); +appendMember(incorrect, 20); +auto incorrectLayout = inspectMpduAggregate(&incorrect, B(0), B(incorrect.getDataLength())); +ASSERT(!incorrectLayout.complete && !incorrectLayout.ranges.front().valid); +ASSERT(incorrectLayout.ranges.back().kind == MpduRangeKind::MPDU && incorrectLayout.ranges.back().valid); +for (int byte : {2, 3}) { + Packet good("good"); + appendMember(good, 20); + auto bytes = good.peekAllAsBytes()->getBytes(); + bytes[byte] ^= 1; + Packet bad("bad", makeShared(bytes)); + auto layout = inspectMpduAggregate(&bad, B(0), B(bytes.size())); + ASSERT(!layout.complete && !layout.ranges.front().valid); +} +Packet finalPadding("final-padding"); +appendMember(finalPadding, 1); +finalPadding.insertAtBack(makeShared(std::vector(3, 0))); +ASSERT(!inspectMpduAggregate(&finalPadding, B(0), B(finalPadding.getDataLength())).complete); +auto overrun = makeShared(); +overrun->setLength(4095); +Packet missingMember("missing-member", overrun); +auto refusedLayout = inspectMpduAggregate(&missingMember, B(0), B(4)); +ASSERT(!refusedLayout.complete && refusedLayout.ranges.size() == 1 && !refusedLayout.ranges[0].valid); +for (int size : {0, 1, 2, 3}) { + Packet shortInput("short", makeShared(std::vector(size, 0))); + auto layout = inspectMpduAggregate(&shortInput, B(0), B(size)); + ASSERT(layout.ranges.size() == (size == 0 ? 0 : 1)); + ASSERT(layout.complete == (size == 0)); + if (size != 0) { + ASSERT(layout.ranges[0].kind == MpduRangeKind::DELIMITER); + ASSERT(!layout.ranges[0].valid && layout.ranges[0].length == B(size)); + } +} +auto tooLong = makeShared(); +tooLong->setLength(4096); +Packet overflow("overflow", tooLong); +bool refused = false; +try { overflow.peekAllAsBytes(); } +catch (const cRuntimeError&) { refused = true; } +ASSERT(refused); +auto invalidLength = makeShared(); +invalidLength->setLengthField(B(65536)); +Packet excessivePsdu("excessive-psdu", invalidLength); +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 diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test new file mode 100644 index 00000000000..f82be88e6cd --- /dev/null +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -0,0 +1,247 @@ +%description: +Check peer/TID BAR arbitration, window boundaries, cyclic sequence order, and ACK-state isolation. +The production policy reads the production ACK handler through an in-progress queue. +These helper checks supplement the HCF module fixture. + +%includes: +#include +#include +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +using namespace inet; +using namespace inet::ieee80211; + +namespace Ieee80211BlockAckScheduling_1 { +template struct ImplicitBarQuery : std::false_type {}; +template struct ImplicitBarQuery().isBlockAckReqNeeded(nullptr, nullptr))>> : std::true_type {}; +template struct ImplicitBarParameters : std::false_type {}; +template struct ImplicitBarParameters().computeBlockAckReqParameters(nullptr, nullptr))>> : std::true_type {}; +template struct ImplicitBarWindow : std::false_type {}; +template struct ImplicitBarWindow().processTransmittedBlockAckReq(std::declval>()))>> : std::true_type {}; + +static_assert(!ImplicitBarQuery::value, "The interface requires an explicit agreement handler"); +static_assert(!ImplicitBarQuery::value, "The concrete policy requires an explicit agreement handler"); +static_assert(!ImplicitBarParameters::value, "The interface requires an explicit agreement handler"); +static_assert(!ImplicitBarParameters::value, "The concrete policy requires an explicit agreement handler"); +static_assert(!ImplicitBarWindow::value, "The ACK handler requires an explicit agreement window"); + +class SchedulingCw : public IRecoveryProcedure::ICwCalculator { + public: + int cw = 0; + void incrementCw() override { cw = std::min(7, 2 * cw + 1); } + void resetCw() override { cw = 0; } + int getCw() override { return cw; } +}; +class SchedulingRecovery : public QosRecoveryProcedure { + public: + void configure(ICwCalculator *calculator) { cwCalculator = calculator; shortRetryLimit = 1; } + int getStationSrc() const { return stationShortRetryCounter; } + bool hasDataRetry() const { return !shortRetryCounter.empty() || !longRetryCounter.empty(); } +}; +class SchedulingPolicy : public OriginatorQosAckPolicy { + public: + SchedulingPolicy() { blockAckReqThreshold = 5; } +}; +class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { + public: + OriginatorBlockAckAgreement *accept(MacAddress peer, Tid tid, int start, int size) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), size, false, false); + agreement->setIsAddbaResponseReceived(true); + blockAckAgreements[{peer, tid}] = agreement; + return agreement; + } +}; +class SchedulingFrames : public InProgressFrames { + public: + Packet *candidate = nullptr; + void configure(QosAckHandler *handler) { ackHandler = handler; } + Packet *peekFrameToTransmit() override { return candidate; } + Ptr add(MacAddress peer, Tid tid, int sequence, bool sent = true) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setAckPolicy(BLOCK_ACK); + auto packet = new Packet("data", header); + take(packet); + packet->setArrivalTime(simTime()); + inProgressFrames.push_back(packet); + ackHandler->frameGotInProgress(header); + if (sent) + check_and_cast(ackHandler)->processTransmittedDataOrMgmtFrame(header); + return header; + } +}; +} + +%activity: +SchedulingCw cw; +SchedulingRecovery recovery; +recovery.configure(&cw); +recovery.controlFrameTransmissionFailed(); +ASSERT(recovery.getStationSrc() == 1 && cw.getCw() == 1); +recovery.controlFrameTransmissionFailed(); +ASSERT(recovery.getStationSrc() == 0 && cw.getCw() == 0); +recovery.controlFrameTransmissionFailed(); +recovery.blockAckFrameReceived(); +ASSERT(recovery.getStationSrc() == 0 && cw.getCw() == 0 && !recovery.hasDataRetry()); +const MacAddress peer("02:00:00:00:00:02"); +const MacAddress other("02:00:00:00:00:03"); +OriginatorBlockAckAgreement window(peer, 5, SequenceNumberCyclic(4094), 4, false, false); +window.advanceWindow(SequenceNumberCyclic(2), {SequenceNumberCyclic(0), SequenceNumberCyclic(4095)}); +ASSERT(window.getStartingSequenceNumber() == SequenceNumberCyclic(4095)); +window.advanceWindow(SequenceNumberCyclic(2), {SequenceNumberCyclic(0)}); +ASSERT(window.getStartingSequenceNumber() == SequenceNumberCyclic(0)); +window.advanceWindow(SequenceNumberCyclic(2), {}); +ASSERT(window.getStartingSequenceNumber() == SequenceNumberCyclic(2)); +ASSERT(OriginatorBlockAckAgreement::isInBitmap(SequenceNumberCyclic(62), SequenceNumberCyclic(4095))); +ASSERT(!OriginatorBlockAckAgreement::isInBitmap(SequenceNumberCyclic(63), SequenceNumberCyclic(4095))); +ASSERT(!OriginatorBlockAckAgreement::isInBitmap(SequenceNumberCyclic(4094), SequenceNumberCyclic(4095))); +ASSERT(!window.containsSequence(SequenceNumberCyclic(62))); +SchedulingPolicy policy; +SchedulingAgreements agreements; +QosAckHandler ack; +SchedulingFrames frames; +frames.configure(&ack); +agreements.accept(peer, 2, 100, 8); +auto selectedAgreement = agreements.accept(peer, 5, 4094, 4); +auto otherAgreement = agreements.accept(other, 5, 4094, 4); +std::vector> unrelated; +for (int i = 100; i < 104; i++) + unrelated.push_back(frames.add(peer, 2, i)); +auto selected = frames.add(peer, 5, 4095); +auto wrap = frames.add(peer, 5, 0); +auto outside = frames.add(peer, 5, 2); +auto separatePeer = frames.add(other, 5, 4095); +// A candidate inside TID 2 defers only that group. +frames.add(peer, 2, 104, false); +frames.candidate = frames.getFrames(frames.getLength() - 1); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, nullptr)); +ASSERT(std::get<2>(policy.computeBlockAckReqParameters(&frames, nullptr, nullptr)) == -1); +auto choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 5); +ASSERT(std::get<1>(choice) == SequenceNumberCyclic(4095)); +ASSERT(selectedAgreement->containsSequence(SequenceNumberCyclic(1))); +ASSERT(!selectedAgreement->containsSequence(SequenceNumberCyclic(2))); +auto bar = makeShared(); +bar->setReceiverAddress(peer); +bar->setTidInfo(5); +bar->setStartingSequenceNumber(SequenceNumberCyclic(4095)); +ack.processTransmittedBlockAckReq(bar, selectedAgreement); +ASSERT(ack.getQoSDataAckStatus(selected) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +ASSERT(ack.getQoSDataAckStatus(wrap) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +for (auto header : unrelated) + ASSERT(ack.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ASSERT(ack.getQoSDataAckStatus(outside) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ASSERT(ack.getQoSDataAckStatus(separatePeer) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ack.processFailedBlockAckReq(bar); +ASSERT(ack.isEligibleToTransmit(selected)); +ASSERT(ack.isEligibleToTransmit(wrap)); +ASSERT(ack.getQoSDataAckStatus(separatePeer) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +frames.candidate = nullptr; +choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 2); +ASSERT(std::get<1>(choice) == SequenceNumberCyclic(100)); +ASSERT(otherAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(4094)); +for (int start : {100, 4094}) { + for (int responseOffset : {0, 6, 100}) { + QosAckHandler compressedAck; + SchedulingFrames compressedFrames; + compressedFrames.configure(&compressedAck); + OriginatorBlockAckAgreement active(peer, 5, SequenceNumberCyclic(start), 64, false, false); + auto earlier = compressedFrames.add(peer, 5, (start + 2) % 4096); + auto requested = compressedFrames.add(peer, 5, (start + 4) % 4096); + auto unsent = compressedFrames.add(peer, 5, (start + 5) % 4096, false); + auto outsideWindow = compressedFrames.add(peer, 5, (start + 64) % 4096); + auto otherTid = compressedFrames.add(peer, 2, (start + 2) % 4096); + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + compressedAck.processTransmittedBlockAckReq(request, &active); + ASSERT(compressedAck.getQoSDataAckStatus(earlier) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(compressedAck.getQoSDataAckStatus(requested) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setTidInfo(5); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + responseOffset); + BitVector bitmap; + for (int bit = 0; bit < 64; bit++) + bitmap.appendBit(responseOffset == 0 && (bit == 2 || bit == 5)); + response->setBlockAckBitmap(bitmap); + compressedAck.processReceivedCompressedBlockAck(response, &active); + ASSERT(compressedAck.getQoSDataAckStatus(earlier) == (responseOffset == 0 ? + QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + ASSERT(compressedAck.isEligibleToTransmit(requested)); + ASSERT(compressedAck.getQoSDataAckStatus(unsent) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(compressedAck.getQoSDataAckStatus(otherTid) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(compressedAck.getQoSDataAckStatus(outsideWindow) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + BitVector outsideBitmap; + for (int bit = 0; bit < 64; bit++) + outsideBitmap.appendBit(bit == 60); + response->setBlockAckBitmap(outsideBitmap); + compressedAck.processReceivedCompressedBlockAck(response, &active); + ASSERT(compressedAck.getQoSDataAckStatus(outsideWindow) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + } +} +auto duration = SimTime(1, SIMTIME_US); +EdcaTransmitLifetimeHandler lifetime(duration, duration, duration, duration); +frames.setLifetimeHandler(&lifetime); +for (int i = 0; i < frames.getLength(); i++) + lifetime.registerFrame(frames.getFrames(i), AC_VI); +ASSERT(policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +wait(duration); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +ASSERT(ack.getQoSDataAckStatus(unrelated.front()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +// 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 +Block Ack scheduling isolation assertions passed. diff --git a/tests/unit/Ieee80211BlockAckWire_1.test b/tests/unit/Ieee80211BlockAckWire_1.test new file mode 100644 index 00000000000..41c8138da32 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckWire_1.test @@ -0,0 +1,230 @@ +%description: +Check Basic Type 0 and Compressed Type 2 BAR and Block Ack against independent octets. +Check reserved bits, unsupported Types, malformed fragment subfields, and fixed lengths. +The wire oracle follows the C1 contract in the Compressed Block Ack replacement plan. +Basic fields preserve the retained Type 0 compatibility contract. +Compressed uses the non-HE, 64-position format from IEEE Std 802.11-2024, 9.3.1.7.2 and 9.3.1.8.2. +This test does not establish complete exchanges or response airtime. + +%includes: +#include "inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +class BlockAckWireSerializer : public Ieee80211MacHeaderSerializer +{ + public: + using Ieee80211MacHeaderSerializer::serializeFields; + using Ieee80211MacHeaderSerializer::deserializeFields; +}; + +static std::vector encode(const Ptr& header) +{ + BlockAckWireSerializer serializer; + MemoryOutputStream stream; + serializer.serializeFields(stream, header); + return stream.getData(); +} + +static Ptr decode(const std::vector& bytes) +{ + MemoryInputStream stream(bytes); + return Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); +} + +static void requireWriteFailure(const Ptr& header) +{ + bool rejected = false; + try { encode(header); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); +} + +static void addresses(const Ptr& header) +{ + header->setReceiverAddress(MacAddress("02:11:22:33:44:55")); + header->setTransmitterAddress(MacAddress("06:aa:bb:cc:dd:ee")); + header->setDurationField(SimTime(0x1234, SIMTIME_US)); +} + +// Each vector is independent of the serializer output. +static std::vector golden(bool response, bool compressed) +{ + std::vector bytes = { + uint8_t(response ? 0x94 : 0x84), 0x00, 0x34, 0x12, + 0x02, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x06, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + uint8_t(compressed ? 0x04 : 0x21), 0x50, 0xe0, 0xff + }; + if (response) { + bytes.resize(compressed ? 28 : 148, 0); + // SSN 4094: positions 0, 1, 2, and 63 represent 4094, 4095, 0, and 61. + if (compressed) { + bytes[20] = 0x07; + bytes[27] = 0x80; + } + else { + // Basic position 0 reports fragments 0 and 15; position 63 reports fragment 8. + bytes[20] = 0x01; + bytes[21] = 0x80; + bytes[147] = 0x01; + } + } + return bytes; +} + +%activity: +OriginatorBlockAckProcedure procedure; +auto built = dynamicPtrCast( + procedure.buildCompressedBlockAckReqFrame(MacAddress("02:11:22:33:44:55"), 5, SequenceNumberCyclic(4094))); +ASSERT(built && built->getReceiverAddress() == MacAddress("02:11:22:33:44:55")); +ASSERT(built->getTidInfo() == 5 && built->getStartingSequenceNumber().get() == 4094); +ASSERT(built->getFragmentNumber() == 0 && built->getChunkLength() == B(20)); +addresses(built); +ASSERT(encode(built) == golden(false, true)); + +for (bool response : {false, true}) { + for (bool compressed : {false, true}) { + Ptr header; + if (response) { + if (compressed) { + auto ba = makeShared(); + ba->setBlockAckPolicy(true); + ba->setReserved(0x1ff); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + ba->setBlockAckBitmap(BitVector(std::vector{7, 0, 0, 0, 0, 0, 0, 0x80})); + ASSERT(ba->getChunkLength() == B(28)); + header = ba; + } + else { + auto ba = makeShared(); + ba->setBlockAckPolicy(true); + ba->setReserved(4); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + for (int i = 0; i < 64; i++) + ba->setBlockAckBitmap(i, BitVector(std::vector{uint8_t(i == 0), uint8_t(i == 0 ? 0x80 : i == 63 ? 1 : 0)})); + ASSERT(ba->getChunkLength() == B(148)); + header = ba; + } + } + else { + if (compressed) { + auto bar = makeShared(); + bar->setBarAckPolicy(true); + bar->setReserved(0x1ff); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + header = bar; + } + else { + auto bar = makeShared(); + bar->setBarAckPolicy(true); + bar->setReserved(4); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + header = bar; + } + } + addresses(header); + auto expected = golden(response, compressed); + ASSERT(encode(header) == expected); + ASSERT(header->getChunkLength() == B(expected.size())); + if (response) { + auto fcs = makeShared(); + ASSERT(header->getChunkLength() + fcs->getChunkLength() == B(compressed ? 32 : 152)); + } + auto result = decode(expected); + ASSERT(result->isCorrect()); + ASSERT(result->isComplete()); + ASSERT(encode(result) == expected); + if (response) { + if (compressed) { + auto ba = dynamicPtrCast(result); + ASSERT(ba && ba->getTidInfo() == 5 && ba->getStartingSequenceNumber().get() == 4094); + ASSERT(ba->getBlockAckBitmap().getSize() == 64); + ASSERT(ba->getBlockAckBitmap().getBit(0) && ba->getBlockAckBitmap().getBit(2)); + ASSERT(ba->getBlockAckBitmap().getBit(63) && !ba->getBlockAckBitmap().getBit(62)); + } + else { + auto ba = dynamicPtrCast(result); + ASSERT(ba && ba->getReserved() == 4 && ba->getBlockAckPolicy()); + ASSERT(ba->getBlockAckBitmap(0).getBit(15)); + ASSERT(ba->getBlockAckBitmap(63).getBit(8)); + } + } + else { + if (compressed) { + auto bar = dynamicPtrCast(result); + ASSERT(bar && bar->getTidInfo() == 5 && bar->getStartingSequenceNumber().get() == 4094); + } + else { + auto bar = dynamicPtrCast(result); + ASSERT(bar && bar->getReserved() == 4 && bar->getBarAckPolicy()); + } + } + if (compressed) { + auto reserved = expected; + reserved[16] |= 0xe1; + reserved[17] |= 0x0f; + auto ignored = decode(reserved); + ASSERT(ignored->isCorrect()); + ASSERT(encode(ignored) == expected); + if (response) { + auto ba = dynamicPtrCast(ignored); + ASSERT(ba && !ba->getBlockAckPolicy() && ba->getReserved() == 0); + } + else { + auto bar = dynamicPtrCast(ignored); + ASSERT(bar && !bar->getBarAckPolicy() && bar->getReserved() == 0); + } + auto malformed = expected; + malformed[18] |= 1; + ASSERT(!decode(malformed)->isCorrect()); + if (response) + dynamicPtrCast(header)->setFragmentNumber(1); + else + dynamicPtrCast(header)->setFragmentNumber(1); + requireWriteFailure(header); + } + else { + for (int reserved : {1, 2, 3}) { + if (response) + dynamicPtrCast(header)->setReserved(reserved); + else + dynamicPtrCast(header)->setReserved(reserved); + requireWriteFailure(header); + } + } + for (size_t length : {size_t(17), expected.size() - 1}) { + auto shortFrame = expected; + shortFrame.resize(length); + ASSERT(!decode(shortFrame)->isComplete()); + } + } + for (int type = 0; type < 16; type++) { + if (type == 0 || type == 2) + continue; + auto unsupported = golden(response, true); + unsupported[16] = type << 1; + auto result = decode(unsupported); + ASSERT(!result->isCorrect()); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + } +} +for (int bits : {0, 63, 65}) { + auto ba = makeShared(); + ba->setBlockAckBitmap(bits == 0 ? BitVector() : BitVector(0u, bits)); + requireWriteFailure(ba); +} +EV << "Block Ack wire checks passed\n"; + +%contains: stdout +Block Ack wire checks passed diff --git a/tests/unit/Ieee80211CompressedBlockAckRate_1.test b/tests/unit/Ieee80211CompressedBlockAckRate_1.test new file mode 100644 index 00000000000..1aa02ea3a16 --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAckRate_1.test @@ -0,0 +1,169 @@ +%description: +Check Compressed response rates through the production QoS rate selector. +Use independent MIB rate sets for two peers and both response directions. +Retain the Basic response contract and reject an invalid Compressed override. + +%includes: +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211CompressedBlockAckRate_1 { +class CompressedRateSelection : public QosRateSelection { + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LOCAL) + mib.reference(this, "mibModule", true); + } + public: + void configure(const Ieee80211ModeSet *catalog, const IIeee80211Mode *response) { + modeSet = catalog; + responseBlockAckFrameMode = response; + } + void controlOverride(const IIeee80211Mode *mode) { controlFrameMode = mode; } +}; +Define_Module(CompressedRateSelection); + +static Ieee80211RateSetState rates(std::initializer_list basic) { + Ieee80211RateSetState state; + state.supported.known = state.operational.known = state.basic.known = true; + for (int rate : {6, 9, 12, 18, 24, 36, 48, 54}) + state.supported.legacyRates.insert(Mbps(rate)); + state.operational = state.supported; + for (int rate : basic) + state.basic.legacyRates.insert(Mbps(rate)); + return state; +} +} + +%file: TestNetwork.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mib.Ieee80211Mib; +simple CompressedRateSelection extends SimpleModule { + parameters: + @class(CompressedRateSelection); + string mibModule; +} +network TestNetwork { + submodules: + test: Test; + mib: Ieee80211Mib; + selector: CompressedRateSelection { parameters: mibModule = "^.mib"; } +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +record-vector-results = false +record-scalar-results = false + +%activity: +using namespace Ieee80211CompressedBlockAckRate_1; +auto mib = check_and_cast(getModuleByPath("mib")); +auto selector = check_and_cast(getModuleByPath("selector")); +const MacAddress a("02:00:00:00:00:01"), b("02:00:00:00:00:02"); +for (const char *name : {"n(mixed-2.4Ghz)", "ac"}) { + auto catalog = Ieee80211ModeSet::getModeSet(name); + auto mode = [&](int rate) { return catalog->getMode(Mbps(rate)); }; + auto inputCatalog = Ieee80211ModeSet::getModeSet(name[0] == 'a' ? "a" : "g(mixed)"); + auto fastInput = inputCatalog->getMode(Mbps(54)); + mib->setLocalRateSet(rates({6, 12, 24})); + mib->setBssRateSet(rates({6, 12, 24})); + selector->configure(catalog, nullptr); + auto bar = makeShared(); + bar->setReceiverAddress(b); + bar->setTransmitterAddress(a); + auto check = [&](const IIeee80211Mode *input, int expected) { + Packet predict("predict"), respond("respond"); + predict.addTag()->setMode(input); + respond.addTag()->setMode(input); + auto predicted = selector->computeResponseBlockAckFrameMode(&predict, bar, ResponseQueryRole::PREDICT); + auto response = selector->computeResponseBlockAckFrameMode(&respond, bar, ResponseQueryRole::RESPOND); + ASSERT(predicted == mode(expected)); + ASSERT(response == predicted); + ASSERT(response->getHtMcsIndex() < 0); + ASSERT(response->getDuration(B(32)) < response->getDuration(LENGTH_BASIC_BLOCKACK)); + }; + check(fastInput, 24); + check(mode(6), 6); + mib->setBssRateSet(rates({48})); + check(mode(24), 24); + mib->clearBssRateSet(); + check(fastInput, 24); + mib->setBssRateSet(rates({})); + check(fastInput, 24); + mib->setBssRateSet(rates({6, 12, 24})); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto ht = catalog->getMode(i); + auto family = ht->getModulationClass(); + if (family != IIeee80211Mode::ModulationClass::HT && family != IIeee80211Mode::ModulationClass::VHT) + continue; + auto reference = ht->getNonHtReferenceRate(); + int expected = reference >= Mbps(24) ? 24 : reference >= Mbps(12) ? 12 : 6; + check(ht, expected); + } + mib->setBssRateSet(rates({48})); + selector->configure(catalog, mode(6)); + bool refused = false; + try { check(fastInput, 6); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + selector->configure(catalog, nullptr); + auto basic = makeShared(); + Packet packet("basic"); + packet.addTag()->setMode(mode(24)); + ASSERT(selector->computeResponseBlockAckFrameMode(&packet, basic, ResponseQueryRole::RESPOND) == mode(24)); + auto bss = rates({6, 12, 24}); + auto peer = rates({6}); + peer.operational.legacyRates = {Mbps(6), Mbps(12)}; + mib->installBssAndPeerRateSets(bss, b, peer); + Packet originated("originated"); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, false) == mode(12)); + mib->removePeerRateSet(b); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, false) == mode(24)); + mib->setBssRateSet(rates({})); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + mib->clearBssRateSet(); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + selector->controlOverride(mode(6)); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(6)); + mib->setBssRateSet(rates({6})); + selector->controlOverride(mode(24)); + refused = false; + try { selector->computeCompressedBlockAckReqMode(&originated, bar, true); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto ht = catalog->getMode(i); + if (ht->getModulationClass() != IIeee80211Mode::ModulationClass::HT) + continue; + selector->controlOverride(ht); + refused = false; + try { selector->computeCompressedBlockAckReqMode(&originated, bar, true); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + break; + } + selector->controlOverride(nullptr); +} +// The full OFDM catalog supplies optional rates for the alternate-rate contract. +auto ofdm = Ieee80211ModeSet::getModeSet("a"); +auto primary = ofdm->getMode(Mbps(48)); +auto alternate = ofdm->getMode(Mbps(36)); +ASSERT(primary->getDuration(B(32)) == alternate->getDuration(B(32))); +mib->setBssRateSet(rates({36, 48})); +selector->configure(ofdm, alternate); +Packet alternatePacket("alternate"); +alternatePacket.addTag()->setMode(ofdm->getMode(Mbps(54))); +auto alternateBar = makeShared(); +ASSERT(selector->computeResponseBlockAckFrameMode(&alternatePacket, alternateBar, ResponseQueryRole::RESPOND) == alternate); +EV << "Compressed response rate checks passed.\n"; + +%contains: stdout +Compressed response rate checks passed. diff --git a/tests/unit/Ieee80211CompressedBlockAckRecord_1.test b/tests/unit/Ieee80211CompressedBlockAckRecord_1.test new file mode 100644 index 00000000000..58daa02e92a --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAckRecord_1.test @@ -0,0 +1,71 @@ +%description: +Check explicit Compressed receipt state independently of the delivery buffer. +Absent positions remain zero after delivery, window movement, and sequence wrap. +The preceding receipt remains available for the individual-MPDU release condition. +These checks do not establish HCF interframe recognition. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" + +using namespace inet; +using namespace inet::ieee80211; + +class ReceiptProcedure : public RecipientBlockAckProcedure { + public: + using RecipientBlockAckProcedure::buildBlockAck; +}; + +%activity: +const MacAddress peer("02:00:00:00:00:02"); +for (int start : {100, 4094}) { + RecipientBlockAckAgreement agreement(peer, 5, SequenceNumberCyclic(start), 4, SIMTIME_ZERO); + agreement.setVariant(BlockAckVariant::COMPRESSED); + auto record = agreement.getBlockAckRecord(); + auto bar = makeShared(); + bar->setTransmitterAddress(peer); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + ReceiptProcedure procedure; + auto response = dynamicPtrCast(procedure.buildBlockAck(bar, &agreement)); + ASSERT(response != nullptr && response->getChunkLength() == B(28)); + ASSERT(response->getBlockAckBitmap().getSize() == 64); + for (int bit = 0; bit < 64; bit++) + ASSERT(!response->getBlockAckBitmap().getBit(bit)); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setTransmitterAddress(peer); + data->setTid(5); + data->setAckPolicy(NORMAL_ACK); + data->setSequenceNumber(SequenceNumberCyclic(start)); + record->compressedFrameReceived(data); + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start))); + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start) - 1)); + // Delivery and Basic compatibility cleanup must not erase Compressed receipt truth. + record->removeAckStates(SequenceNumberCyclic(start) + 1); + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start))); + data->setSequenceNumber(SequenceNumberCyclic(start) + 4); + record->compressedFrameReceived(data); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(start) + 1); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(start))); + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start))); + response = dynamicPtrCast(procedure.buildBlockAck(bar, &agreement)); + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(start) + 1); + for (int bit = 0; bit < 64; bit++) + ASSERT(response->getBlockAckBitmap().getBit(bit) == (bit == 3)); + record->advanceCompressedWindow(SequenceNumberCyclic(start) + 5); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(start) + 4)); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(start))); + record->advanceCompressedWindow(SequenceNumberCyclic(start) + 8); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(start) + 4)); + for (int bit = 0; bit < 64; bit++) + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start) + 8 + bit)); + // An old BAR must preserve the current scoreboard start. + record->advanceCompressedWindow(SequenceNumberCyclic(start)); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(start) + 8); +} +EV << "Compressed receipt state checks passed.\n"; + +%contains: stdout +Compressed receipt state checks passed. diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..455332ef964 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -33,6 +33,18 @@ peer.maxAmpduLengthExponent = 3; Ieee80211HtOperation operation; operation.operatingChannelWidth = MHz(20); +for (bool permitted : {false, true}) { + auto advertised = operation; + advertised.primaryChannel = 0; + advertised.rifsMode = permitted; + auto band = physicallayer::Ieee80211CompliantBands::getBand("2.4 GHz"); + auto element = makeHtOperationElement(band, advertised); + ASSERT(element.rifsMode == permitted); + auto received = makeHtOperation(band, element); + ASSERT(received == advertised); + received.rifsMode = !permitted; + ASSERT(!(received == advertised)); +} operation.basicMcsSupported[0] = true; auto negotiated = negotiateHtCapabilities(local, peer, operation); diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 0dc059d2d40..552c8f54549 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -235,6 +235,14 @@ for (int mcs = 0; mcs < 32; mcs++) { ASSERT(typedHt->getSlotTime() == SimTime(20, SIMTIME_US)); ASSERT(typedAcHt->getSifsTime() == SimTime(16, SIMTIME_US)); ASSERT(typedAcHt->getSlotTime() == SimTime(9, SIMTIME_US)); + ASSERT(typedHt->getSignalExtensionTime() == SimTime(6, SIMTIME_US)); + ASSERT(typedAcHt->getSignalExtensionTime() == SIMTIME_ZERO); + ASSERT(typedHt->getDuration(B(100)) - typedAcHt->getDuration(B(100)) == SimTime(6, SIMTIME_US)); + if (mcs == 0 && width == MHz(20) && gi == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) { + // MCS 0: 36 us preamble + ceil((16 + 800 + 6) / 26) * 4 us + band extension. + ASSERT(typedHt->getDuration(B(100)) == SimTime(170, SIMTIME_US)); + ASSERT(typedAcHt->getDuration(B(100)) == SimTime(164, SIMTIME_US)); + } ASSERT(acHt->getDataMode()->getNetBitrate() == ht->getDataMode()->getNetBitrate()); ASSERT(acHt->getDataMode()->getNumberOfSpatialStreams() == ht->getDataMode()->getNumberOfSpatialStreams()); ASSERT(vhtOnlyModeSet->getIsMandatory(acHt) == htModeSet->getIsMandatory(ht)); diff --git a/tests/unit/Ieee80211MultipleProtection_1.test b/tests/unit/Ieee80211MultipleProtection_1.test index 1867858da01..d777b2e0e56 100644 --- a/tests/unit/Ieee80211MultipleProtection_1.test +++ b/tests/unit/Ieee80211MultipleProtection_1.test @@ -39,11 +39,12 @@ class PredicateFrames : public InProgressFrames { int reads = 0; bool hasInProgressFrames() override { return true; } Packet *getFrameToTransmit() override { reads++; return selected; } + Packet *peekFrameToTransmit() override { reads++; return selected; } }; class PredicateAckPolicy : public OriginatorQosAckPolicy { public: bool bar = false; - bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *) const override { return bar; } + bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return bar; } }; class PredicateRtsPolicy : public IRtsPolicy { public: diff --git a/tests/unit/Ieee80211ResponseTimeout_1.test b/tests/unit/Ieee80211ResponseTimeout_1.test index dcafbffa653..c486ec99abd 100644 --- a/tests/unit/Ieee80211ResponseTimeout_1.test +++ b/tests/unit/Ieee80211ResponseTimeout_1.test @@ -49,6 +49,8 @@ class TestRateSelection : public SimpleModule, public IRateSelection, public IQo const Ptr& header) override { return responseMode; } virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override { return responseMode; } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *, const Ptr&, bool) override { return responseMode; } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckResponseMode(const physicallayer::IIeee80211Mode *) override { return responseMode; } }; Define_Module(TestRateSelection); 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. diff --git a/tests/unit/Ieee80211TxNavChoice_1.test b/tests/unit/Ieee80211TxNavChoice_1.test index 168f85cd213..4d2816e2b0e 100644 --- a/tests/unit/Ieee80211TxNavChoice_1.test +++ b/tests/unit/Ieee80211TxNavChoice_1.test @@ -23,6 +23,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 { @@ -60,6 +63,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); diff --git a/tests/unit/TransmissionTimingSnapshot_1.test b/tests/unit/TransmissionTimingSnapshot_1.test new file mode 100644 index 00000000000..ff99ca1b5e5 --- /dev/null +++ b/tests/unit/TransmissionTimingSnapshot_1.test @@ -0,0 +1,138 @@ +%description: +Check shared transmission timing without a WLAN consumer or a propagation model. +Use generic transmissions and cached arrivals with exact endpoints. +Require snapshots to retain their values after cache expiry deletes the original objects. +Check positive, zero, and negative gaps for the same transmitter and different transmitters. +Check missing transmissions, missing arrivals, and the same transmission identity. +Require absent arrival queries to leave all three cache implementations unchanged before their first transmission. +This unit test does not establish physical reception or interframe protocol behavior. + +%includes: +#include +#include "inet/physicallayer/wireless/common/antenna/IsotropicAntenna.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/TransmissionBase.h" +#include "inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h" +#include "inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h" +#include "inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h" +#include "inet/physicallayer/wireless/common/medium/RadioMedium.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/Radio.h" +#include "inet/physicallayer/wireless/common/signal/Arrival.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TimingMedium : public RadioMedium { + public: + void attach(ICommunicationCache *cache) { communicationCache = cache; } +}; + +// These adapters supply identity and component references. No radio procedure executes. +class TimingRadio : public Radio { + int identity; + const IRadioMedium *attachedMedium; + const IAntenna *attachedAntenna; + const IReceiver *attachedReceiver; + public: + TimingRadio(int identity, const IRadioMedium *medium, const IAntenna *antenna, const IReceiver *receiver) : + identity(identity), attachedMedium(medium), attachedAntenna(antenna), attachedReceiver(receiver) {} + int getId() const override { return identity; } + const IRadioMedium *getMedium() const override { return attachedMedium; } + const IAntenna *getAntenna() const override { return attachedAntenna; } + const IReceiver *getReceiver() const override { return attachedReceiver; } +}; + +class TimingCache : public MapCommunicationCache { + public: + size_t receptionSlots() const { + size_t count = 0; + for (const auto& entry : transmissionCache) + if (entry.second.receptionCacheEntries) + count += entry.second.receptionCacheEntries->size(); + return count; + } +}; + +static TransmissionBase *makeTiming(const IRadio *radio, const Packet *packet, int startUs, int endUs) +{ + return new TransmissionBase(radio, packet, SimTime(startUs, SIMTIME_US), SimTime(endUs, SIMTIME_US), + SIMTIME_ZERO, SIMTIME_ZERO, SimTime(endUs - startUs, SIMTIME_US), Coord(), Coord(), + Quaternion(), Quaternion(), nullptr, nullptr, nullptr, nullptr, nullptr); +} + +template +static void checkAbsentArrival(const IRadio *radio, const ITransmission *transmission) +{ + Cache cache; + ASSERT(cache.getNumRadios() == 0 && cache.getNumTransmissions() == 0); + for (int i = 0; i < 3; i++) + ASSERT(cache.findCachedArrival(radio, transmission) == nullptr); + ASSERT(cache.getNumRadios() == 0 && cache.getNumTransmissions() == 0); +} + +%activity: +Packet packet("timing-input"); +IsotropicAntenna antenna; +FlatReceiverBase receiver; +TimingMedium medium; +TimingRadio transmitter(1, &medium, &antenna, &receiver); +TimingRadio recipient(2, &medium, &antenna, &receiver); +TimingRadio absentArrival(3, &medium, &antenna, &receiver); +TimingCache cache; +medium.attach(&cache); +auto previous = std::unique_ptr(makeTiming(&transmitter, &packet, 100, 140)); +checkAbsentArrival(&recipient, previous.get()); +checkAbsentArrival(&recipient, previous.get()); +checkAbsentArrival(&recipient, previous.get()); +int previousId = previous->getId(); +ASSERT(medium.getTransmissionTimingSnapshot(previousId).transmissionId == -1); +ASSERT(!medium.computePrecedingSignalGap(previousId, TransmissionTimingSnapshot()).has_value()); +cache.addRadio(&transmitter); +cache.addRadio(&recipient); +cache.addRadio(&absentArrival); +auto previousPointer = previous.release(); +cache.addTransmission(previousPointer); +cache.setCachedArrival(&recipient, previousPointer, new Arrival(SimTime(7, SIMTIME_US), SimTime(7, SIMTIME_US), + SimTime(107, SIMTIME_US), SimTime(147, SIMTIME_US), SIMTIME_ZERO, SIMTIME_ZERO, + SimTime(40, SIMTIME_US), Coord(), Coord(), Quaternion(), Quaternion())); +auto snapshot = medium.getTransmissionTimingSnapshot(previousId); +ASSERT(snapshot.transmissionId == previousId && snapshot.transmitterRadioId == 1); +ASSERT(snapshot.startTime == SimTime(100, SIMTIME_US) && snapshot.endTime == SimTime(140, SIMTIME_US)); +ASSERT(snapshot.arrivalEndTimes.size() == 1 && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +ASSERT(cache.findCachedArrival(&absentArrival, previousPointer) == nullptr); +ASSERT(cache.receptionSlots() == 1); +cache.setCachedInterferenceEndTime(previousPointer, SimTime(160, SIMTIME_US)); +wait(SimTime(200, SIMTIME_US)); +int expired = 0; +cache.removeNonInterferingTransmissions([&](const ITransmission *transmission) { + ASSERT(transmission->getId() == previousId); + expired++; +}); +ASSERT(expired == 1 && cache.getNumTransmissions() == 0); +ASSERT(medium.getTransmissionTimingSnapshot(previousId).transmissionId == -1); +ASSERT(!medium.computePrecedingSignalGap(previousId, snapshot).has_value()); +ASSERT(snapshot.endTime == SimTime(140, SIMTIME_US) && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +for (const TimingRadio *source : {&transmitter, &recipient}) { + int precedingEndUs = source == &transmitter ? 140 : 147; + for (int gapUs : {10, 0, -5}) { + auto current = makeTiming(source, &packet, precedingEndUs + gapUs, precedingEndUs + gapUs + 10); + cache.addTransmission(current); + auto gap = medium.computePrecedingSignalGap(current->getId(), snapshot); + ASSERT(gap.has_value() && *gap == SimTime(gapUs, SIMTIME_US)); + ASSERT(!medium.computePrecedingSignalGap(current->getId(), TransmissionTimingSnapshot()).has_value()); + auto sameId = snapshot; + sameId.transmissionId = current->getId(); + ASSERT(!medium.computePrecedingSignalGap(current->getId(), sameId).has_value()); + auto noArrival = snapshot; + noArrival.arrivalEndTimes.clear(); + auto withoutArrival = medium.computePrecedingSignalGap(current->getId(), noArrival); + ASSERT(source == &transmitter ? withoutArrival.has_value() : !withoutArrival.has_value()); + cache.removeTransmission(current); + } +} +ASSERT(snapshot.transmissionId == previousId && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +EV << "Shared transmission timing snapshot checks passed\n"; + +%contains: stdout +Shared transmission timing snapshot checks passed