From 435fc0866d1f3a682d3c4f377d20f993501a8303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:41:53 +0200 Subject: [PATCH 01/17] queueing: add: preserve pull effects for selected extraction Before this commit, consumers can inspect queued packets but cannot extract a later eligible packet with ordinary pull effects. Collection removal represents disposal. Deferred Block Ack DATA needs selection past an ineligible head without loss of queue observations. After this commit, the optional collection contract provides a pure predicate query and atomic selected extraction. PacketQueue, CompoundPacketQueueBase, and PriorityScheduler preserve configured service order. Each layer applies its ordinary pull effects once. For example, extraction behind a deferred head retains ownership, residence metadata, and queue events. An unsupported provider reports UnsupportedPacketSelection before extraction. Other queue and predicate errors retain their types. Direct tests cover storage, ownership, metadata, events, scheduler order, and unsupported providers. Ordinary head service remains unchanged. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.queueing | behavior.add | test whatsnew | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 + .../contract/ISelectivePacketCollection.h | 34 +++ .../queueing/queue/CompoundPacketQueueBase.cc | 20 ++ .../queueing/queue/CompoundPacketQueueBase.h | 5 +- src/inet/queueing/queue/PacketQueue.cc | 28 ++- src/inet/queueing/queue/PacketQueue.h | 6 +- .../queueing/scheduler/PriorityScheduler.cc | 46 ++++ .../queueing/scheduler/PriorityScheduler.h | 5 +- tests/module/PacketSelectiveExtraction_1.test | 213 ++++++++++++++++++ 9 files changed, 356 insertions(+), 5 deletions(-) create mode 100644 src/inet/queueing/contract/ISelectivePacketCollection.h create mode 100644 tests/module/PacketSelectiveExtraction_1.test diff --git a/WHATSNEW b/WHATSNEW index bdad647e6ad..e60ba90f7e1 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -510,6 +510,10 @@ Notable backward compatible changes are the following: 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/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/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. From 2c6b1df761f67abd436526bb821282306f1c143e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:44:06 +0200 Subject: [PATCH 02/17] ieee80211: fix: preserve Block Ack lifecycle boundaries Before this commit, Action dispatch can reuse deleted headers and repeat agreement effects. Setup can accept an uncorrelated response. Teardown can retain obsolete work or unsent fragments of transmitted DATA. DATA extraction can advance sequence state before setup resolves. After this commit, HCF dispatches complete accepted Actions once. Handlers correlate peer, TID, and dialog token before acceptance. The response determines agreement parameters. Directional retirement detaches the agreement before cleanup. Association replacement clears both roles before notification. Accepted physical exchanges finish safely but cannot submit retired protected frames after CTS. Setup defers new DATA extraction until the procedure resolves. Already numbered DATA resolves before ADDBA. A later successor can start setup after an empty interval without an empty request. The response timeout starts after the request ACK. Uncertain setup requires DELBA before surviving unsent DATA can resume. The QoS data service assigns a monotonic allocation index before fragmentation. All fragments retain that index in a tag. The agreement retains its highest transmitted index after ACK removal. Teardown uses that index to discard incomplete DATA. Older retransmissions cannot move the boundary backward. Wire sequence numbers remain unchanged. For example, sequence 0 transmits before sequences 1 through 2047 expire. Fragment 0 of sequence 2048 next transmits. Teardown discards its unsent siblings despite the exact half-range gap. Later intact DATA and unrelated peer DATA remain eligible. Deferred deletion protects references during accepted physical exchanges. An internal collision that discards first DATA also triggers retirement. HCF queues DELBA before the unsent successor can resume. Contention uses current eligibility, so deferred work cannot charge another category an internal collision. HCF schedules the earliest absolute deadline across both agreement roles. These lifecycle contracts change together because retirement requires safe Action invalidation and accepted-exchange completion at first use. The shared queue capability precedes this commit. Direct fixtures cover Action correlation, timeout, collision, fragment retirement, sequence gaps, and association reset. The migration guide explains custom-module contracts. Seven approved Basic fingerprint rows describe the changed setup boundaries. Numbered DATA resolves before setup, while infrastructure MacQosWithBlockAck waits for later DATA instead of empty-successor setup. Graphical expectations remain unchanged. Basic remains the default. Compressed exchanges, final-transfer arbitration, and MAC stop/start remain outside this commit. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change.fix | test whatsnew migration fingerprint | ieee80211-compressed-blockack-replacement --- WHATSNEW | 16 + doc/src/migration-guide/index.rst | 45 ++ .../mac/Ieee80211DataSequenceTag.msg | 17 + .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 7 + .../linklayer/ieee80211/mac/Ieee80211Mac.h | 4 +- .../blockack/OriginatorBlockAckAgreement.h | 16 + .../OriginatorBlockAckAgreementHandler.cc | 125 ++- .../OriginatorBlockAckAgreementHandler.h | 16 +- .../OriginatorBlockAckAgreementPolicy.cc | 27 +- .../OriginatorBlockAckAgreementPolicy.h | 1 + .../OriginatorBlockAckAgreementPolicy.ned | 5 +- .../mac/blockack/RecipientBlockAckAgreement.h | 13 + .../RecipientBlockAckAgreementHandler.cc | 83 +- .../RecipientBlockAckAgreementHandler.h | 9 +- .../RecipientBlockAckAgreementPolicy.cc | 17 +- .../RecipientBlockAckAgreementPolicy.h | 1 + .../RecipientBlockAckAgreementPolicy.ned | 2 +- .../blockackreordering/BlockAckReordering.cc | 38 + .../blockackreordering/BlockAckReordering.h | 2 + .../ieee80211/mac/channelaccess/Edcaf.cc | 5 + .../ieee80211/mac/channelaccess/Edcaf.h | 3 + .../mac/contention/EdcaCollisionController.cc | 4 +- .../ieee80211/mac/contract/BlockAckVariant.h | 14 + .../IBlockAckAgreementHandlerCallback.h | 3 +- .../contract/IBlockAckAgreementLifecycle.h | 22 + .../mac/contract/IFrameSequenceHandler.h | 2 + .../IOriginatorBlockAckAgreementHandler.h | 20 + .../IOriginatorBlockAckAgreementPolicy.h | 3 + .../IRecipientBlockAckAgreementHandler.h | 7 + .../IRecipientBlockAckAgreementPolicy.h | 3 + .../contract/IRecipientQosMacDataService.h | 2 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 755 +++++++++++++++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 39 +- .../duplicateremoval/QosDuplicateRemoval.cc | 18 + .../duplicateremoval/QosDuplicateRemoval.h | 2 + .../mac/fragmentation/BasicReassembly.cc | 6 +- .../mac/framesequence/FrameSequenceHandler.cc | 11 + .../mac/framesequence/FrameSequenceHandler.h | 1 + .../framesequence/PrimitiveFrameSequences.cc | 16 +- .../mac/originator/NonQosRecoveryProcedure.cc | 27 +- .../mac/originator/NonQosRecoveryProcedure.h | 2 + .../mac/originator/OriginatorQosAckPolicy.cc | 4 +- .../originator/OriginatorQosMacDataService.cc | 67 +- .../originator/OriginatorQosMacDataService.h | 12 +- .../ieee80211/mac/queue/InProgressFrames.cc | 1 + .../ieee80211/mac/queue/InProgressFrames.h | 3 + .../recipient/RecipientQosMacDataService.cc | 61 +- .../recipient/RecipientQosMacDataService.h | 2 + .../QoSSequenceNumberAssignment.cc | 6 + .../QoSSequenceNumberAssignment.h | 1 + .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 11 + .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 7 + .../ieee80211/mgmt/Ieee80211MgmtBase.h | 1 + .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 1 + tests/fingerprint/examples.csv | 4 +- tests/fingerprint/showcases.csv | 10 +- .../Ieee80211BlockAckActionDelivery_1.test | 70 ++ .../Ieee80211BlockAckActionDispatch_1.test | 178 +++++ .../Ieee80211BlockAckActionDisposal_1.test | 470 +++++++++++ .../module/Ieee80211BlockAckAdmission_1.test | 132 +++ .../Ieee80211BlockAckAssociationReset_1.test | 451 +++++++++++ .../Ieee80211BlockAckDataSelection_1.test | 313 ++++++++ tests/module/Ieee80211BlockAckDeferral_1.test | 393 +++++++++ .../Ieee80211BlockAckDeferredCollision_1.test | 203 +++++ ...Ieee80211BlockAckFragmentRetirement_1.test | 297 +++++++ .../module/Ieee80211BlockAckLifecycle_1.test | 358 +++++++++ .../Ieee80211BlockAckNegotiation_1.test | 83 ++ .../Ieee80211BlockAckResponseWait_1.test | 242 ++++++ .../Ieee80211BlockAckRetiredData_1.test | 325 ++++++++ 69 files changed, 4994 insertions(+), 121 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag.msg create mode 100644 src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementLifecycle.h create mode 100644 tests/module/Ieee80211BlockAckActionDelivery_1.test create mode 100644 tests/module/Ieee80211BlockAckActionDispatch_1.test create mode 100644 tests/module/Ieee80211BlockAckActionDisposal_1.test create mode 100644 tests/module/Ieee80211BlockAckAdmission_1.test create mode 100644 tests/module/Ieee80211BlockAckAssociationReset_1.test create mode 100644 tests/module/Ieee80211BlockAckDataSelection_1.test create mode 100644 tests/module/Ieee80211BlockAckDeferral_1.test create mode 100644 tests/module/Ieee80211BlockAckDeferredCollision_1.test create mode 100644 tests/module/Ieee80211BlockAckFragmentRetirement_1.test create mode 100644 tests/module/Ieee80211BlockAckLifecycle_1.test create mode 100644 tests/module/Ieee80211BlockAckNegotiation_1.test create mode 100644 tests/module/Ieee80211BlockAckResponseWait_1.test create mode 100644 tests/module/Ieee80211BlockAckRetiredData_1.test diff --git a/WHATSNEW b/WHATSNEW index e60ba90f7e1..097c0d79b9e 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -428,6 +428,22 @@ 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. + + Basic remains the default. Compressed selection still stops initialization. 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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 6da76178c57..138bc9d7fa1 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -158,6 +158,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. Basic remains the default. Compressed selection still stops initialization. + +The recipient response now determines the buffer size and inactivity timeout. The built-in recipient selects the smaller of 64 and its configured maximum. A request size of zero supplies no size guidance. The response copies the request dialog token. Both negotiated A-MSDU flags must permit aggregation under an active agreement. + +The originator starts ``mac.hcf.originatorBlockAckAgreementPolicy.addbaFailureTimeout`` after the request receives its ACK. Its default is 100 ms. The value must be positive and finite. + +If the response does not arrive, HCF sends DELBA before it releases surviving unsent DATA. DATA lifetime separately limits retained DATA. Failed DELBA disposes of the current deferred batch. Later DATA starts one fresh recovery exchange. + +At ordinary teardown, the originator discards every transmitted, unacknowledged MPDU for the selected agreement. This includes DATA whose lifetime remains valid when inactivity expires. It also discards unsent fragments through the highest transmitted allocation index, even if an earlier fragment received an ACK. This local index records allocation order before fragmentation. Later unsent DATA retains sequence continuity. + +For example, fragment 0 of sequence 4095 enters transmission before teardown. Teardown discards its unsent fragments and preserves the intact successor with sequence 0. The recipient delivers complete buffered units in cyclic order before it releases resources. It discards incomplete units. For example, complete units 101 and 102 can reach the upper process after teardown across absent unit 100. + +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(notifyFailure)``. A false argument ends an exchange that an accepted Action already resolves, without obsolete failure feedback. Cleanup must destroy the exchange context before it deletes deferred frame copies. These repairs do not add MAC stop/start recovery or final-transfer teardown. + IEEE 802.11 Response Query Roles -------------------------------- 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/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index b864c39f435..5db40af167f 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); 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/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..8ef73652db3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -24,6 +24,9 @@ class INET_API OriginatorBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; bool isAMsduSupported = false; + bool responseAMsduSupported = false; + int dialogToken = -1; + int64_t highestTransmittedDataIndex = -1; bool isDelayedBlockAckPolicySupported = false; bool isAddbaResponseReceived = false; bool isAddbaRequestSent = false; @@ -49,6 +52,19 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } + // IEEE Std 802.11-2024, 10.25.2 defines ADDBA negotiation; INET requires both flags before A-MSDU aggregation. + bool isAMsduPermitted() const { return isAMsduSupported && responseAMsduSupported; } + int getDialogToken() const { return dialogToken; } + void setDialogToken(int token) { dialogToken = token; } + void setResponseAMsduSupported(bool value) { responseAMsduSupported = value; } + bool getFirstDataTransmitted() const { return highestTransmittedDataIndex >= 0; } + int64_t getHighestTransmittedDataIndex() const { return highestTransmittedDataIndex; } + void recordTransmittedData(int64_t allocationIndex) + { + ASSERT(allocationIndex >= 0); + if (highestTransmittedDataIndex < allocationIndex) + highestTransmittedDataIndex = allocationIndex; + } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } virtual MacAddress getReceiverAddr() const { return receiverAddr; } virtual Tid getTid() const { return tid; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2f11dc365db..fdc8c44dd1e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -8,15 +8,108 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" namespace inet { namespace ieee80211 { +void OriginatorBlockAckAgreementHandler::noteEligibleDataTransmission(MacAddress peer, Tid tid) +{ + Procedure procedure; + procedure.state = ProcedureState::WAITING_FOR_SUCCESSOR; + procedures.emplace(AgreementId(peer, tid), procedure); +} + +void OriginatorBlockAckAgreementHandler::beginSetupIntent(MacAddress peer, Tid tid) +{ + auto result = procedures.emplace(AgreementId(peer, tid), Procedure()); + if (result.first->second.state == ProcedureState::WAITING_FOR_SUCCESSOR) + result.first->second.state = ProcedureState::SETUP_INTENT; +} + +void OriginatorBlockAckAgreementHandler::beginTeardown(MacAddress peer, Tid tid) +{ + Procedure procedure; + procedure.state = ProcedureState::TEARDOWN_PENDING; + procedures[{peer, tid}] = procedure; +} + +void OriginatorBlockAckAgreementHandler::failTeardown(MacAddress peer, Tid tid) +{ + Procedure procedure; + procedure.state = ProcedureState::RECOVERY_NEEDED; + procedures[{peer, tid}] = procedure; +} + +void OriginatorBlockAckAgreementHandler::finishProcedure(MacAddress peer, Tid tid) +{ + procedures.erase({peer, tid}); +} + +void OriginatorBlockAckAgreementHandler::markRequestAttempted(MacAddress peer, Tid tid) +{ + auto it = procedures.find({peer, tid}); + if (it != procedures.end() && it->second.state == ProcedureState::REQUEST_PENDING) + it->second.requestAttempted = true; +} + +void OriginatorBlockAckAgreementHandler::acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) +{ + auto it = procedures.find({peer, tid}); + if (it != procedures.end() && it->second.state == ProcedureState::REQUEST_PENDING) + it->second.responseDeadline = simTime() + timeout; +} + void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest) { OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; + blockAckAgreement->setDialogToken(addbaRequest->getDialogToken()); + procedures[agreementId].state = ProcedureState::REQUEST_PENDING; +} + +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::detachAgreement(MacAddress peer, Tid tid) +{ + auto it = blockAckAgreements.find({peer, tid}); + if (it == blockAckAgreements.end()) + return nullptr; + auto agreement = it->second; + blockAckAgreements.erase(it); + return agreement; +} + +std::vector OriginatorBlockAckAgreementHandler::detachPeer(MacAddress peer) +{ + std::vector removed; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + if (it->first.first == peer) { + removed.push_back(it->second); + it = blockAckAgreements.erase(it); + } + else + ++it; + } + for (auto it = procedures.begin(); it != procedures.end();) { + if (it->first.first == peer) + it = procedures.erase(it); + else + ++it; + } + return removed; +} + +void OriginatorBlockAckAgreementHandler::startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, + IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) +{ + auto request = buildAddbaRequest(peer, tid, sequence, policy); + request->setAMsduSupported(aMsdu && policy->isMsduSupported()); + // IEEE Std 802.11-2024, Table 9-466: ordinary setup uses a nonzero dialog token. + auto& token = dialogTokens[{peer, tid}]; + token = token % 255 + 1; + request->setDialogToken(token); + createAgreement(request); + callback->processMgmtFrame(new Packet("AddbaReq", request), request); } simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() @@ -40,15 +133,16 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + std::vector expired; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { - MacAddress receiverAddr = id.first.first; - Tid tid = id.first.second; - const auto& delba = buildDelba(receiverAddr, tid, 39); - auto delbaPacket = new Packet("Delba", delba); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + if (agreement->getIsAddbaResponseReceived() && agreement->getExpirationTime() <= now) + expired.push_back(id.first); + } + for (const auto& id : expired) { + const auto& delba = buildDelba(id.first, id.second, 39); + auto delbaPacket = new Packet("Delba", delba); + procedureCallback->processMgmtFrame(delbaPacket, delba); } scheduleInactivityTimer(agreementHandlerCallback); } @@ -87,9 +181,7 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + callback->scheduleInactivityTimer(); } OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) @@ -124,12 +216,8 @@ void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originato void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) { auto agreement = getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); - if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr) { - auto addbaReq = buildAddbaRequest(dataHeader->getReceiverAddress(), dataHeader->getTid(), dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); - createAgreement(addbaReq); - auto addbaPacket = new Packet("AddbaReq", addbaReq); - callback->processMgmtFrame(addbaPacket, addbaReq); - } + if (agreement && agreement->getIsAddbaResponseReceived()) + agreement->recordTransmittedData(packet->getTag()->getAllocationIndex()); } void OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -149,6 +237,7 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree agreement->setIsAddbaResponseReceived(true); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); + agreement->setResponseAMsduSupported(addbaResp->getAMsduSupported()); agreement->calculateExpirationTime(); } @@ -157,13 +246,11 @@ void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(const PtrgetReceiverAddress(), addbaReq->getTid()); if (agreement) agreement->setIsAddbaRequestSent(true); - else - throw cRuntimeError("Block Ack Agreement should have already been added"); } void OriginatorBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + // HCF retires the agreement before it queues DELBA. ACK completes teardown. } void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4a449d17da3..af0c99d0cb7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,6 +21,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg { protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; + std::map procedures; + std::map dialogTokens; protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); @@ -28,10 +30,22 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); - virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: + virtual simtime_t computeEarliestExpirationTime() override; + const std::map& getAgreements() const override { return blockAckAgreements; } + const std::map& getProcedures() const override { return procedures; } + virtual void noteEligibleDataTransmission(MacAddress peer, Tid tid) override; + virtual void beginSetupIntent(MacAddress peer, Tid tid) override; + virtual void beginTeardown(MacAddress peer, Tid tid) override; + virtual void failTeardown(MacAddress peer, Tid tid) override; + virtual void finishProcedure(MacAddress peer, Tid tid) override; + virtual void markRequestAttempted(MacAddress peer, Tid tid) override; + virtual void acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) override; + OriginatorBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) override; + std::vector detachPeer(MacAddress peer) override; + void startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) override; virtual ~OriginatorBlockAckAgreementHandler(); virtual void processTransmittedAddbaReq(const Ptr& addbaReq) override; virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 44b4c0874fa..835216df923 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -9,6 +9,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { @@ -24,15 +25,23 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) aMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); - // TODO addbaFailureTimeout = par("addbaFailureTimeout"); + addbaFailureTimeout = par("addbaFailureTimeout"); + if (addbaFailureTimeout <= SIMTIME_ZERO || addbaFailureTimeout == SIMTIME_MAX) + throw cRuntimeError("addbaFailureTimeout must be positive and finite"); + if (maximumAllowedBufferSize <= 0 || blockAckTimeoutValue < SIMTIME_ZERO) + throw cRuntimeError("Invalid Block Ack buffer size or inactivity timeout"); WATCH(blockAckReqThreshold); } } simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const { - // TODO ADDBAFailureTimeout -- 6.3.29.2.2 Semantics of the service primitive - throw cRuntimeError("Unimplemented"); + return addbaFailureTimeout; +} + +bool OriginatorBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const +{ + return variant == BlockAckVariant::BASIC || (localHt && peerHt); } bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const Ptr& header) @@ -42,8 +51,16 @@ bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const P bool OriginatorBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) { - ASSERT(agreement); - return true; + // IEEE Std 802.11-2024, 10.25.2 and Table 9-466: match the pending setup and its nonzero dialog token. + // Immediate Block Ack and the 64-entry bound are INET profile limits for historical Basic compatibility. + return agreement != nullptr && !agreement->getIsAddbaResponseReceived() && + addbaResp->getTransmitterAddress() == agreement->getReceiverAddr() && + addbaResp->getTid() == agreement->getTid() && + addbaResp->getDialogToken() > 0 && addbaResp->getDialogToken() <= 255 && + addbaResp->getDialogToken() == agreement->getDialogToken() && + addbaResp->getStatusCode() == SC_SUCCESSFUL && addbaResp->getBlockAckPolicy() && + addbaResp->getBufferSize() > 0 && addbaResp->getBufferSize() <= 64 && + addbaResp->getBlockAckTimeoutValue() >= SIMTIME_ZERO; } bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index b762cc29828..b0b7e9cce24 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -32,6 +32,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi virtual void initialize(int stage) override; public: + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override; virtual bool isAddbaReqNeeded(Packet *packet, const Ptr& header) override; virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) override; virtual bool isDelbaAccepted(const Ptr& delba) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index bbb569b4b20..ab9970f883f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -21,8 +21,9 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); - int maximumAllowedBufferSize = default(64); - double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + int maximumAllowedBufferSize = default(64); // The ADDBA Request uses this advisory buffer size; the recipient response determines the accepted allocation. + double blockAckTimeoutValue @unit(s) = default(0s); // Zero disables inactivity expiration. + double addbaFailureTimeout @unit(s) = default(100ms); // Response wait after request ACK. @display("i=block/control"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 40a4186fead..6e9634cae6f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -23,6 +23,8 @@ class INET_API RecipientBlockAckAgreement : public cObject simtime_t blockAckTimeoutValue = 0; bool isAddbaResponseSent = false; simtime_t expirationTime = -1; + SequenceNumberCyclic highestReceivedSequence; + int highestReceivedFragment = 0; public: RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); @@ -33,6 +35,17 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual BlockAckRecord *getBlockAckRecord() const { return blockAckRecord; } virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } + void recordReceived(const Ptr& header) { + auto sequence = header->getSequenceNumber(); + if (!highestReceivedSequence.isSpecified() || highestReceivedSequence < sequence) { + highestReceivedSequence = sequence; + highestReceivedFragment = header->getFragmentNumber(); + } + else if (highestReceivedSequence == sequence) + highestReceivedFragment = std::max(highestReceivedFragment, int(header->getFragmentNumber())); + } + const SequenceNumberCyclic& getHighestReceivedSequence() const { return highestReceivedSequence; } + int getHighestReceivedFragment() const { return highestReceivedFragment; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } virtual void addbaResposneSent() { isAddbaResponseSent = true; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 6219ac77140..717368a5a46 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { @@ -24,9 +25,7 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback) { - simtime_t earliestExpirationTime = computeEarliestExpirationTime(); - if (earliestExpirationTime != SIMTIME_MAX) - callback->scheduleInactivityTimer(earliestExpirationTime); + callback->scheduleInactivityTimer(); } // The inactivity timer at a recipient is reset when MPDUs corresponding to the TID for which the Block Ack @@ -35,12 +34,15 @@ void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreeme // void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) { + // IEEE Std 802.11-2024, 11.5.4: matching Block Ack-policy DATA refreshes inactivity, including a duplicate. if (qosHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { // TODO + Implicit Block Ack Tid tid = qosHeader->getTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); auto agreement = getAgreement(tid, originatorAddr); - if (agreement) + if (agreement) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } } } @@ -51,15 +53,16 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + std::vector> expired; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { - MacAddress receiverAddr = id.first.first; - Tid tid = id.first.second; - const auto& delba = buildDelba(receiverAddr, tid, 39); - auto delbaPacket = new Packet("Delba", delba); - procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes - } + if (agreement->getExpirationTime() <= now) + expired.push_back(id.first); + } + for (const auto& id : expired) { + const auto& delba = buildDelba(id.first, id.second, 39); + auto delbaPacket = new Packet("Delba", delba); + procedureCallback->processMgmtFrame(delbaPacket, delba); } scheduleInactivityTimer(agreementHandlerCallback); } @@ -110,8 +113,10 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR Tid tid = addbaRequest->getTid(); addbaResponse->setTid(tid); addbaResponse->setBlockAckPolicy(!addbaRequest->getBlockAckPolicy() && blockAckAgreementPolicy->delayedBlockAckPolicySupported() ? false : true); - addbaResponse->setBufferSize(addbaRequest->getBufferSize() <= blockAckAgreementPolicy->getMaximumAllowedBufferSize() ? addbaRequest->getBufferSize() : blockAckAgreementPolicy->getMaximumAllowedBufferSize()); - addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? blockAckAgreementPolicy->getBlockAckTimeoutValue() : addbaRequest->getBlockAckTimeoutValue()); + addbaResponse->setDialogToken(addbaRequest->getDialogToken()); + // IEEE Std 802.11-2024, 10.25.2: the request allocation and timeout are advisory. + addbaResponse->setBufferSize(std::min(64, blockAckAgreementPolicy->getMaximumAllowedBufferSize())); + addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue()); addbaResponse->setAMsduSupported(blockAckAgreementPolicy->aMsduSupported()); return addbaResponse; } @@ -124,8 +129,6 @@ void RecipientBlockAckAgreementHandler::updateAgreement(const Ptrsecond; agreement->addbaResposneSent(); } - else - throw cRuntimeError("Agreement is not found"); } void RecipientBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) @@ -155,26 +158,62 @@ void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) { EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest)) { + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); + bool accepted = blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest) && + blockAckAgreements.count(id) == 0 && pendingTeardowns.count(id) == 0; + addbaResponse->setStatusCode(accepted ? SC_SUCCESSFUL : SC_UNSPECIFIED); + if (accepted) { EV_DETAIL << "Addba Request has been accepted. Creating a new Block Ack Agreement." << endl; - auto agreement = addAgreement(addbaRequest); + auto parameters = makeShared(*addbaRequest); + parameters->setBufferSize(addbaResponse->getBufferSize()); + parameters->setBlockAckTimeoutValue(addbaResponse->getBlockAckTimeoutValue()); + auto agreement = addAgreement(parameters); EV_DETAIL << "Agreement is added with the following parameters: " << *agreement << endl; EV_DETAIL << "Building Addba Response" << endl; - auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); - auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); - callback->processMgmtFrame(addbaResponsePacket, addbaResponse); } + callback->processMgmtFrame(new Packet("AddbaResponse", addbaResponse), addbaResponse); +} + +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::detachAgreement(MacAddress peer, Tid tid) +{ + auto it = blockAckAgreements.find({peer, tid}); + if (it == blockAckAgreements.end()) + return nullptr; + auto agreement = it->second; + blockAckAgreements.erase(it); + return agreement; +} + +std::vector RecipientBlockAckAgreementHandler::detachPeer(MacAddress peer) +{ + std::vector removed; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + if (it->first.first == peer) { + removed.push_back(it->second); + it = blockAckAgreements.erase(it); + } + else + ++it; + } + for (auto it = pendingTeardowns.begin(); it != pendingTeardowns.end();) { + if (it->first == peer) + it = pendingTeardowns.erase(it); + else + ++it; + } + return removed; } void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + // HCF retains teardown state until the Action reaches a terminal result. } void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + terminateAgreement(delba->getTransmitterAddress(), delba->getTid()); } RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 1e65a1557ef..ac93c8366e6 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,6 +26,7 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre { protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; + std::set> pendingTeardowns; protected: virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); @@ -33,10 +34,16 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual void updateAgreement(const Ptr& addbaResponse); virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); - virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: + virtual simtime_t computeEarliestExpirationTime() override; + const std::map, RecipientBlockAckAgreement *>& getAgreements() const override { return blockAckAgreements; } + bool isTeardownPending(MacAddress peer, Tid tid) const override { return pendingTeardowns.count({peer, tid}) != 0; } + void beginTeardown(MacAddress peer, Tid tid) override { pendingTeardowns.insert({peer, tid}); } + void finishTeardown(MacAddress peer, Tid tid) override { pendingTeardowns.erase({peer, tid}); } + RecipientBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) override; + std::vector detachPeer(MacAddress peer) override; virtual ~RecipientBlockAckAgreementHandler(); virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) override; virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index 73813feaa5d..f2bd97fa697 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -19,12 +19,27 @@ void RecipientBlockAckAgreementPolicy::initialize(int stage) isAMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); + if (maximumAllowedBufferSize <= 0 || blockAckTimeoutValue < SIMTIME_ZERO) + throw cRuntimeError("Invalid Block Ack buffer size or inactivity timeout"); } } bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaReq) { - return true; + // IEEE Std 802.11-2024, 10.25.2 and Table 9-466: validate ordinary setup; the requested buffer size is advisory. + // INET supports immediate Block Ack for historical Basic compatibility and selects at most 64 entries in the response. + return !addbaReq->getTransmitterAddress().isUnspecified() && + !addbaReq->getTransmitterAddress().isMulticast() && + addbaReq->getTid() >= 0 && addbaReq->getTid() <= 15 && + addbaReq->getDialogToken() > 0 && addbaReq->getDialogToken() <= 255 && + addbaReq->getBlockAckPolicy() && addbaReq->getBufferSize() >= 0 && + addbaReq->getBufferSize() <= 1023 && addbaReq->getStartingSequenceNumber().isSpecified() && + addbaReq->getBlockAckTimeoutValue() >= SIMTIME_ZERO; +} + +bool RecipientBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const +{ + return variant == BlockAckVariant::BASIC || (localHt && peerHt); } bool RecipientBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h index 995fcc29fdb..d74bfd4240b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h @@ -28,6 +28,7 @@ class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IR virtual void initialize(int stage) override; public: + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override; virtual bool isAddbaReqAccepted(const Ptr& addbaReq) override; virtual bool isDelbaAccepted(const Ptr& delba) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned index 8c41285b009..6fb403594cf 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned @@ -20,7 +20,7 @@ simple RecipientBlockAckAgreementPolicy extends SimpleModule like IRecipientBloc bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); - double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + double blockAckTimeoutValue @unit(s) = default(0s); // Zero disables inactivity expiration. @display("i=block/control"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 6cffea3c37d..02a811e4732 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -12,6 +12,44 @@ namespace inet { namespace ieee80211 { +BlockAckReordering::ReleasedFrames BlockAckReordering::detachAgreement(MacAddress peer, Tid tid, + bool deliverComplete, std::vector& discarded) +{ + auto it = receiveBuffers.find({tid, peer}); + if (it == receiveBuffers.end()) + return {}; + auto buffer = it->second; + receiveBuffers.erase(it); + ReleasedFrames released; + auto start = buffer->getNextExpectedSequenceNumber(); + for (int i = 0; i < buffer->getBufferSize(); i++) { + auto sequence = start + i; + auto position = buffer->getBuffer().find(sequence.get()); + if (position == buffer->getBuffer().end()) + continue; + auto fragments = position->second; + if (deliverComplete && isComplete(fragments)) + released.push_back({sequence.get(), fragments}); + else + discarded.insert(discarded.end(), fragments.begin(), fragments.end()); + buffer->removeFrame(sequence); + } + delete buffer; + return released; +} + +void BlockAckReordering::clearPeer(MacAddress peer) +{ + for (auto it = receiveBuffers.begin(); it != receiveBuffers.end();) { + if (it->first.second == peer) { + delete it->second; + it = receiveBuffers.erase(it); + } + else + ++it; + } +} + BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) { ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 42215dd9b40..05a203ed44b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -50,6 +50,8 @@ class INET_API BlockAckReordering public: virtual ~BlockAckReordering(); + ReleasedFrames detachAgreement(MacAddress peer, Tid tid, bool deliverComplete, std::vector& discarded); + void clearPeer(MacAddress peer); void processReceivedDelba(const Ptr& delba); ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index 5dc48059089..cb0e9d6549b 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -165,6 +165,11 @@ bool Edcaf::isInternalCollision() return collisionController->isInternalCollision(this); } +bool Edcaf::hasEligibleFrames() const +{ + return frameEligibility ? frameEligibility() : !pendingQueue->isEmpty() || inProgressFrames->getLength() != 0; +} + int Edcaf::getCwMax(AccessCategory ac, int aCwMax, int aCwMin) { switch (ac) { diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index c2d716ade33..b9a77e9c051 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -49,6 +49,7 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub InProgressFrames *inProgressFrames = nullptr; bool owning = false; + std::function frameEligibility; simtime_t slotTime = -1; simtime_t sifs = -1; @@ -102,6 +103,8 @@ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, pub // Edcaf virtual bool isOwning() { return owning; } virtual bool isInternalCollision(); + void setFrameEligibility(std::function predicate) { frameEligibility = predicate; } + bool hasEligibleFrames() const; virtual AccessCategory getAccessCategory() { return ac; } }; diff --git a/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc b/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc index 33b94c93563..4b3172e039f 100644 --- a/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc +++ b/src/inet/linklayer/ieee80211/mac/contention/EdcaCollisionController.cc @@ -36,9 +36,9 @@ bool EdcaCollisionController::isInternalCollision(Edcaf *edcaf) simtime_t now = simTime(); AccessCategory accessCategory = edcaf->getAccessCategory(); auto hasFrames = [](const Edcaf *candidate) { - return !candidate->getPendingQueue()->isEmpty() || candidate->getInProgressFrames()->getLength() != 0; + return candidate->hasEligibleFrames(); }; - // An expired empty queue cannot initiate a TXOP or cause an internal collision. + // Deferred or expired DATA cannot initiate a TXOP or cause an internal collision. // Keep the winner's in-progress frame visible until its transmission completes. if (txStartTimes[accessCategory] == now && hasFrames(edcaf)) { for (int ac = accessCategory + 1; ac < 4; ac++) { diff --git a/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h b/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h new file mode 100644 index 00000000000..f02e1033036 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_BLOCKACKVARIANT_H +#define __INET_BLOCKACKVARIANT_H + +namespace inet { +namespace ieee80211 { + +enum class BlockAckVariant { BASIC, COMPRESSED }; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cc4a7b86b92..3fefe1666c0 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -18,7 +18,8 @@ class INET_API IBlockAckAgreementHandlerCallback public: virtual ~IBlockAckAgreementHandlerCallback() {} - virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + // The coordinator recomputes the earliest absolute deadline across both agreement roles. + virtual void scheduleInactivityTimer() = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/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..0f61eaa9891 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -48,6 +48,8 @@ class INET_API IFrameSequenceHandler virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; + // The caller can suppress failure feedback for an exchange that its procedure already resolved. + virtual void interruptFrameSequence(bool notifyFailure) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 4a01e89b6c3..7161304e0ad 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -22,7 +22,27 @@ namespace ieee80211 { class INET_API IOriginatorBlockAckAgreementHandler { public: + typedef std::pair AgreementId; + enum class ProcedureState { SETUP_INTENT, REQUEST_PENDING, TEARDOWN_PENDING, RECOVERY_NEEDED, WAITING_FOR_SUCCESSOR }; + struct Procedure { + ProcedureState state = ProcedureState::SETUP_INTENT; + bool requestAttempted = false; + simtime_t responseDeadline = SIMTIME_MAX; + }; virtual ~IOriginatorBlockAckAgreementHandler() {} + virtual const std::map& getAgreements() const = 0; + virtual const std::map& getProcedures() const = 0; + virtual simtime_t computeEarliestExpirationTime() = 0; + virtual OriginatorBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) = 0; + virtual std::vector detachPeer(MacAddress peer) = 0; + virtual void noteEligibleDataTransmission(MacAddress peer, Tid tid) = 0; + virtual void beginSetupIntent(MacAddress peer, Tid tid) = 0; + virtual void beginTeardown(MacAddress peer, Tid tid) = 0; + virtual void failTeardown(MacAddress peer, Tid tid) = 0; + virtual void finishProcedure(MacAddress peer, Tid tid) = 0; + virtual void markRequestAttempted(MacAddress peer, Tid tid) = 0; + virtual void acknowledgeRequest(MacAddress peer, Tid tid, simtime_t timeout) = 0; + virtual void startSetup(MacAddress peer, Tid tid, SequenceNumberCyclic sequence, IOriginatorBlockAckAgreementPolicy *policy, IProcedureCallback *callback, bool aMsdu) = 0; virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void processTransmittedAddbaReq(const Ptr& addbaReq) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 40ea82aa95e..c6e1ddd9638 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -9,6 +9,7 @@ #define __INET_IORIGINATORBLOCKACKAGREEMENTPOLICY_H #include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { @@ -21,6 +22,8 @@ class INET_API IOriginatorBlockAckAgreementPolicy public: virtual ~IOriginatorBlockAckAgreementPolicy() {} + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const = 0; + virtual bool isAddbaReqNeeded(Packet *packet, const Ptr& header) = 0; virtual bool isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) = 0; virtual bool isDelbaAccepted(const Ptr& delba) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 357745c3ddb..5c4beb3fe04 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -23,6 +23,13 @@ class INET_API IRecipientBlockAckAgreementHandler { public: virtual ~IRecipientBlockAckAgreementHandler() {} + virtual simtime_t computeEarliestExpirationTime() = 0; + virtual const std::map, RecipientBlockAckAgreement *>& getAgreements() const = 0; + virtual RecipientBlockAckAgreement *detachAgreement(MacAddress peer, Tid tid) = 0; + virtual std::vector detachPeer(MacAddress peer) = 0; + virtual bool isTeardownPending(MacAddress peer, Tid tid) const = 0; + virtual void beginTeardown(MacAddress peer, Tid tid) = 0; + virtual void finishTeardown(MacAddress peer, Tid tid) = 0; virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h index 48009a67161..2f429ef7169 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h @@ -9,6 +9,7 @@ #define __INET_IRECIPIENTBLOCKACKAGREEMENTPOLICY_H #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" namespace inet { namespace ieee80211 { @@ -18,6 +19,8 @@ class INET_API IRecipientBlockAckAgreementPolicy public: virtual ~IRecipientBlockAckAgreementPolicy() {} + virtual bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const = 0; + virtual bool isAddbaReqAccepted(const Ptr& addbaReq) = 0; virtual bool isDelbaAccepted(const Ptr& delba) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 8a3ff4c0b7c..7e97329e94d 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -23,6 +23,8 @@ class INET_API IRecipientQosMacDataService public: virtual ~IRecipientQosMacDataService() {} + virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) = 0; + virtual void clearPeer(MacAddress peer) = 0; virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index a05c4f1d269..7deeca7ef0f 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -10,6 +10,7 @@ #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" @@ -19,6 +20,9 @@ #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" namespace inet { @@ -46,9 +50,11 @@ void Hcf::initialize(int stage) if (blockAckVariant == BlockAckVariant::COMPRESSED) throw cRuntimeError("Compressed Block Ack exchanges are not implemented"); mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); + mib = check_and_cast(mac->getModuleByPath(mac->par("mibModule"))); startRxTimer = new cMessage("startRxTimeout"); inactivityTimer = new cMessage("blockAckInactivityTimer"); dataLifetimeTimer = new cMessage("dataLifetimeTimer"); + addbaResponseTimer = new cMessage("addbaResponseTimer"); edca = check_and_cast(getSubmodule("edca")); hcca = check_and_cast(getSubmodule("hcca")); tx = check_and_cast(getModuleByPath(par("txModule"))); @@ -83,10 +89,17 @@ void Hcf::initialize(int stage) lifetimeHandler->setDeadlineChangedCallback([this]() { scheduleDataLifetimeTimer(); }); check_and_cast(originatorDataService)->setLifetimeHandler( lifetimeHandler, [this]() { expireDataFrames(); }); + check_and_cast(originatorDataService)->setEligibility( + [this](Packet *packet) { return isFrameEligible(packet, false); }, + [this](Packet *packet) { return isAMsduPermitted(packet); }); + mib->subscribe(Ieee80211Mib::rateStateChangedSignal, this); for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + edca->getEdcaf(static_cast(ac))->setFrameEligibility( + [this, ac]() { return hasEligibleFrameToTransmit(static_cast(ac)); }); check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(packetDroppedSignal, this); auto frames = edca->getEdcaf(static_cast(ac))->getInProgressFrames(); frames->setLifetimeHandler(lifetimeHandler); + frames->setTransmissionEligibility([this](Packet *packet) { return isFrameEligible(packet, true); }); frames->subscribe(InProgressFrames::packetDequeuedSignal, this); } } @@ -179,6 +192,8 @@ void Hcf::expireDataFrames(bool preserveSequence) PacketDropDetails details; details.setReason(LIFETIME_EXPIRED); emit(packetDroppedSignal, packet, &details); + if (inProgress) + firstDataDiscarded(header); if (!inProgress) delete packet; } @@ -200,8 +215,22 @@ void Hcf::forEachChild(cVisitor *v) void Hcf::handleMessage(cMessage *msg) { - if (msg == dataLifetimeTimer) + if (msg == addbaResponseTimer) { + std::vector> expired; + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) { + if (entry.second.responseDeadline <= simTime()) + expired.push_back(entry.first); + } + for (const auto& id : expired) + beginTeardown(id.first, id.second, true, 39); + scheduleAddbaResponseTimer(); + updateContention(); + } + else if (msg == dataLifetimeTimer) { expireDataFrames(); + updateAgreementProcedures(); + updateContention(); + } else if (msg == startRxTimer) { if (!isReceptionInProgress()) { frameSequenceHandler->handleStartRxTimeout(); @@ -209,8 +238,19 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == inactivityTimer) { if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) { - originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); - recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + std::vector> expired; + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + if (entry.second->getIsAddbaResponseReceived() && entry.second->getExpirationTime() <= simTime()) + expired.push_back({entry.first.first, entry.first.second, true}); + } + for (const auto& entry : recipientBlockAckAgreementHandler->getAgreements()) { + if (entry.second->getExpirationTime() <= simTime()) + expired.push_back({entry.first.first, entry.first.second, false}); + } + for (const auto& entry : expired) + beginTeardown(std::get<0>(entry), std::get<1>(entry), std::get<2>(entry), 39); + scheduleInactivityTimer(); + updateContention(); } else throw cRuntimeError("Unknown event"); @@ -255,7 +295,8 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetType() == ST_DATA_WITH_QOS) edca->getLifetimeHandler()->registerFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + updateAgreementProcedures(); + if (!pendingQueue->isEmpty() && !handlingAgreementAction && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -274,6 +315,7 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, Enter_Method("%s", cComponent::getSignalName(signalID)); auto packet = check_and_cast(obj); edca->getLifetimeHandler()->forgetFrame(packet->getId()); + actionTerminated(packet, false); if (packet->findTag() != nullptr) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); @@ -290,10 +332,463 @@ void Hcf::scheduleStartRxTimer(simtime_t timeout) scheduleAfter(timeout, startRxTimer); } -void Hcf::scheduleInactivityTimer(simtime_t timeout) +void Hcf::scheduleInactivityTimer() { Enter_Method("scheduleInactivityTimer"); - rescheduleAfter(timeout, inactivityTimer); + // IEEE Std 802.11-2024, 11.5.4 requires inactivity expiration. HCF reads each role's absolute deadline. + simtime_t deadline = SIMTIME_MAX; + if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) + deadline = std::min(originatorBlockAckAgreementHandler->computeEarliestExpirationTime(), + recipientBlockAckAgreementHandler->computeEarliestExpirationTime()); + cancelEvent(inactivityTimer); + if (deadline != SIMTIME_MAX) + scheduleAt(std::max(simTime(), deadline), inactivityTimer); +} + +void Hcf::scheduleAddbaResponseTimer() +{ + simtime_t deadline = SIMTIME_MAX; + if (originatorBlockAckAgreementHandler) { + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) + deadline = std::min(deadline, entry.second.responseDeadline); + } + cancelEvent(addbaResponseTimer); + if (deadline != SIMTIME_MAX) + scheduleAt(std::max(simTime(), deadline), addbaResponseTimer); +} + +bool Hcf::isProfileAllowed(MacAddress peer, bool originator) const +{ + auto state = mib->findPeerHtState(peer); + bool peerHt = state && state->valid && (originator ? state->negotiatedCapabilities.localTxPeerRx.valid : + state->negotiatedCapabilities.localRxPeerTx.valid); + return originator ? originatorBlockAckAgreementPolicy->isProfileAllowed(blockAckVariant, mib->isHtOperationSupported(), peerHt) : + recipientBlockAckAgreementPolicy->isProfileAllowed(blockAckVariant, mib->isHtOperationSupported(), peerHt); +} + +bool Hcf::hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const +{ + if (finishingSequence || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + for (int i = 0; i < context->getNumSteps(); i++) { + auto step = dynamic_cast(context->getStep(i)); + if (!step) + continue; + auto packet = step->getFrameToTransmit(); + if (!obsoleteActions.count(packet->getId())) + continue; + auto header = packet->peekAtFront(); + if (header->getReceiverAddress() != peer) + continue; + if (auto request = dynamicPtrCast(header)) { + if (originator && request->getTid() == tid) + return true; + } + else if (auto response = dynamicPtrCast(header)) { + if (!originator && response->getTid() == tid) + return true; + } + else if (auto delba = dynamicPtrCast(header)) { + if (delba->getInitiator() == originator && delba->getTid() == tid) + return true; + } + } + return false; +} + +bool Hcf::isFrameEligible(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); + }; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + 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::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) +{ + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto edcaf = edca->getEdcaf(static_cast(ac)); + auto frames = edcaf->getInProgressFrames(); + std::vector discard; + for (int i = 0; i < frames->getLength(); i++) { + auto packet = frames->getFrames(i); + auto data = dynamicPtrCast(packet->peekAtFront()); + bool transmitted = data && edcaf->getAckHandler()->getQoSDataAckStatus(data) != QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED; + if (data && frameSequenceHandler->isSequenceRunning()) { + auto context = frameSequenceHandler->getContext(); + for (int j = 0; j < context->getNumSteps(); j++) { + if (auto step = dynamic_cast(context->getStep(j))) + transmitted |= step->getFrameToTransmit() == packet; + } + } + // IEEE Std 802.11-2024, 11.5.3.4: the next MPDU must follow the highest transmitted sequence. + // Include unsent fragments even if ACK processing already removed an earlier fragment. + // Local allocation indices preserve order across gaps and wire sequence wraparound. + if (data && data->getReceiverAddress() == peer && data->getTid() == tid && + (all || transmitted || (highestTransmittedDataIndex >= 0 && + packet->getTag()->getAllocationIndex() <= highestTransmittedDataIndex))) + discard.push_back(packet); + } + for (auto packet : discard) { + auto data = packet->peekAtFront(); + edcaf->getAckHandler()->dropFrame(data); + edcaf->getRecoveryProcedure()->dropFrame(data); + frames->dropFrame(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + } + if (all) { + auto queue = edcaf->getPendingQueue(); + discard.clear(); + for (int i = 0; i < queue->getNumPackets(); i++) { + auto packet = queue->getPacket(i); + auto data = dynamicPtrCast(packet->peekAtFront()); + if (data && data->getReceiverAddress() == peer && data->getTid() == tid) + discard.push_back(packet); + } + for (auto packet : discard) { + queue->removePacket(packet); + take(packet); + edca->getLifetimeHandler()->forgetFrame(packet->getId()); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) + frames->clearDroppedFrames(); + } +} + +void Hcf::firstDataDiscarded(const Ptr& header) +{ + if (!originatorBlockAckAgreementHandler) + return; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived() && !agreement->getFirstDataTransmitted() && + agreement->getStartingSequenceNumber() == header->getSequenceNumber()) + beginTeardown(header->getReceiverAddress(), header->getTid(), true, 37); +} + +void Hcf::invalidateActions(MacAddress peer, Tid tid, bool originator) +{ + std::set invalid; + std::set attempted; + std::set rtsRecoveryOwned; + for (auto it = ownedActions.begin(); it != ownedActions.end();) { + if (it->second.peer == peer && it->second.tid == tid && it->second.originator == originator) { + invalid.insert(it->first); + if (it->second.attempted) + attempted.insert(it->first); + if (it->second.rtsRecoveryOwned) + rtsRecoveryOwned.insert(it->first); + obsoleteActions.insert(it->first); + it = ownedActions.erase(it); + } + else + ++it; + } + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + auto edcaf = edca->getEdcaf(static_cast(ac)); + auto queue = edcaf->getPendingQueue(); + std::vector removed; + for (int i = 0; i < queue->getNumPackets(); i++) { + if (invalid.count(queue->getPacket(i)->getId())) + removed.push_back(queue->getPacket(i)); + } + for (auto packet : removed) { + queue->removePacket(packet); + take(packet); + obsoleteActions.erase(packet->getId()); + delete packet; + } + auto frames = edcaf->getInProgressFrames(); + removed.clear(); + for (int i = 0; i < frames->getLength(); i++) { + if (invalid.count(frames->getFrames(i)->getId())) + removed.push_back(frames->getFrames(i)); + } + for (auto packet : removed) { + auto header = packet->peekAtFront(); + edcaf->getAckHandler()->dropFrame(header); + if (attempted.count(packet->getId())) + edcaf->getMgmtAndNonQoSRecoveryProcedure()->dropFrame(packet, header); + if (rtsRecoveryOwned.count(packet->getId())) + edcaf->getMgmtAndNonQoSRecoveryProcedure()->dropRtsProtectedFrame(header); + frames->dropFrame(packet); + if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) + obsoleteActions.erase(packet->getId()); + } + if (!frameSequenceHandler->isSequenceRunning() || frameSequenceHandler->getContext()->getInProgressFrames() != frames) + frames->clearDroppedFrames(); + } +} + +void Hcf::retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset) +{ + if (originator) { + auto agreement = originatorBlockAckAgreementHandler->detachAgreement(peer, tid); + invalidateActions(peer, tid, originator); + discardAgreementData(peer, tid, associationReset, agreement ? agreement->getHighestTransmittedDataIndex() : -1); + if (agreement) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement); + delete agreement; + } + } + else { + auto agreement = recipientBlockAckAgreementHandler->detachAgreement(peer, tid); + invalidateActions(peer, tid, originator); + if (agreement) { + sendUp(recipientDataService->retireAgreement(agreement, associationReset)); + emit(blockAckAgreementDeletedSignal, agreement); + delete agreement; + } + } + scheduleInactivityTimer(); +} + +void Hcf::beginTeardown(MacAddress peer, Tid tid, bool originator, int reasonCode) +{ + if (!originatorBlockAckAgreementHandler || !recipientBlockAckAgreementHandler) + return; + if (originator) { + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + auto it = procedures.find({peer, tid}); + if (it != procedures.end() && it->second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING) + return; + originatorBlockAckAgreementHandler->beginTeardown(peer, tid); + } + else { + if (recipientBlockAckAgreementHandler->isTeardownPending(peer, tid)) + return; + recipientBlockAckAgreementHandler->beginTeardown(peer, tid); + } + retireAgreement(peer, tid, originator); + auto delba = makeShared(); + delba->setReceiverAddress(peer); + delba->setTid(tid); + delba->setInitiator(originator); + delba->setReasonCode(reasonCode); + processMgmtFrame(new Packet("Delba", delba), delba); + scheduleAddbaResponseTimer(); +} + +void Hcf::actionTerminated(Packet *packet, bool acknowledged) +{ + auto it = ownedActions.find(packet->getId()); + if (it == ownedActions.end()) + return; + auto action = it->second; + ownedActions.erase(it); + if (action.rtsRecoveryOwned) + edca->getEdcaf(AC_VO)->getMgmtAndNonQoSRecoveryProcedure()->dropRtsProtectedFrame(packet->peekAtFront()); + if (action.kind == OwnedAction::REQUEST) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(action.peer, action.tid); + if (!agreement || agreement->getIsAddbaResponseReceived()) + return; + if (acknowledged) + originatorBlockAckAgreementHandler->acknowledgeRequest(action.peer, action.tid, + originatorBlockAckAgreementPolicy->computeAddbaFailureTimeout()); + else if (action.attempted) + // INET recovery policy confirms peer teardown before another setup. + beginTeardown(action.peer, action.tid, true, 37); + else { + originatorBlockAckAgreementHandler->finishProcedure(action.peer, action.tid); + retireAgreement(action.peer, action.tid, true); + } + } + else if (action.kind == OwnedAction::DELBA) { + if (action.originator) { + if (acknowledged) + originatorBlockAckAgreementHandler->finishProcedure(action.peer, action.tid); + else { + // INET recovery policy discards this batch after failed DELBA delivery. + originatorBlockAckAgreementHandler->failTeardown(action.peer, action.tid); + discardAgreementData(action.peer, action.tid, true); + } + } + else + recipientBlockAckAgreementHandler->finishTeardown(action.peer, action.tid); + } + scheduleAddbaResponseTimer(); + if (action.kind == OwnedAction::DELBA || (action.kind == OwnedAction::REQUEST && !acknowledged)) { + updateAgreementProcedures(); + updateContention(); + } +} + +void Hcf::resetPeer(MacAddress peer) +{ + 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::processLowerFrame(Packet *packet, const Ptr& header) @@ -302,6 +797,33 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& take(packet); EV_INFO << "Processing lower frame: " << packet->getName() << endl; auto edcaf = edca->getChannelOwner(); + auto action = dynamicPtrCast(header); + bool blockAckAction = dynamicPtrCast(header) || + dynamicPtrCast(header) || dynamicPtrCast(header); + if (action && blockAckAction && isForUs(header) && edcaf && frameSequenceHandler->isSequenceRunning() && + dynamic_cast(frameSequenceHandler->getContext()->getLastStep())) { + handlingAgreementAction = true; + emit(packetReceivedFromPeerSignal, packet); + std::unique_ptr responseSource(packet->dup()); + auto outgoing = check_and_cast(frameSequenceHandler->getContext()->getStepBeforeLast())->getFrameToTransmit(); + auto accepted = recipientDataService->managementFrameReceived(packet, action); + for (auto frame : accepted) { + take(frame); + dispatchAgreementAction(frame->peekAtFront()); + } + cancelEvent(startRxTimer); + bool resolved = obsoleteActions.count(outgoing->getId()) || + (dynamicPtrCast(outgoing->peekAtFront()) && + !edcaf->getInProgressFrames()->containsFrame(outgoing)); + frameSequenceHandler->interruptFrameSequence(!resolved); + recipientAckProcedure->processReceivedFrame(responseSource.get(), action, check_and_cast(recipientAckPolicy), this); + for (auto frame : accepted) + delete frame; + handlingAgreementAction = false; + updateAgreementProcedures(); + updateContention(); + return; + } if (edcaf && frameSequenceHandler->isSequenceRunning()) { // TODO always call processResponse? if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) { @@ -420,6 +942,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 +961,14 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) void Hcf::frameSequenceFinished() { Enter_Method("frameSequenceFinished"); + finishingSequence = true; emit(IFrameSequenceHandler::frameSequenceFinishedSignal, frameSequenceHandler->getContext()); auto edcaf = edca->getChannelOwner(); if (edcaf) { // The context still owns references. dropFrame defers deletion until context disposal. expireDataFrames(false); - bool startContention = hasFrameToTransmit(); // TODO outstanding frame + updateAgreementProcedures(); + bool startContention = !handlingAgreementAction && hasFrameToTransmit(); edcaf->releaseChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review edcaf->getTxopProcedure()->endTxop(); @@ -457,12 +983,51 @@ void Hcf::frameSequenceFinished() } else throw cRuntimeError("Frame sequence finished but channel owner not found!"); + obsoleteActions.clear(); + finishingSequence = false; } void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) { EV_INFO << "Processing received frame " << packet->getName() << " as recipient.\n"; emit(packetReceivedFromPeerSignal, packet); + if (recipientBlockAckAgreementHandler) { + // IEEE Std 802.11-2024, 11.5.4: valid Block Ack traffic without an agreement requires DELBA. + // Reject malformed DATA and unsupported BAR fields before this recovery path. + if (auto data = dynamicPtrCast(header)) { + if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK && + (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified() || + data->getTransmitterAddress().isUnspecified() || data->getTransmitterAddress().isMulticast())) { + delete packet; + return; + } + if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK && + !recipientBlockAckAgreementHandler->getAgreement(data->getTid(), data->getTransmitterAddress())) { + beginTeardown(data->getTransmitterAddress(), data->getTid(), false, 38); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + return; + } + } + else if (auto bar = dynamicPtrCast(header)) { + if (bar->getFragmentNumber() != 0 || bar->getMultiTid() || bar->getCompressedBitmap() || + bar->getTidInfo() < 0 || bar->getTidInfo() > 15 || !bar->getStartingSequenceNumber().isSpecified() || + bar->getTransmitterAddress().isUnspecified() || bar->getTransmitterAddress().isMulticast()) { + delete packet; + return; + } + auto agreement = recipientBlockAckAgreementHandler->getAgreement(bar->getTidInfo(), bar->getTransmitterAddress()); + if (!agreement) { + beginTeardown(bar->getTransmitterAddress(), bar->getTidInfo(), false, 38); + delete packet; + return; + } + agreement->calculateExpirationTime(); + scheduleInactivityTimer(); + } + } if (auto dataOrMgmtHeader = dynamicPtrCast(header)) recipientAckProcedure->processReceivedFrame(packet, dataOrMgmtHeader, check_and_cast(recipientAckPolicy), this); if (auto dataHeader = dynamicPtrCast(header)) { @@ -471,8 +1036,19 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const PtrdataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { - sendUp(recipientDataService->managementFrameReceived(packet, mgmtHeader)); - recipientProcessReceivedManagementFrame(mgmtHeader); + auto accepted = recipientDataService->managementFrameReceived(packet, mgmtHeader); + for (auto frame : accepted) { + take(frame); + auto completeHeader = frame->peekAtFront(); + if (dynamicPtrCast(completeHeader)) { + recipientProcessReceivedManagementFrame(completeHeader); + delete frame; + } + else + sendUp({frame}); + } + updateAgreementProcedures(); + updateContention(); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -496,35 +1072,76 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header) +{ + dispatchAgreementAction(header); +} + +bool Hcf::dispatchAgreementAction(const Ptr& header) { if (recipientBlockAckAgreementHandler && originatorBlockAckAgreementHandler) { if (auto addbaRequest = dynamicPtrCast(header)) { + auto peer = addbaRequest->getTransmitterAddress(); + auto tid = addbaRequest->getTid(); + auto previous = recipientBlockAckAgreementHandler->getAgreement(tid, peer); + if (!isProfileAllowed(peer, false) || hasObsoleteActionInFlight(peer, tid, false)) { + auto response = makeShared(); + response->setReceiverAddress(peer); + response->setTid(tid); + response->setDialogToken(addbaRequest->getDialogToken()); + response->setStatusCode(SC_UNSPECIFIED); + processMgmtFrame(new Packet("AddbaResponse", response), response); + return false; + } recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); - emit(blockAckAgreementAddedSignal, agreement); + if (!previous && agreement) + emit(blockAckAgreementAddedSignal, agreement); + scheduleInactivityTimer(); } else if (auto addbaResp = dynamicPtrCast(header)) { + auto peer = addbaResp->getTransmitterAddress(); + auto tid = addbaResp->getTid(); + auto pending = originatorBlockAckAgreementHandler->getAgreement(peer, tid); + if (!pending || pending->getIsAddbaResponseReceived() || pending->getDialogToken() != addbaResp->getDialogToken()) + return false; + if (addbaResp->getStatusCode() != SC_SUCCESSFUL) { + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + retireAgreement(peer, tid, true); + scheduleAddbaResponseTimer(); + return true; + } + if (!isProfileAllowed(peer, true) || !originatorBlockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, pending)) + return false; originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); auto agreement = originatorBlockAckAgreementHandler->getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + invalidateActions(peer, tid, true); emit(blockAckAgreementAddedSignal, agreement); + scheduleAddbaResponseTimer(); + if (!hasQueuedData(peer, tid) && !hasNumberedData(peer, tid)) + beginTeardown(peer, tid, true, 37); + return true; } else if (auto delba = dynamicPtrCast(header)) { - if (delba->getInitiator()) { - auto agreement = recipientBlockAckAgreementHandler->getAgreement(delba->getTid(), delba->getReceiverAddress()); - emit(blockAckAgreementDeletedSignal, agreement); - recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); - } - else { - auto agreement = originatorBlockAckAgreementHandler->getAgreement(delba->getReceiverAddress(), delba->getTid()); - emit(blockAckAgreementDeletedSignal, agreement); - originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy); - } + auto peer = delba->getTransmitterAddress(); + auto tid = delba->getTid(); + bool originator = !delba->getInitiator(); + if (tid < 0 || tid > 15 || peer.isMulticast() || peer.isUnspecified()) + return false; + if (originator) + originatorBlockAckAgreementHandler->finishProcedure(peer, tid); + else + recipientBlockAckAgreementHandler->finishTeardown(peer, tid); + retireAgreement(peer, tid, originator); + scheduleAddbaResponseTimer(); + return true; } else ; // Beacon, etc } else ; // Optional modules + return false; } void Hcf::transmissionComplete(Packet *packet, const Ptr& header) @@ -550,6 +1167,8 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) Enter_Method("originatorProcessRtsProtectionFailed"); auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); + if (obsoleteActions.count(packet->getId()) || !edcaf->getInProgressFrames()->containsFrame(packet)) + return; if (edcaf) { EV_INFO << "RTS frame transmission failed\n"; bool retryLimitReached = false; @@ -559,6 +1178,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 +1199,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 +1216,13 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) void Hcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + auto edcaf = edca->getChannelOwner(); + if (edcaf) + edcaf->emit(packetSentToPeerSignal, packet); + if (obsoleteActions.count(packet->getId()) || + (dynamicPtrCast(packet->peekAtFront()) && + !edca->getChannelOwner()->getInProgressFrames()->containsFrame(packet))) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; auto transmittedHeader = packet->peekAtFront(); if (auto dataOrMgmt = dynamicPtrCast(transmittedHeader)) { @@ -599,9 +1231,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 +1260,14 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); + if (originatorBlockAckAgreementHandler && dataHeader->getType() == ST_DATA_WITH_QOS && + !dataHeader->getReceiverAddress().isMulticast() && isProfileAllowed(dataHeader->getReceiverAddress(), true) && + originatorBlockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && + !originatorBlockAckAgreementHandler->getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid())) { + originatorBlockAckAgreementHandler->noteEligibleDataTransmission(dataHeader->getReceiverAddress(), dataHeader->getTid()); + if (hasQueuedData(dataHeader->getReceiverAddress(), dataHeader->getTid())) + originatorBlockAckAgreementHandler->beginSetupIntent(dataHeader->getReceiverAddress(), dataHeader->getTid()); + } if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } @@ -670,6 +1308,9 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) Enter_Method("originatorProcessFailedFrame"); auto failedHeader = failedPacket->peekAtFront(); auto edcaf = edca->getChannelOwner(); + if (obsoleteActions.count(failedPacket->getId()) || + (dynamicPtrCast(failedHeader) && !edcaf->getInProgressFrames()->containsFrame(failedPacket))) + return; if (edcaf) { bool retryLimitReached = false; if (auto dataHeader = dynamicPtrCast(failedHeader)) { @@ -713,6 +1354,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 +1374,12 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) void Hcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTransmittedPacket) { Enter_Method("originatorProcessReceivedFrame"); - EV_INFO << "Processing received frame " << receivedPacket->getName() << " as originator in frame sequence.\n"; emit(packetReceivedFromPeerSignal, receivedPacket); + if (obsoleteActions.count(lastTransmittedPacket->getId()) || + (dynamicPtrCast(lastTransmittedPacket->peekAtFront()) && + !edca->getChannelOwner()->getInProgressFrames()->containsFrame(lastTransmittedPacket))) + return; + EV_INFO << "Processing received frame " << receivedPacket->getName() << " as originator in frame sequence.\n"; auto receivedHeader = receivedPacket->peekAtFront(); auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); auto step = check_and_cast(frameSequenceHandler->getContext()->getStepBeforeLast()); @@ -786,6 +1432,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetInProgressFrames()->dropFrame(lastTransmittedPacket); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { + actionTerminated(lastTransmittedPacket, true); FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, lastTransmittedPacket, &transmissionDetails); @@ -833,9 +1480,15 @@ void Hcf::originatorProcessReceivedDataFrame(const PtrgetEdcaf(ac); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return edcaf->getInProgressFrames()->hasEligibleInProgressFrames() || + check_and_cast(originatorDataService)->peekEligibleFrame(edcaf->getPendingQueue()) != nullptr; else throw cRuntimeError("Hcca is unimplemented"); } @@ -845,7 +1498,7 @@ bool Hcf::hasFrameToTransmit() expireDataFrames(); auto edcaf = edca->getChannelOwner(); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return hasFrameToTransmit(edcaf->getAccessCategory()); else throw cRuntimeError("Hcca is unimplemented"); } @@ -974,6 +1627,12 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) Enter_Method("transmitFrame"); auto channelOwner = edca->getChannelOwner(); if (channelOwner) { + auto header = packet->peekAtFront(); + if (dynamicPtrCast(header) && + !channelOwner->getInProgressFrames()->containsFrame(packet)) + return false; + if (!isFrameEligible(packet, true)) + return false; auto lifetimeHandler = edca->getLifetimeHandler(); auto step = frameSequenceHandler->getContext()->getLastStep(); auto protectedPacket = dynamic_cast(step); @@ -981,7 +1640,6 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) // IEEE Std 802.11-2024, 10.23.2.12.1: a delayed start cannot exceed DATA lifetime. if (lifetimeHandler->isExpired(data, simTime() + ifs)) return false; - auto header = packet->peekAtFront(); auto txop = channelOwner->getTxopProcedure(); auto context = frameSequenceHandler->getContext(); bool scopedSingle = txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && !par("isBlockAckSupported").boolValue(); @@ -1077,6 +1735,19 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) RateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; tx->transmitFrame(packet, packet->peekAtFront(), ifs, false, this); + if (auto dataHeader = dynamicPtrCast(header)) { + if (originatorBlockAckAgreementHandler) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived()) + agreement->recordTransmittedData(packet->getTag()->getAllocationIndex()); + } + } + auto action = ownedActions.find(packet->getId()); + if (action != ownedActions.end()) { + action->second.attempted = true; + if (action->second.kind == OwnedAction::REQUEST) + originatorBlockAckAgreementHandler->markRequestAttempted(action->second.peer, action->second.tid); + } return true; } else @@ -1124,6 +1795,37 @@ void Hcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const void Hcf::processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) { Enter_Method("processMgmtFrame"); + OwnedAction action; + action.peer = mgmtHeader->getReceiverAddress(); + bool owned = true; + if (auto request = dynamicPtrCast(mgmtHeader)) { + action.tid = request->getTid(); + action.originator = true; + action.kind = OwnedAction::REQUEST; + } + else if (auto response = dynamicPtrCast(mgmtHeader)) { + action.tid = response->getTid(); + action.originator = false; + action.kind = OwnedAction::RESPONSE; + } + else if (auto delba = dynamicPtrCast(mgmtHeader)) { + action.tid = delba->getTid(); + action.originator = delba->getInitiator(); + action.kind = OwnedAction::DELBA; + bool pending = action.originator ? + originatorBlockAckAgreementHandler->getProcedures().count({action.peer, action.tid}) != 0 && + originatorBlockAckAgreementHandler->getProcedures().at({action.peer, action.tid}).state == IOriginatorBlockAckAgreementHandler::ProcedureState::TEARDOWN_PENDING : + recipientBlockAckAgreementHandler->isTeardownPending(action.peer, action.tid); + if (!pending) { + beginTeardown(action.peer, action.tid, action.originator, delba->getReasonCode()); + delete mgmtPacket; + return; + } + } + else + owned = false; + if (owned) + ownedActions.emplace(mgmtPacket->getId(), action); mgmtPacket->insertAtBack(makeShared()); processUpperFrame(mgmtPacket, mgmtHeader); } @@ -1167,6 +1869,7 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + cancelAndDelete(addbaResponseTimer); cancelAndDelete(dataLifetimeTimer); cancelAndDelete(startRxTimer); cancelAndDelete(inactivityTimer); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 4a0537880f8..aaa5c20cad6 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -12,6 +12,7 @@ #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" #include "inet/linklayer/ieee80211/mac/contract/ICtsPolicy.h" @@ -41,6 +42,7 @@ namespace inet { namespace ieee80211 { class Ieee80211Mac; +class Ieee80211Mib; /** * Implements IEEE 802.11 Hybrid Coordination Function. @@ -48,7 +50,6 @@ class Ieee80211Mac; class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public IBlockAckAgreementHandlerCallback, public ModeSetListener { private: - enum class BlockAckVariant { BASIC, COMPRESSED }; BlockAckVariant blockAckVariant = BlockAckVariant::BASIC; public: @@ -58,11 +59,26 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: protected: Ieee80211Mac *mac = nullptr; + Ieee80211Mib *mib = nullptr; IRateControl *dataAndMgmtRateControl = nullptr; cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; cMessage *dataLifetimeTimer = nullptr; + cMessage *addbaResponseTimer = nullptr; + struct OwnedAction { + MacAddress peer; + Tid tid = -1; + bool originator = false; + enum Kind { REQUEST, RESPONSE, DELBA } kind = REQUEST; + bool attempted = false; + bool rtsRecoveryOwned = false; + }; + std::map ownedActions; + std::set obsoleteActions; + bool handlingAgreementAction = false; + bool finishingSequence = false; + bool updatingProcedures = false; // Transmission and Reception IRx *rx = nullptr; @@ -113,6 +129,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -131,10 +148,27 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: TxopProcedure *txop, simtime_t ppduDuration); virtual bool hasFrameToTransmit(); virtual bool hasFrameToTransmit(AccessCategory ac); + bool hasEligibleFrameToTransmit(AccessCategory ac) const; virtual bool isReceptionInProgress(); void scheduleDataLifetimeTimer(); void expireDataFrames(bool preserveSequence = true); std::set getReferencedDataFrames() const; + bool isFrameEligible(Packet *packet, bool numbered) const; + bool isAMsduPermitted(Packet *packet) const; + bool hasQueuedData(MacAddress peer, Tid tid) const; + bool hasNumberedData(MacAddress peer, Tid tid) const; + bool hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const; + void updateAgreementProcedures(); + void updateContention(); + void scheduleAddbaResponseTimer(); + void actionTerminated(Packet *packet, bool acknowledged); + void invalidateActions(MacAddress peer, Tid tid, bool originator); + void retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset = false); + void beginTeardown(MacAddress peer, Tid tid, bool originator, int reasonCode); + void discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex = -1); + void firstDataDiscarded(const Ptr& header); + bool dispatchAgreementAction(const Ptr& header); + bool isProfileAllowed(MacAddress peer, bool originator) const; // Recipient virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); @@ -176,12 +210,13 @@ 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; diff --git a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc index 88395deef09..098aad01b1c 100644 --- a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc +++ b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.cc @@ -12,6 +12,24 @@ namespace inet { namespace ieee80211 { +void QoSDuplicateRemoval::setLastReceived(MacAddress peer, Tid tid, const SequenceNumberCyclic& sequence, int fragment) +{ + if (sequence.isSpecified()) + lastSeenSeqNumCache.insert_or_assign(Key(peer, tid), SequenceControlField(sequence.get(), fragment)); +} + +void QoSDuplicateRemoval::clearPeer(MacAddress peer) +{ + for (auto it = lastSeenSeqNumCache.begin(); it != lastSeenSeqNumCache.end();) { + if (it->first.first == peer) + it = lastSeenSeqNumCache.erase(it); + else + ++it; + } + lastSeenSharedSeqNumCache.erase(peer); + lastSeenTimePriorityManagementSeqNumCache.erase(peer); +} + bool QoSDuplicateRemoval::isDuplicate(const Ptr& header) { SequenceControlField seqVal(header->getSequenceNumber().get(), header->getFragmentNumber()); diff --git a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h index d1075215fbc..4a947d4a9f5 100644 --- a/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h +++ b/src/inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h @@ -28,6 +28,8 @@ class INET_API QoSDuplicateRemoval : public IDuplicateRemoval Mac2SeqValMap lastSeenTimePriorityManagementSeqNumCache; public: + void setLastReceived(MacAddress peer, Tid tid, const SequenceNumberCyclic& sequence, int fragment); + void clearPeer(MacAddress peer); virtual bool isDuplicate(const Ptr& header) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 2f32f0207b9..ce397baca9e 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -26,8 +26,10 @@ Packet *BasicReassembly::addFragment(Packet *packet) // Frame is not fragmented if (!header->getMoreFragments() && header->getFragmentNumber() == 0) return packet; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) + // IEEE Std 802.11-2024, 10.4: dispatch Block Ack Actions only after complete MMPDU reassembly. + bool blockAckAction = dynamicPtrCast(header) || + dynamicPtrCast(header) || dynamicPtrCast(header); + if (dynamicPtrCast(header) && !blockAckAction) return packet; // find entry for this frame Key key; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index 66699fadeb0..cc6450101f3 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -14,6 +14,17 @@ namespace inet { namespace ieee80211 { +void FrameSequenceHandler::interruptFrameSequence(bool notifyFailure) +{ + ASSERT(isSequenceRunning()); + ASSERT(dynamic_cast(context->getLastStep())); + context->getLastStep()->setCompletion(IFrameSequenceStep::Completion::REJECTED); + if (notifyFailure) + abortFrameSequence(); + else + finishFrameSequence(); +} + void FrameSequenceHandler::handleStartRxTimeout() { auto lastStep = context->getLastStep(); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index de514c54c9d..fa65175856a 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -35,6 +35,7 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual void processResponse(Packet *frame) override; virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; + virtual void interruptFrameSequence(bool notifyFailure) override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } virtual ~FrameSequenceHandler(); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 60c875bb16f..3fda1fcd52c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -150,7 +150,13 @@ IFrameSequenceStep *ManagementAckFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + Packet *packet = nullptr; + if (context->getQoSContext() && dynamic_cast(context->getLastStep())) { + if (auto rts = dynamic_cast(context->getStepBeforeLast())) + packet = const_cast(rts->getProtectedFrame()); + } + if (packet == nullptr) + packet = context->getInProgressFrames()->getFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: { @@ -195,7 +201,13 @@ IFrameSequenceStep *ManagementFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + Packet *packet = nullptr; + if (context->getQoSContext() && dynamic_cast(context->getLastStep())) { + if (auto rts = dynamic_cast(context->getStepBeforeLast())) + packet = const_cast(rts->getProtectedFrame()); + } + if (packet == nullptr) + packet = context->getInProgressFrames()->getFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 4074ff0004c..2b77ca61c30 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -133,20 +133,25 @@ void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header) { EV_WARN << "Retry limit reached for " << *packet << ".\n"; - auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); - if (packet->getByteLength() >= rtsThreshold) { - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { - auto it = shortRetryCounter.find(id); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + dropFrame(packet, header); emit(retryLimitReachedSignal, packet); } +void NonQosRecoveryProcedure::dropFrame(Packet *packet, const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + if (packet->getByteLength() >= rtsThreshold) + longRetryCounter.erase(id); + else + shortRetryCounter.erase(id); +} + +void NonQosRecoveryProcedure::dropRtsProtectedFrame(const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + shortRetryCounter.erase(id); +} + // // The SRC for an MPDU of type Data or MMPDU and the SSRC shall be incremented every // time transmission of a MAC frame of length less than or equal to dot11RTSThreshold diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h index f738e56dde6..83c04085b1b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h @@ -65,6 +65,8 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); virtual void retryLimitReached(Packet *packet, const Ptr& header); + void dropFrame(Packet *packet, const Ptr& header); + void dropRtsProtectedFrame(const Ptr& header); virtual int getLongRetryLimit() { return longRetryLimit; } virtual int getShortRetryLimit() { return shortRetryLimit; } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 484f239cb09..b99e0a3339b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -98,6 +98,8 @@ AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const PtrgetIsAddbaResponseReceived() && header->getAMsduPresent() && !agreement->isAMsduPermitted()) + throw cRuntimeError("A-MSDU is not permitted by both ADDBA flags"); if (agreement->getIsAddbaResponseReceived() && isBlockAckPolicyEligibleFrame(packet, header)) { if (checkAgreementPolicy(header, agreement)) return AckPolicy::BLOCK_ACK; @@ -116,7 +118,7 @@ bool OriginatorQosAckPolicy::isBlockAckPolicyEligibleFrame(Packet *packet, const bool OriginatorQosAckPolicy::checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const { bool bufferFull = agreement->getBufferSize() == agreement->getNumSentBaPolicyFrames(); - bool aMsduOk = agreement->getIsAMsduSupported() || !header->getAMsduPresent(); + bool aMsduOk = agreement->isAMsduPermitted() || !header->getAMsduPresent(); // TODO bool baPolicy = agreement->getIsDelayedBlockAckPolicySupported() || !frame->getAckPolicy(); return !bufferFull && aMsduOk && (header->getSequenceNumber() >= agreement->getStartingSequenceNumber()); // TODO && baPolicy } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index ef82c225c78..40f7e33106c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -9,11 +9,13 @@ #include +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" #include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" +#include "inet/queueing/contract/ISelectivePacketCollection.h" namespace inet { namespace ieee80211 { @@ -33,9 +35,29 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } -Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue) +Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected) { auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); + if (subframes) { + bool valid = !subframes->empty() && subframes->front() == selected; + std::set unique; + for (auto frame : *subframes) { + bool present = false; + for (int i = 0; i < pendingQueue->getNumPackets(); i++) { + if (pendingQueue->getPacket(i) == frame) { + present = true; + break; + } + } + valid &= present && unique.insert(frame).second && + (!extractionEligible || extractionEligible(frame)) && + (!aMsduPermitted || aMsduPermitted(frame)); + } + if (!valid) { + delete subframes; + return nullptr; + } + } if (subframes) { std::vector sourceIds; AccessCategory ac = AC_BE; @@ -105,16 +127,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,6 +149,8 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu assignSequenceNumber(header); packet->insertAtFront(header); } + if (dynamicPtrCast(packet->peekAtFront())) + packet->addTagIfAbsent()->setAllocationIndex(nextDataAllocationIndex++); // if (msduIntegrityAndProtection) // frame = protectMsduIfNeeded(frame); std::vector *fragments = nullptr; @@ -143,6 +171,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..40366a32b9c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -41,16 +41,26 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p IMpduAggregation *aMpduAggregation = nullptr; EdcaTransmitLifetimeHandler *lifetimeHandler = nullptr; // Borrowed from EDCA. std::function expireFrames; + std::function extractionEligible; + std::function aMsduPermitted; + int64_t nextDataAllocationIndex = 0; protected: virtual void initialize() override; virtual void assignSequenceNumber(const Ptr& header); virtual std::vector *fragmentIfNeeded(Packet *frame); - virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue); + virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected); virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); public: + void setEligibility(std::function eligible, std::function permission) { + extractionEligible = eligible; + aMsduPermitted = permission; + } + Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const; + SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; + bool hasAMsduAggregation() const { return aMsduAggregationPolicy != nullptr; } virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 76838835cce..e9918ee20f7 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -63,6 +63,7 @@ bool InProgressFrames::containsFrame(const Packet *packet) const bool InProgressFrames::isEligibleToTransmit(Packet *packet) { return (!lifetimeHandler || !lifetimeHandler->isExpired(packet)) && + (!transmissionEligible || transmissionEligible(packet)) && ackHandler->isEligibleToTransmit(packet->peekAtFront()); } diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index ca7175ee2d5..293aebfc3a3 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -32,6 +32,7 @@ class INET_API InProgressFrames : public SimpleModule EdcaTransmitLifetimeHandler *lifetimeHandler = nullptr; // Borrowed from EDCA; null for DCF. std::vector inProgressFrames; std::vector droppedFrames; + std::function transmissionEligible; protected: virtual void initialize(int stage) override; @@ -43,6 +44,8 @@ class INET_API InProgressFrames : public SimpleModule public: virtual ~InProgressFrames(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler) { lifetimeHandler = handler; } + void setTransmissionEligibility(std::function predicate) { transmissionEligible = predicate; } + bool hasEligibleInProgressFrames() { return hasEligibleFrameToTransmit(); } virtual std::string str() const override; virtual void forEachChild(cVisitor *v) override; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 95fac972e3a..e73a15720c1 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -57,6 +57,11 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data { Enter_Method("dataFrameReceived"); take(dataPacket); + if (blockAckAgreementHandler) { + auto agreement = blockAckAgreementHandler->getAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); + if (agreement) + agreement->recordReceived(dataHeader); + } // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; @@ -116,20 +121,17 @@ std::vector RecipientQosMacDataService::managementFrameReceived(Packet Enter_Method("managementFrameReceived"); take(mgmtPacket); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption - if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) + if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { + delete mgmtPacket; return std::vector(); + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting - if (dynamicPtrCast(mgmtHeader)) { - delete mgmtPacket; + if (mgmtPacket == nullptr) return std::vector(); - } - else - return std::vector({ mgmtPacket }); + return std::vector({ mgmtPacket }); } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -193,6 +195,49 @@ RecipientQosMacDataService::~RecipientQosMacDataService() delete blockAckReordering; } +std::vector RecipientQosMacDataService::retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) +{ + Enter_Method("retireAgreement"); + auto peer = agreement->getBlockAckRecord()->getOriginatorAddress(); + auto tid = agreement->getBlockAckRecord()->getTid(); + auto duplicate = check_and_cast(duplicateRemoval); + // IEEE Std 802.11-2024, 11.5.3.5: ordinary teardown retains the highest received sequence number. + if (!associationReset) + duplicate->setLastReceived(peer, tid, agreement->getHighestReceivedSequence(), agreement->getHighestReceivedFragment()); + std::vector discarded; + auto released = blockAckReordering->detachAgreement(peer, tid, !associationReset, discarded); + for (auto packet : discarded) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + std::vector complete; + for (const auto& unit : released) { + auto frame = defragment(unit.second); + if (!frame) + continue; + if (frame->peekAtFront()->getAMsduPresent()) { + emit(packetDeaggregatedSignal, frame); + auto subframes = aMsduDeaggregation->deaggregateFrame(frame); + complete.insert(complete.end(), subframes->begin(), subframes->end()); + delete subframes; + } + else + complete.push_back(frame); + } + return complete; +} + +void RecipientQosMacDataService::clearPeer(MacAddress peer) +{ + Enter_Method("clearPeer"); + blockAckReordering->clearPeer(peer); + check_and_cast(duplicateRemoval)->clearPeer(peer); + for (int tid = -1; tid < 16; tid++) + basicReassembly->purge(peer, tid, 0, 4095); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index e4b8ee28865..10b26ca843f 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -46,6 +46,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, virtual Packet *defragment(Packet *mgmtFragment); public: + virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) override; + virtual void clearPeer(MacAddress peer) override; virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; diff --git a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc index c0d2085cf3d..8f5f1340436 100644 --- a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc +++ b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.cc @@ -10,6 +10,12 @@ namespace inet { namespace ieee80211 { +SequenceNumberCyclic QoSSequenceNumberAssignment::getNextSequenceNumber(MacAddress peer, Tid tid) const +{ + auto it = lastSentSeqNums.find({peer, tid}); + return it == lastSentSeqNums.end() ? SequenceNumberCyclic(0) : it->second + 1; +} + QoSSequenceNumberAssignment::CacheType QoSSequenceNumberAssignment::getCacheType(const Ptr& header, bool incoming) { bool isTimePriorityFrame = false; // TODO diff --git a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h index 07928c860b5..81cb584ace8 100644 --- a/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h +++ b/src/inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h @@ -36,6 +36,7 @@ class INET_API QoSSequenceNumberAssignment : public ISequenceNumberAssignment virtual CacheType getCacheType(const Ptr& header, bool incoming); public: + SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; virtual void assignSequenceNumber(const Ptr& header) override; }; diff --git a/src/inet/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/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 3ae180b8557..88293c812bb 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -7,7 +7,7 @@ # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;c2ed-b352/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, dd36-1076/tplx;b703-3ef2/~tNl;e07d-2980/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, b80d-4ae8/tplx;72b7-86c2/~tNl;d385-62e0/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 5fbb-9fd9/tplx;c0a4-97f6/~tNl;2373-df01/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 2f2d-73b1/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 2c80-6844/tplx;4e9e-cf86/~tNl;ea3d-e802/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -658,7 +658,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, d929-5913/tplx;f602-4424/~tNl;f2fb-4d98/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 0473-f9d2/tplx;e92c-7352/~tNl;317d-aa4f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, e07a-4c5f/tplx;9f6c-7889/~tNl;29ea-169c/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, fbab-b84e/tplx;47cb-b9e5/~tNl;c4f3-fa00/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f8a4-9474/tplx;1cba-cccc/~tNl;538e-67df/~tND;08b5-d005/tyf, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6f09-d8eb/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 572d1330c90..836d42b5183 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -199,9 +199,9 @@ /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;7b7c-2bfb/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 1082-899c/tplx;0068-9ae9/~tNl;e0b3-3193/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 2235-516d/tplx;7b51-e67c/~tNl;9a3f-afc7/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 8bfb-6ac9/tplx;5647-2cd1/~tNl;481a-ec9d/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, b828-6bd6/tplx;026f-2c6d/~tNl;2cd4-4795/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 021a-d29a/tplx;1597-ec6a/~tNl;feff-7e88/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 5b9e-801f/tplx;8731-2504/~tNl;9724-d1ce/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 8df2-40a3/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, c99f-354d/tplx;8e10-a3f0/~tNl;ac4b-3ba1/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 4228-9743/tplx;8610-abcd/~tNl;68c2-19f0/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, f509-65f1/tplx;6b3a-0d65/~tNl;c474-7174/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 6150-7a8b/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb5-d354/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 4ffd-4fbf/tplx;3a89-2021/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, f58d-d2fd/tplx;38f7-f642/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 diff --git a/tests/module/Ieee80211BlockAckActionDelivery_1.test b/tests/module/Ieee80211BlockAckActionDelivery_1.test new file mode 100644 index 00000000000..b2ac3745b86 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDelivery_1.test @@ -0,0 +1,70 @@ +%description: +Check complete Block Ack Action delivery through the production recipient data service. +An original Action reaches HCF. Its MAC retry does not reach the agreement handler. +A new Action with another sequence number remains eligible for dispatch. +Two management fragments produce one complete Action after the final fragment. + +%file: ActionDelivery.cc +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" + +namespace inet::ieee80211 { +class ActionDeliveryTest : public cSimpleModule { + protected: + void initialize() override { + scheduleAt(SIMTIME_ZERO, new cMessage("test")); + } + void handleMessage(cMessage *message) override { + delete message; + auto service = check_and_cast(getModuleByPath("^.recipient")); + for (int index = 0; index < 6; index++) { + auto header = makeShared(); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); + header->setSequenceNumber(SequenceNumberCyclic(index < 2 ? 10 : index == 2 ? 11 : 12)); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + header->setDialogToken(37); + header->setTid(5); + header->setBufferSize(8); + header->setBlockAckPolicy(true); + header->setRetry(index == 1 || index == 5); + header->setMoreFragments(index == 3); + header->setFragmentNumber(index >= 4 ? 1 : 0); + auto packet = new Packet("action", header); + packet->insertAtBack(makeShared(B(2))); + packet->insertAtBack(makeShared()); + auto frames = service->managementFrameReceived(packet, header); + ASSERT(frames.size() == (index == 0 || index == 2 || index == 4 ? 1 : 0)); + for (auto frame : frames) { + ASSERT(frame->peekAtFront() != nullptr); + take(frame); + delete frame; + } + } + std::cout << "Block Ack Action delivery assertions passed.\n"; + } +}; +Define_Module(ActionDeliveryTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +simple ActionDeliveryTest { parameters: @class(::inet::ieee80211::ActionDeliveryTest); } +network ActionDeliveryNetwork { + submodules: + recipient: RecipientQosMacDataService; + test: ActionDeliveryTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDeliveryNetwork +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Block Ack Action delivery assertions passed. diff --git a/tests/module/Ieee80211BlockAckActionDispatch_1.test b/tests/module/Ieee80211BlockAckActionDispatch_1.test new file mode 100644 index 00000000000..1ec9db351e9 --- /dev/null +++ b/tests/module/Ieee80211BlockAckActionDispatch_1.test @@ -0,0 +1,178 @@ +%description: +Check duplicate Block Ack Actions through initialized production HCF. +The test observes response submission without a wireless transmission. +HCF must acknowledge a MAC retry without a second agreement dispatch. + +The deadline check derives 5 ms from an agreement created at 1 ms with a 4 ms timeout. + +%extraargs: -r 0..2 + +%file: ActionDispatch.cc +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ActionDispatchHcf : public Hcf { + public: + int dispatches = 0; + int acknowledgments = 0; + int responses = 0; + int delivered = 0; + Ptr response; + void checkAgreementDeadline() { + Enter_Method("checkAgreementDeadline"); + receiveRequest(false); + auto expectedDeadline = SimTime(5, SIMTIME_MS); + ASSERT(recipientBlockAckAgreementHandler->computeEarliestExpirationTime() == expectedDeadline); + scheduleInactivityTimer(); + ASSERT(inactivityTimer->getArrivalTime() == expectedDeadline); + cancelEvent(inactivityTimer); + } + void receiveRequest(bool retry) { + Enter_Method("receiveRequest"); + auto header = makeShared(); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + header->setReceiverAddress(mac->getAddress()); + header->setSequenceNumber(SequenceNumberCyclic(10)); + header->setRetry(retry); + header->setTid(5); + header->setDialogToken(37); + header->setBlockAckPolicy(true); + header->setBufferSize(8); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("request", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode( + modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + } + void checkNegotiatedWindow() { + Enter_Method("checkNegotiatedWindow"); + receiveRequest(false); + ASSERT(response != nullptr && response->getBufferSize() == 8); + auto agreement = recipientBlockAckAgreementHandler->getAgreement(5, MacAddress("02:00:00:00:00:02")); + ASSERT(agreement != nullptr && agreement->getBufferSize() == 8); + auto bar = makeShared(); + bar->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + bar->setReceiverAddress(mac->getAddress()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(104)); + bar->setFragmentNumber(0); + auto request = new Packet("bar-before-data", bar); + request->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(request, bar); + ASSERT(delivered == 0); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(data->getTransmitterAddress()); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(104)); + data->setAckPolicy(BLOCK_ACK); + auto packet = new Packet("first-data", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, data); + ASSERT(delivered == 1); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered++; + take(frame); + delete frame; + } + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + dispatches++; + Hcf::recipientProcessReceivedManagementFrame(header); + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + acknowledgments++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + responses++; + response = dynamicPtrCast(header); + delete packet; + } +}; +Define_Module(ActionDispatchHcf); +class ActionDispatchTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + if (par("scenario").intValue() == 1) + hcf->checkAgreementDeadline(); + else if (par("scenario").intValue() == 2) + hcf->checkNegotiatedWindow(); + else { + hcf->receiveRequest(false); + hcf->receiveRequest(true); + ASSERT(hcf->acknowledgments == 2); + ASSERT(hcf->dispatches == 1); + ASSERT(hcf->responses == 1); + ASSERT(hcf->response != nullptr && hcf->response->getStatusCode() == SC_SUCCESSFUL); + ASSERT(hcf->response->getDialogToken() == 37); + ASSERT(hcf->response->getBufferSize() == 64); + ASSERT(hcf->response->getBlockAckTimeoutValue() == SimTime(4, SIMTIME_MS)); + } + std::cout << "Block Ack Action dispatch assertions passed.\n"; + } +}; +Define_Module(ActionDispatchTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ActionDispatchHcf extends Hcf { parameters: @class(::inet::ieee80211::ActionDispatchHcf); } +simple ActionDispatchTest { parameters: int scenario; @class(::inet::ieee80211::ActionDispatchTest); } +network ActionDispatchNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ActionDispatchTest; +} + +%inifile: omnetpp.ini +[General] +network = ActionDispatchNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +*.test.scenario = ${scenario=0,1,2} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ActionDispatchHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.blockAckTimeoutValue = 4ms +**.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.maximumAllowedBufferSize = ${scenario} == 2 ? 8 : 64 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack Action dispatch assertions passed. diff --git a/tests/module/Ieee80211BlockAckActionDisposal_1.test b/tests/module/Ieee80211BlockAckActionDisposal_1.test new file mode 100644 index 00000000000..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/Ieee80211BlockAckDataSelection_1.test b/tests/module/Ieee80211BlockAckDataSelection_1.test new file mode 100644 index 00000000000..d1b8c948262 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDataSelection_1.test @@ -0,0 +1,313 @@ +%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)); + 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..89da8e14084 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -0,0 +1,393 @@ +%description: +Check Basic setup deferral through wireless DATA, ADDBA, ACK, and DELBA exchanges. +The receiver holds its actual response queue until 150 ms after request ACK. +The 100 ms default must expire; a 250 ms timeout must accept the delayed response. +Other runs check first-DATA numbering, deferred voice versus eligible video, and genuine collision. +Request delays of 100 ms and 450 ms reverse the response deadline and DATA lifetime order. +Newer DATA must preserve the response deadline after the original DATA expires. +RTS failure and lifetime expiry must retire an agreement before its first DATA transmission. +Spaced DATA must start setup before later extraction after an earlier eligible DATA completes. +The same rule applies when greedy A-MSDU aggregation empties the earlier queue. +All runs use seed 0 and zero contention windows. + +%extraargs: -r 0..11 + +%file: Deferral.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" + +using namespace inet; +using namespace inet::ieee80211; + +class DeferralHcf : public Hcf { + public: + int requests = 0; + int delbas = 0; + int dataVoice = 0; + int dataVideo = 0; + int dataAfterRequest = 0; + int aggregatesAccepted = 0; + int requestStart = -1; + int firstData = -1; + simtime_t requestAccepted = SIMTIME_MAX; + simtime_t requestAck = SIMTIME_MAX; + simtime_t observedDeadline = SIMTIME_MAX; + simtime_t responseQueued = SIMTIME_MAX; + simtime_t responseAccepted = SIMTIME_MAX; + simtime_t responseReceived = SIMTIME_MAX; + bool holdResponse = false; + bool holdRequest = false; + bool dropProtectedCts = false; + int droppedCts = 0; + int firstRtsSequence = -1; + int rtsFailures = 0; + bool dataEligible(Packet *frame) const { return isFrameEligible(frame, false); } + bool hasAgreement(Tid tid) const { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(MacAddress("02:00:00:00:00:02"), tid); + return agreement && agreement->getIsAddbaResponseReceived(); + } + bool deferred(Tid tid) const { + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + return procedures.count({MacAddress("02:00:00:00:00:02"), tid}) != 0; + } + bool contending(AccessCategory ac) const { + return check_and_cast(edca->getEdcaf(ac)->getSubmodule("contention"))->isContentionInProgress(); + } + void processMgmtFrame(Packet *frame, const Ptr& header) override { + if (dynamicPtrCast(header) && responseQueued == SIMTIME_MAX) { + responseQueued = simTime(); + if (holdResponse) + rx->setContentionBlocked(true); + } + Hcf::processMgmtFrame(frame, header); + } + protected: + void transmitControlResponseFrame(Packet *response, const Ptr& responseHeader, + Packet *received, const Ptr& receivedHeader) override { + if (dropProtectedCts && droppedCts == 0 && responseAccepted != SIMTIME_MAX && dynamicPtrCast(receivedHeader)) { + droppedCts++; + delete response; + return; + } + Hcf::transmitControlResponseFrame(response, responseHeader, received, receivedHeader); + } + void originatorProcessRtsProtectionFailed(Packet *frame) override { + if (requestStart >= 0) { + auto data = dynamicPtrCast(frame->peekAtFront()); + if (data && data->getTid() == 6) { + rtsFailures++; + if (firstRtsSequence < 0) + firstRtsSequence = data->getSequenceNumber().get(); + } + } + Hcf::originatorProcessRtsProtectionFailed(frame); + } + void frameSequenceFinished() override { + Hcf::frameSequenceFinished(); + if (holdRequest && requestAccepted == SIMTIME_MAX && deferred(6)) + rx->setContentionBlocked(true); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + take(frame); + delete frame; + } + } + bool transmitFrame(Packet *frame, simtime_t ifs) override { + auto header = frame->peekAtFront(); + bool accepted = Hcf::transmitFrame(frame, ifs); + if (!accepted) + return false; + if (auto request = dynamicPtrCast(header)) { + requests++; + if (requestStart < 0) { + requestStart = request->getStartingSequenceNumber().get(); + requestAccepted = simTime(); + ASSERT(observedDeadline == SIMTIME_MAX); + } + } + if (dynamicPtrCast(header)) + delbas++; + if (dynamicPtrCast(header) && responseAccepted == SIMTIME_MAX) + responseAccepted = simTime(); + if (auto data = dynamicPtrCast(header)) { + if (data->getAMsduPresent()) aggregatesAccepted++; + if (data->getTid() == 6) dataVoice++; + if (data->getTid() == 5) dataVideo++; + if (requestStart >= 0 && data->getTid() == 6) { + if (firstData < 0) firstData = data->getSequenceNumber().get(); + dataAfterRequest++; + } + } + return true; + } + void originatorProcessReceivedControlFrame(Packet *frame, const Ptr& header, + Packet *last, const Ptr& lastHeader, AccessCategory ac) override { + bool request = dynamicPtrCast(lastHeader) != nullptr; + Hcf::originatorProcessReceivedControlFrame(frame, header, last, lastHeader, ac); + if (request && requestAck == SIMTIME_MAX) { + requestAck = simTime(); + auto it = originatorBlockAckAgreementHandler->getProcedures().find({MacAddress("02:00:00:00:00:02"), 6}); + if (it != originatorBlockAckAgreementHandler->getProcedures().end()) + observedDeadline = it->second.responseDeadline; + } + } + void recipientProcessReceivedManagementFrame(const Ptr& header) override { + if (dynamicPtrCast(header) && responseReceived == SIMTIME_MAX) + responseReceived = simTime(); + Hcf::recipientProcessReceivedManagementFrame(header); + } +}; +Define_Module(DeferralHcf); + +class DeferralTest : public cSimpleModule, public cListener { + int collisions = 0; + int videoRetryDrops = 0; + int lifetimeDrops = 0; + protected: + void receiveSignal(cComponent *, simsignal_t, unsigned long value, cObject *) override { collisions += value; } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + auto drop = check_and_cast(details); + auto header = dynamicPtrCast(check_and_cast(object)->peekAtFront()); + if (drop->getReason() == RETRY_LIMIT_REACHED && header && header->getTid() == 5) + videoRetryDrops++; + if (drop->getReason() == LIFETIME_EXPIRED) + lifetimeDrops++; + } + Packet *accept(DeferralHcf *hcf, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(header->getReceiverAddress()); + auto frame = new Packet(tid == 6 ? "voice-data" : "video-data", header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + hcf->processUpperFrame(frame, header); + return frame; + } + void activity() override { + auto sender = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf")); + auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); + auto senderRx = check_and_cast(getModuleByPath("^.sender.wlan[0].mac.rx")); + auto receiverRx = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.rx")); + int scenario = par("scenario"); + sender->subscribe(Hcf::edcaCollisionDetectedSignal, this); + sender->subscribe(packetDroppedSignal, this); + senderRx->setContentionBlocked(true); + if (scenario >= 10) { + auto edca = check_and_cast(sender->getSubmodule("edca")); + accept(sender, 6); + if (scenario == 11) + accept(sender, 6); + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + ASSERT(sender->dataVoice == 1); + ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 1 : 0)); + ASSERT(sender->requests == 0); + ASSERT(edca->getEdcaf(AC_VO)->getPendingQueue()->isEmpty()); + ASSERT(edca->getEdcaf(AC_VO)->getInProgressFrames()->getLength() == 0); + auto later = accept(sender, 6); + bool deferredBeforeExtraction = !sender->dataEligible(later); + if (scenario == 11) + accept(sender, 6); + std::cout << "Later DATA admission at " << simTime() << ": deferred=" << deferredBeforeExtraction << ".\n"; + wait(SimTime(5, SIMTIME_MS)); + std::cout << "Spaced DATA: requests=" << sender->requests << ", DATA accepted=" << sender->dataVoice + << ", aggregates=" << sender->aggregatesAccepted << ", request start=" << sender->requestStart + << ", first DATA=" << sender->firstData << ".\n"; + ASSERT(deferredBeforeExtraction); + ASSERT(sender->requests == 1 && sender->hasAgreement(6)); + ASSERT(sender->requestStart == 1 && sender->firstData == sender->requestStart); + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 2 : 0)); + } + else if (scenario == 4) { + accept(sender, 6); + accept(sender, 5); + ASSERT(sender->contending(AC_VO) && sender->contending(AC_VI)); + senderRx->setContentionBlocked(false); + wait(SimTime(50, SIMTIME_US)); + ASSERT(sender->dataVoice == 1 && sender->dataVideo == 0); + ASSERT(collisions == 1 && videoRetryDrops == 1); + } + else { + receiver->holdResponse = scenario != 2 && scenario < 8; + receiver->dropProtectedCts = scenario >= 8; + sender->holdRequest = scenario >= 5 && scenario != 8; + accept(sender, 6); + accept(sender, 6); + auto deferredFrame = accept(sender, 6); + auto deferredId = deferredFrame->getId(); + ASSERT(sender->contending(AC_VO)); + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + if (scenario >= 5 && scenario != 8) { + ASSERT(sender->requests == 0 && sender->requestAck == SIMTIME_MAX); + ASSERT(sender->deferred(6)); + auto release = SimTime(scenario == 9 ? 500 : scenario == 5 ? 100 : 450, SIMTIME_MS); + wait(release - simTime()); + sender->holdRequest = false; + senderRx->setContentionBlocked(false); + wait(SimTime(5, SIMTIME_MS)); + } + std::cout << "Requests=" << sender->requests << ", start=" << sender->requestStart << ", voice DATA=" << sender->dataVoice << ".\n"; + ASSERT(sender->requests == 1); + ASSERT(sender->requestStart == sender->dataVoice - sender->dataAfterRequest); + ASSERT(sender->requestAck != SIMTIME_MAX); + ASSERT(sender->requestAck > sender->requestAccepted); + auto timeout = SimTime(scenario == 1 ? 250 : 100, SIMTIME_MS); + ASSERT(sender->observedDeadline == sender->requestAck + timeout); + if (scenario >= 8) { + if (scenario == 9) + wait(SimTime(560, SIMTIME_MS) - simTime()); + ASSERT(sender->rtsFailures >= 1); + ASSERT(sender->firstRtsSequence == sender->requestStart); + ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); + ASSERT(!sender->hasAgreement(6)); + ASSERT(sender->delbas == 1); + ASSERT(lifetimeDrops == (scenario == 9 ? 1 : 0)); + } + else if (scenario >= 5) { + auto edca = check_and_cast(sender->getSubmodule("edca")); + auto lifetime = edca->getLifetimeHandler(); + ASSERT(lifetime->getDeadline(deferredFrame) == SimTime(512, SIMTIME_MS)); + if (scenario == 5) { + ASSERT(sender->observedDeadline < SimTime(512, SIMTIME_MS)); + wait(sender->observedDeadline + SimTime(5, SIMTIME_MS) - simTime()); + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 1); + ASSERT(lifetimeDrops == 0); + } + else { + ASSERT(sender->observedDeadline > SimTime(512, SIMTIME_MS)); + if (scenario == 7) { + wait(SimTime(480, SIMTIME_MS) - simTime()); + accept(sender, 6); + } + wait(SimTime(513, SIMTIME_MS) - simTime()); + ASSERT(lifetimeDrops == 1 && lifetime->findFrame(deferredId) == nullptr); + if (scenario == 6) + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 0); + else { + ASSERT(sender->delbas == 0 && sender->deferred(6)); + wait(sender->observedDeadline + SimTime(5, SIMTIME_MS) - simTime()); + ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 1); + } + } + std::cout << "Request release=" << SimTime(scenario == 5 ? 100 : 450, SIMTIME_MS) << ", request ACK=" << sender->requestAck + << ", response deadline=" << sender->observedDeadline << ", original DATA deadline=0.512.\n"; + } + else if (scenario == 2) { + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->firstData == sender->requestStart); + } + else { + ASSERT(sender->deferred(6)); + ASSERT(sender->dataAfterRequest == 0); + ASSERT(deferredFrame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); + ASSERT(!sender->contending(AC_VO)); + std::cout << "The pre-setup VO request resolved. Deferred VO has no contention request at " << simTime() << ".\n"; + if (scenario == 3) { + accept(sender, 5); + wait(SimTime(5, SIMTIME_MS)); + ASSERT(sender->dataVideo == 1); + ASSERT(sender->dataAfterRequest == 0); + ASSERT(collisions == 0 && videoRetryDrops == 0); + } + else { + wait(sender->requestAck + SimTime(150, SIMTIME_MS) - simTime()); + receiverRx->setContentionBlocked(false); + wait(SimTime(10, SIMTIME_MS)); + if (scenario == 1) { + ASSERT(receiver->responseAccepted != SIMTIME_MAX); + ASSERT(receiver->responseAccepted >= sender->requestAck + SimTime(150, SIMTIME_MS)); + } + std::cout << "Request ACK=" << sender->requestAck << ", response deadline=" << sender->observedDeadline + << ", response queued=" << receiver->responseQueued << ", response Tx accepted=" << receiver->responseAccepted << ".\n"; + if (scenario == 0) { + ASSERT(sender->delbas >= 1); + ASSERT(!sender->hasAgreement(6)); + } + else { + ASSERT(sender->delbas == 0); + ASSERT(sender->hasAgreement(6)); + ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->firstData == sender->requestStart); + } + } + } + } + sender->unsubscribe(Hcf::edcaCollisionDetectedSignal, this); + sender->unsubscribe(packetDroppedSignal, this); + std::cout << "Block Ack deferral scenario " << scenario << " passed.\n"; + endSimulation(); + } + public: + DeferralTest() : cSimpleModule(65536) {} +}; +Define_Module(DeferralTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.blockack.OriginatorBlockAckAgreementPolicy; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module DeferralHcf extends Hcf { parameters: @class(::DeferralHcf); } +simple LongTimeoutPolicy extends OriginatorBlockAckAgreementPolicy { parameters: addbaFailureTimeout = 250ms; } +simple DeferralTest { parameters: int scenario; @class(::DeferralTest); } +network DeferralNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sender: WirelessHost; + receiver: WirelessHost; + test: DeferralTest; +} + +%inifile: omnetpp.ini +[General] +network = DeferralNetwork +sim-time-limit = 1s +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0..11} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "DeferralHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} != 4 +**.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.typename = ${policy="OriginatorBlockAckAgreementPolicy","LongTimeoutPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy" ! scenario} +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = ${aggregation="","","","","","","","","","","","BasicMsduAggregationPolicy" ! scenario} +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = ${scenario} >= 8 && ${scenario} <= 9 ? 1B : 2346B +**.wlan[*].mac.hcf.rtsPolicy.ctsTimeout = ${scenario} == 9 ? 50ms : -1s +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = ${scenario} == 9 ? 10 : 1 +*.sender.wlan[0].address = "02:00:00:00:00:01" +*.receiver.wlan[0].address = "02:00:00:00:00:02" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sender.mobility.initialX = 0m +*.sender.mobility.initialY = 0m +*.receiver.mobility.initialX = 1m +*.receiver.mobility.initialY = 0m + +%contains: stdout +Block Ack deferral scenario 11 passed. diff --git a/tests/module/Ieee80211BlockAckDeferredCollision_1.test b/tests/module/Ieee80211BlockAckDeferredCollision_1.test new file mode 100644 index 00000000000..8bc3eef8be4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckDeferredCollision_1.test @@ -0,0 +1,203 @@ +%description: +Check a pending EDCA request after production inactivity teardown defers its DATA. +The fixture supplies an accepted Basic agreement; it does not test negotiation. +VI and BE request the same grant time, at 34 microseconds. +The VI agreement expires at 33 microseconds and queues DELBA in VO. +VO uses six AIFS slots and grants later, at 70 microseconds. +Deferred VI must not cause collision feedback or disposal of eligible BE DATA. +The control retains eligible VI and must preserve the genuine collision. +The third run gives BE an accepted agreement with SSN zero and two DATA frames. +An internal collision discards sequence zero before transmission. +HCF must retire that agreement and queue DELBA before a successor can transmit. + +%extraargs: -r 0..2 + +%file: DeferredCollision.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" + +using namespace inet; +using namespace inet::ieee80211; + +class DeferredCollisionOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(simtime_t timeout, Tid tid) { + MacAddress peer("02:00:00:00:00:02"); + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(0), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; + +class DeferredCollisionHcf : public Hcf { + public: + int videoAccepted = 0; + int bestEffortAccepted = 0; + int delbas = 0; + void prepare(bool expire, Tid tid) { + Enter_Method("prepare"); + delete originatorBlockAckAgreementHandler; + auto handler = new DeferredCollisionOriginator(); + originatorBlockAckAgreementHandler = handler; + handler->accept(expire ? SimTime(33, SIMTIME_US) : SIMTIME_ZERO, tid); + if (expire) + scheduleInactivityTimer(); + } + bool contending(AccessCategory ac) const { + return check_and_cast(edca->getEdcaf(ac)->getSubmodule("contention"))->isContentionInProgress(); + } + bool eligible(AccessCategory ac) { return hasFrameToTransmit(ac); } + int queued(AccessCategory ac) const { return edca->getEdcaf(ac)->getPendingQueue()->getNumPackets(); } + bool hasAgreement(Tid tid) const { + return originatorBlockAckAgreementHandler->getAgreement(MacAddress("02:00:00:00:00:02"), tid) != nullptr; + } + protected: + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbas++; + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *frame, simtime_t ifs) override { + auto header = dynamicPtrCast(frame->peekAtFront()); + bool accepted = Hcf::transmitFrame(frame, ifs); + if (accepted && header) { + if (header->getTid() == 5) videoAccepted++; + if (header->getTid() == 0) bestEffortAccepted++; + } + return accepted; + } +}; +Define_Module(DeferredCollisionHcf); + +class DeferredCollisionTest : public cSimpleModule, public cListener { + int collisions = 0; + int bestEffortRetryDrops = 0; + protected: + void receiveSignal(cComponent *, simsignal_t, unsigned long count, cObject *) override { collisions += count; } + void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + auto drop = check_and_cast(details); + auto header = dynamicPtrCast(check_and_cast(object)->peekAtFront()); + if (drop->getReason() == RETRY_LIMIT_REACHED && header && header->getTid() == 0) + bestEffortRetryDrops++; + } + void accept(DeferredCollisionHcf *hcf, Tid tid) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + header->setAddress3(header->getReceiverAddress()); + auto frame = new Packet(tid == 5 ? "video-data" : "best-effort-data", header); + frame->insertAtBack(makeShared(B(100))); + frame->insertAtBack(makeShared()); + hcf->processUpperFrame(frame, header); + } + void activity() override { + auto hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + int scenario = par("scenario"); + bool expire = scenario == 0; + Tid agreementTid = scenario == 2 ? 0 : 5; + hcf->prepare(expire, agreementTid); + hcf->subscribe(Hcf::edcaCollisionDetectedSignal, this); + hcf->subscribe(packetDroppedSignal, this); + accept(hcf, 5); + accept(hcf, 0); + if (scenario == 2) + accept(hcf, 0); + ASSERT(hcf->contending(AC_VI) && hcf->contending(AC_BE)); + wait(SimTime(33500, SIMTIME_NS)); + ASSERT(hcf->hasAgreement(agreementTid) == !expire); + ASSERT(hcf->eligible(AC_VI) == !expire); + ASSERT(hcf->eligible(AC_BE)); + std::cout << "At " << simTime() << ": VI eligible=" << hcf->eligible(AC_VI) + << ", VI pending request=" << hcf->contending(AC_VI) + << ", BE pending request=" << hcf->contending(AC_BE) + << ", VO pending request=" << hcf->contending(AC_VO) << ".\n"; + wait(SimTime(16500, SIMTIME_NS)); + std::cout << "At " << simTime() << ": VI DATA accepted=" << hcf->videoAccepted + << ", BE DATA accepted=" << hcf->bestEffortAccepted + << ", collisions=" << collisions << ", BE retry drops=" << bestEffortRetryDrops << ".\n"; + ASSERT(hcf->videoAccepted == (expire ? 0 : 1)); + ASSERT(hcf->bestEffortAccepted == (expire ? 1 : 0)); + ASSERT(collisions == (expire ? 0 : 1)); + ASSERT(bestEffortRetryDrops == (expire ? 0 : 1)); + if (scenario == 2) { + ASSERT(!hcf->hasAgreement(0)); + ASSERT(hcf->delbas == 1); + ASSERT(!hcf->eligible(AC_BE)); + ASSERT(hcf->queued(AC_BE) == 1); + ASSERT(hcf->bestEffortAccepted == 0); + std::cout << "First-DATA collision retires the agreement before successor transmission.\n"; + } + hcf->unsubscribe(Hcf::edcaCollisionDetectedSignal, this); + hcf->unsubscribe(packetDroppedSignal, this); + std::cout << "Deferred collision scenario " << par("scenario").intValue() << " passed.\n"; + endSimulation(); + } + public: + DeferredCollisionTest() : cSimpleModule(65536) {} +}; +Define_Module(DeferredCollisionTest); + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module DeferredCollisionHcf extends Hcf { parameters: @class(::DeferredCollisionHcf); } +simple DeferredCollisionTest { parameters: int scenario; @class(::DeferredCollisionTest); } +network DeferredCollisionNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: DeferredCollisionTest; +} + +%inifile: omnetpp.ini +[General] +network = DeferredCollisionNetwork +sim-time-limit = 1ms +seed-set = 0 +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +*.test.scenario = ${scenario=0,1,2} +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "DeferredCollisionHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +*.host.wlan[0].mac.hcf.edca.edcaf[3].aifsn = 6 +**.wlan[*].mac.hcf.edca.edcaf[*].aifsn = 2 +**.wlan[*].mac.hcf.edca.edcaf[*].recoveryProcedure.shortRetryLimit = 1 +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Deferred collision scenario 1 passed. + +%contains: stdout +First-DATA collision retires the agreement before successor transmission. diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test new file mode 100644 index 00000000000..33429c70e6a --- /dev/null +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -0,0 +1,297 @@ +%description: +Check the sequence boundary after ordinary originator teardown of fragmented DATA. +IEEE Std 802.11-2024, 11.5.3.4 requires the next MPDU to use a later sequence +when the originator discards the last transmitted MPDU. +The fixture supplies accepted Basic agreements and injects immediate ACKs. +Production fragmentation, inactivity expiry, retirement, and transmission remain active. +Runs cover sequence 0 and wraparound at 4095, with expiry during transmission +or after the first fragment receives its Normal Ack. +Later intact DATA and an unrelated peer must retain their transmissions. +Additional runs transmit an earlier DATA unit before a gap in sequence allocation. +The fixture extracts and disposes of the skipped DATA through the production allocator. +Distances 2047, 2048, 2049, and 4096 cover the half-range boundary and a full wrap. + +%extraargs: -r 0..19 + +%file: FragmentRetirement.cc +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class FragmentRetirementOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, int start, simtime_t timeout) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; + +class FragmentRetirementHcf : public Hcf { + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress other = MacAddress("02:00:00:00:00:03"); + int start = 0; + int distance = 0; + bool acknowledgedFirst = false; + bool firstCompleted = false; + bool firstAcknowledged = false; + bool retired = false; + bool successorSent = false; + bool otherSent = false; + int delbas = 0; + + void skipData(int count) { + auto queue = edca->getEdcaf(AC_VI)->getPendingQueue(); + for (int i = 0; i < count; i++) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(5); + auto packet = new Packet("sequence-setup", header); + packet->insertAtBack(makeShared()); + queue->enqueuePacket(packet); + auto frames = originatorDataService->extractFramesToTransmit(queue); + for (auto frame : *frames) { + take(frame); + delete frame; + } + delete frames; + } + } + + void startRetirement() { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 5); + agreement->setStartingSequenceNumber(SequenceNumberCyclic(start)); + agreement->setBlockAckTimeoutValue(SimTime(1024, SIMTIME_US)); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(); + scheduleAfter(SimTime(acknowledgedFirst ? 650 : 974, SIMTIME_US), new cMessage("queue-data")); + } + + void queueData(const char *name, MacAddress address, int length) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(5); + header->setReceiverAddress(address); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(address); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(length))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + + public: + void begin() { + Enter_Method("begin"); + start = par("startSequence"); + distance = par("sequenceDistance"); + acknowledgedFirst = par("acknowledgedFirst"); + delete originatorBlockAckAgreementHandler; + auto handler = new FragmentRetirementOriginator(); + originatorBlockAckAgreementHandler = handler; + handler->accept(peer, start, SIMTIME_ZERO); + handler->accept(other, 0, SIMTIME_ZERO); + // Advance the production allocator without a radio exchange for each earlier sequence. + skipData(start); + subscribe(blockAckAgreementDeletedSignal, this); + if (distance == 0) + startRetirement(); + else { + // Force Normal Ack for the earlier DATA, independent of the fragment ACK policy. + handler->getAgreement(peer, 5)->setStartingSequenceNumber(SequenceNumberCyclic(start) + 1); + queueData("earlier-data", peer, 20); + } + scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verify")); + } + + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, object, details); + return; + } + ASSERT(!retired); + retired = true; + ASSERT(firstCompleted == acknowledgedFirst); + ASSERT(firstAcknowledged == acknowledgedFirst); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, 5) == nullptr); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + auto frames = edca->getEdcaf(AC_VI)->getInProgressFrames(); + for (int i = 0; i < frames->getLength(); i++) { + auto data = dynamicPtrCast(frames->getFrames(i)->peekAtFront()); + ASSERT(!data || data->getReceiverAddress() != peer || data->getSequenceNumber() != SequenceNumberCyclic(start)); + } + } + + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto data = dynamicPtrCast(packet->peekAtFront()); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && data) { + if (packet->isName("earlier-data")) { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start)); + ASSERT(data->getAckPolicy() == NORMAL_ACK); + return accepted; + } + if (data->getReceiverAddress() == peer) { + if (!retired) { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start)); + ASSERT(data->getFragmentNumber() == 0); + } + else { + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(start) + 1); + ASSERT(data->getFragmentNumber() == 0 && !data->getMoreFragments()); + successorSent = true; + } + } + else { + ASSERT(data->getReceiverAddress() == other); + ASSERT(data->getSequenceNumber() == SequenceNumberCyclic(0)); + otherSent = true; + } + } + return accepted; + } + + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto data = dynamicPtrCast(header); + bool earlier = packet->isName("earlier-data"); + bool first = !earlier && data && data->getReceiverAddress() == peer && data->getSequenceNumber() == SequenceNumberCyclic(start); + bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); + if (first) + firstCompleted = true; + if (dynamicPtrCast(header)) + delbas++; + Hcf::transmissionComplete(packet, header); + if (ack) + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage(earlier ? "ack-earlier" : first ? "ack-first" : "ack")); + } + + void handleMessage(cMessage *message) override { + if (message->isName("queue-data")) { + delete message; + queueData("fragmented-data", peer, 400); + queueData("intact-successor", peer, 20); + queueData("other-peer", other, 20); + } + else if (message->isName("ack") || message->isName("ack-first") || message->isName("ack-earlier")) { + bool first = message->isName("ack-first"); + bool earlier = message->isName("ack-earlier"); + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + processLowerFrame(packet, header); + if (first) + firstAcknowledged = true; + if (earlier) { + skipData(distance - 1); + start = (start + distance) % 4096; + startRetirement(); + } + } + else if (message->isName("verify")) { + delete message; + ASSERT(retired && firstCompleted && successorSent && otherSent); + ASSERT(delbas == 1); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + unsubscribe(blockAckAgreementDeletedSignal, this); + std::cout << "Fragment retirement passed: start=" << start << ", acknowledgedFirst=" << acknowledgedFirst + << ", distance=" << distance << std::endl; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(FragmentRetirementHcf); + +class FragmentRetirementDriver : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(FragmentRetirementDriver); +} + +%file: test.ned +import inet.node.inet.WirelessHost; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module FragmentRetirementHcf extends Hcf { + parameters: + int startSequence; + int sequenceDistance; + bool acknowledgedFirst; + @class(::inet::ieee80211::FragmentRetirementHcf); +} +simple FragmentRetirementDriver { parameters: @class(::inet::ieee80211::FragmentRetirementDriver); } +network FragmentRetirementNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + driver: FragmentRetirementDriver; +} + +%inifile: omnetpp.ini +[General] +network = FragmentRetirementNetwork +ned-path = .;../../../../src +sim-time-limit = 20ms +seed-set = 0 +cmdenv-express-mode = true +record-scalar-results = false +record-vector-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "FragmentRetirementHcf" +**.wlan[*].mac.hcf.startSequence = ${start=0,4095} +**.wlan[*].mac.hcf.sequenceDistance = ${distance=0,2047,2048,2049,4096} +**.wlan[*].mac.hcf.acknowledgedFirst = ${acked=false,true} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 200B +**.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = ${acked} ? 0B : 1000B +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 +**.wlan[*].mac.hcf.edca.edcaf[*].aifsn = 20 +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = 0s +*.host.wlan[0].address = "02:00:00:00:00:01" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Fragment retirement passed: start=0, acknowledgedFirst=0 +%contains: stdout +Fragment retirement passed: start=0, acknowledgedFirst=1 +%contains: stdout +Fragment retirement passed: start=4095, acknowledgedFirst=0 +%contains: stdout +Fragment retirement passed: start=4095, acknowledgedFirst=1 diff --git a/tests/module/Ieee80211BlockAckLifecycle_1.test b/tests/module/Ieee80211BlockAckLifecycle_1.test new file mode 100644 index 00000000000..eb6d073cafd --- /dev/null +++ b/tests/module/Ieee80211BlockAckLifecycle_1.test @@ -0,0 +1,358 @@ +%description: +Check Basic teardown through initialized HCF and production receive paths. Each role has agreements for two peers and two TIDs. Deletion callbacks inspect the selected agreement after its removal. Recipient teardown releases complete units behind a gap before the callback. Absent-agreement DATA and BAR cause recipient-role DELBA without agreement allocation. The fixture supplies accepted agreements; it does not test negotiation or wireless transmission. + +The inactivity timer uses the earliest absolute deadline from both agreement roles. The fixture checks that derived deadline and the later expiration of the opposite role. + +%extraargs: -r 0..9 + +%file: Lifecycle.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class LifecycleOriginator : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid, int start = 100, simtime_t timeout = SIMTIME_ZERO) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaRequestSent(true); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(timeout); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; +class LifecycleRecipient : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer, Tid tid, int start = 100, simtime_t timeout = SIMTIME_ZERO) { + auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), 64, timeout); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, tid}] = agreement; + } +}; +class LifecycleHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress other = MacAddress("02:00:00:00:00:03"); + std::vector delivered; + int deletions = 0; + int delbas = 0; + int controlResponses = 0; + bool selectedRecipient = false; + bool requireRelease = false; + bool strictOpposite = true; + bool dynamicRole = false; + void prepare(int start = 100, simtime_t originatorTimeout = SIMTIME_ZERO, simtime_t recipientTimeout = SIMTIME_ZERO) { + Enter_Method("prepare"); + delete originatorBlockAckAgreementHandler; + delete recipientBlockAckAgreementHandler; + auto originator = new LifecycleOriginator(); + auto recipient = new LifecycleRecipient(); + originatorBlockAckAgreementHandler = originator; + recipientBlockAckAgreementHandler = recipient; + for (auto address : {peer, other}) + for (Tid tid : {5, 6}) { + bool selected = address == peer && tid == 5; + originator->accept(address, tid, start, selected ? originatorTimeout : SIMTIME_ZERO); + recipient->accept(address, tid, start, selected ? recipientTimeout : SIMTIME_ZERO); + } + subscribe(blockAckAgreementDeletedSignal, this); + } + void checkInactivityDeadline(simtime_t expectedDeadline) { + Enter_Method("checkInactivityDeadline"); + scheduleInactivityTimer(); + ASSERT(inactivityTimer->isScheduled()); + ASSERT(inactivityTimer->getArrivalTime() == expectedDeadline); + } + bool hasInactivityTimer() const { return inactivityTimer->isScheduled(); } + void retireRecipientLocally() { + Enter_Method("retireRecipientLocally"); + selectedRecipient = true; + beginTeardown(peer, 5, false, 37); + } + bool hasOriginator(MacAddress address, Tid tid) { + return originatorBlockAckAgreementHandler->getAgreement(address, tid) != nullptr; + } + bool hasRecipient(MacAddress address, Tid tid) { + return recipientBlockAckAgreementHandler->getAgreement(tid, address) != nullptr; + } + void receiveDelba(bool initiator, int sequence) { + Enter_Method("receiveDelba"); + selectedRecipient = initiator; + auto header = makeShared(); + header->setTransmitterAddress(peer); + header->setReceiverAddress(mac->getAddress()); + header->setTid(5); + header->setInitiator(initiator); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + auto packet = new Packet("delba", header); + packet->addTag()->setMode( + physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + void receiveData(int sequence, const char *name, MacAddress address, bool incomplete = false) { + Enter_Method("receiveData"); + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(address); + header->setReceiverAddress(mac->getAddress()); + header->setAddress3(address); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setMoreFragments(incomplete); + auto packet = new Packet(name, header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + void receiveBar(MacAddress address, int fragment = 0) { + Enter_Method("receiveBar"); + auto header = makeShared(); + header->setTransmitterAddress(address); + header->setReceiverAddress(mac->getAddress()); + header->setTidInfo(5); + header->setFragmentNumber(fragment); + header->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("bar", header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, header); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *payload, cObject *details) override { + if (signal != blockAckAgreementDeletedSignal) { + Hcf::receiveSignal(source, signal, payload, details); + return; + } + if (requireRelease) + ASSERT((delivered == std::vector{"second", "third"})); + ASSERT(payload != nullptr); + if (dynamicRole) + selectedRecipient = dynamic_cast(payload) != nullptr; + if (selectedRecipient) { + auto agreement = check_and_cast(payload); + ASSERT(agreement->getBlockAckRecord()->getOriginatorAddress() == peer); + ASSERT(agreement->getBlockAckRecord()->getTid() == 5); + ASSERT(!hasRecipient(peer, 5)); + if (strictOpposite) + ASSERT(hasOriginator(peer, 5)); + ASSERT(agreement->getBufferSize() == 64); + } + else { + auto agreement = check_and_cast(payload); + ASSERT(agreement->getReceiverAddr() == peer); + ASSERT(agreement->getTid() == 5); + ASSERT(!hasOriginator(peer, 5)); + if (strictOpposite) + ASSERT(hasRecipient(peer, 5)); + ASSERT(agreement->getBufferSize() == 64); + } + ASSERT(hasOriginator(other, 5)); + ASSERT(hasRecipient(other, 5)); + ASSERT(hasOriginator(peer, 6)); + ASSERT(hasRecipient(peer, 6)); + deletions++; + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + if (dynamicPtrCast(packet->peekAtFront())) + delivered.push_back(packet->getName()); + take(packet); + delete packet; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + controlResponses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (auto delba = dynamicPtrCast(header)) { + if (!dynamicRole) + ASSERT(!delba->getInitiator()); + delbas++; + } + delete packet; + } +}; +Define_Module(LifecycleHcf); +class LifecycleTest : public cSimpleModule { + protected: + LifecycleHcf *hcf = nullptr; + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + if (message->isName("recipientExpiry")) { + delete message; + ASSERT(hcf->hasOriginator(hcf->peer, 5)); + ASSERT(!hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + ASSERT((hcf->delivered == std::vector{"second", "third"})); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + if (message->isName("firstExpiry")) { + delete message; + ASSERT(!hcf->hasOriginator(hcf->peer, 5)); + ASSERT(hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + scheduleAt(SimTime(5000001, SIMTIME_NS), new cMessage("secondExpiry")); + return; + } + if (message->isName("secondExpiry")) { + delete message; + ASSERT(!hcf->hasOriginator(hcf->peer, 5)); + ASSERT(!hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 2); + ASSERT(hcf->delbas == 2); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + if (message->isName("zeroExpiry")) { + delete message; + ASSERT(hcf->hasOriginator(hcf->peer, 5)); + ASSERT(hcf->hasRecipient(hcf->peer, 5)); + ASSERT(hcf->deletions == 0); + ASSERT(hcf->delbas == 0); + ASSERT(!hcf->hasInactivityTimer()); + std::cout << "Block Ack lifecycle assertions passed.\n"; + return; + } + delete message; + hcf = check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf")); + int scenario = par("scenario"); + hcf->prepare(scenario == 5 ? 4094 : 100, + scenario == 6 ? SimTime(2, SIMTIME_MS) : SIMTIME_ZERO, + scenario == 6 ? SimTime(4, SIMTIME_MS) : scenario == 9 ? SimTime(2, SIMTIME_MS) : SIMTIME_ZERO); + if (scenario >= 8) { + hcf->requireRelease = true; + hcf->selectedRecipient = true; + hcf->receiveData(101, "second", hcf->peer); + hcf->receiveData(102, "third", hcf->peer); + ASSERT(hcf->delivered.empty()); + if (scenario == 8) { + hcf->retireRecipientLocally(); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + hcf->retireRecipientLocally(); + ASSERT(hcf->deletions == 1); + ASSERT(hcf->delbas == 1); + std::cout << "Block Ack lifecycle assertions passed.\n"; + } + else { + hcf->checkInactivityDeadline(SimTime(3, SIMTIME_MS)); + scheduleAt(SimTime(3000001, SIMTIME_NS), new cMessage("recipientExpiry")); + } + return; + } + if (scenario >= 6) { + if (scenario == 6) { + hcf->strictOpposite = false; + hcf->dynamicRole = true; + hcf->checkInactivityDeadline(SimTime(3, SIMTIME_MS)); + scheduleAt(SimTime(3000001, SIMTIME_NS), new cMessage("firstExpiry")); + } + else + scheduleAt(SimTime(6, SIMTIME_MS), new cMessage("zeroExpiry")); + return; + } + if (scenario <= 2 || scenario == 5) { + if (scenario == 2 || scenario == 5) { + hcf->requireRelease = true; + if (scenario == 5) + hcf->receiveData(4094, "incomplete-frag0", hcf->peer, true); + hcf->receiveData(scenario == 5 ? 4095 : 101, "second", hcf->peer); + hcf->receiveData(scenario == 5 ? 0 : 102, "third", hcf->peer); + ASSERT(hcf->delivered.empty()); + } + hcf->receiveDelba(scenario != 0, 20); + ASSERT(hcf->deletions == 1); + hcf->receiveDelba(scenario != 0, 21); + ASSERT(hcf->deletions == 1); + if (scenario == 2 || scenario == 5) + ASSERT((hcf->delivered == std::vector{"second", "third"})); + } + else { + const MacAddress absent("02:00:00:00:00:04"); + if (scenario == 4) { + hcf->receiveBar(absent, 1); + ASSERT(hcf->delbas == 0); + ASSERT(hcf->controlResponses == 0); + } + if (scenario == 3) + hcf->receiveData(100, "absent", absent); + else + hcf->receiveBar(absent); + ASSERT(hcf->delivered.empty()); + ASSERT(hcf->delbas == 1); + ASSERT(hcf->controlResponses == 0); + ASSERT(!hcf->hasRecipient(absent, 5)); + ASSERT(!hcf->hasOriginator(absent, 5)); + if (scenario == 3) + hcf->receiveData(101, "absent-again", absent); + else + hcf->receiveBar(absent); + ASSERT(hcf->delbas == 1); + ASSERT(hcf->delivered.empty()); + } + std::cout << "Block Ack lifecycle assertions passed.\n"; + } +}; +Define_Module(LifecycleTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module LifecycleHcf extends Hcf { parameters: @class(::inet::ieee80211::LifecycleHcf); } +simple LifecycleTest { parameters: int scenario; @class(::inet::ieee80211::LifecycleTest); } +network LifecycleNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: LifecycleTest; +} + +%inifile: omnetpp.ini +[General] +network = LifecycleNetwork +ned-path = .;../../../../src +sim-time-limit = 7ms +seed-set = 0 +*.test.scenario = ${0..9} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "LifecycleHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack lifecycle assertions passed. diff --git a/tests/module/Ieee80211BlockAckNegotiation_1.test b/tests/module/Ieee80211BlockAckNegotiation_1.test new file mode 100644 index 00000000000..3f8b9c11a56 --- /dev/null +++ b/tests/module/Ieee80211BlockAckNegotiation_1.test @@ -0,0 +1,83 @@ +%description: +Check recipient negotiation through the production agreement handler. +The recipient response determines buffer size and inactivity timeout. +The response copies the request dialog token. + +%extraargs: -r 0..3 + +%file: Negotiation.cc +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" + +namespace inet::ieee80211 { +class NegotiationPolicy : public IRecipientBlockAckAgreementPolicy { + public: + bool isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const override { + return variant == BlockAckVariant::BASIC || (localHt && peerHt); + } + bool isAddbaReqAccepted(const Ptr&) override { return true; } + bool isDelbaAccepted(const Ptr&) override { return true; } + simtime_t getBlockAckTimeoutValue() const override { return SimTime(20, SIMTIME_MS); } + bool aMsduSupported() const override { return false; } + bool delayedBlockAckPolicySupported() const override { return false; } + int getMaximumAllowedBufferSize() const override { return 64; } +}; +class NegotiationTest : public cSimpleModule, public IProcedureCallback { + protected: + RecipientBlockAckAgreementHandler handler; + NegotiationPolicy policy; + Ptr response; + void initialize() override { scheduleAt(SIMTIME_ZERO, new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + const MacAddress peer("02:00:00:00:00:02"); + auto request = makeShared(); + request->setTransmitterAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(par("requestSize")); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBlockAckTimeoutValue(SimTime(40, SIMTIME_MS)); + handler.processReceivedAddbaRequest(request, &policy, this); + ASSERT(response != nullptr); + ASSERT(response->getDialogToken() == 37); + ASSERT(response->getBufferSize() == 64); + ASSERT(response->getBlockAckTimeoutValue() == SimTime(20, SIMTIME_MS)); + auto agreement = handler.getAgreement(5, peer); + ASSERT(agreement != nullptr); + ASSERT(agreement->getBufferSize() == response->getBufferSize()); + ASSERT(agreement->getBlockAckTimeoutValue() == response->getBlockAckTimeoutValue()); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(100)); + std::cout << "Block Ack negotiation assertions passed.\n"; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + response = dynamicPtrCast(header); + delete packet; + } + void transmitControlResponseFrame(Packet *, const Ptr&, + Packet *, const Ptr&) override { throw cRuntimeError("Unexpected control response"); } +}; +Define_Module(NegotiationTest); +} + +%file: test.ned +simple NegotiationTest { + parameters: + int requestSize; + @class(::inet::ieee80211::NegotiationTest); +} +network NegotiationNetwork { submodules: test: NegotiationTest; } + +%inifile: omnetpp.ini +[General] +network = NegotiationNetwork +ned-path = .;../../../../src +sim-time-limit = 1ms +seed-set = 0 +*.test.requestSize = ${0,8,64,128} +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Block Ack negotiation assertions passed. diff --git a/tests/module/Ieee80211BlockAckResponseWait_1.test b/tests/module/Ieee80211BlockAckResponseWait_1.test new file mode 100644 index 00000000000..9fc88c204d4 --- /dev/null +++ b/tests/module/Ieee80211BlockAckResponseWait_1.test @@ -0,0 +1,242 @@ +%description: +Check Action reception during the final ADDBA Request ACK wait through initialized Basic HCF. The request uses production contention, transmission, and frame-sequence handling. The fixture supplies the initial request record and injects an incoming Action after transmission. Matching success and rejection must suppress obsolete request failure. Wrong-token and unrelated-Action controls must retain ordinary request failure. The test does not model wireless reception or peer contention. + +%extraargs: -r 0..3 + +%file: ResponseWait.cc +#include +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ResponseWaitOriginator : public OriginatorBlockAckAgreementHandler { + public: + void prepare(const Ptr& request) { createAgreement(request); } +}; +class ResponseWaitHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + int attempts = 0; + int failures = 0; + int acknowledgments = 0; + int delbas = 0; + int linkBreaks = 0; + int requestOutcomes = 0; + bool injected = false; + bool dispatchingAction = false; + void begin() { + Enter_Method("begin"); + subscribe(linkBrokenSignal, this); + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + delete originatorBlockAckAgreementHandler; + auto handler = new ResponseWaitOriginator(); + originatorBlockAckAgreementHandler = handler; + auto request = makeShared(); + request->setTransmitterAddress(mac->getAddress()); + request->setReceiverAddress(peer); + request->setTid(5); + request->setDialogToken(37); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + handler->prepare(request); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(mac->getAddress()); + data->setReceiverAddress(peer); + data->setAddress3(peer); + data->setTid(5); + auto packet = new Packet("deferred-data", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, data); + Hcf::processMgmtFrame(new Packet("request", request), request); + } + void verify() { + Enter_Method("verify"); + ASSERT(injected); + ASSERT(attempts == 1); + ASSERT(acknowledgments == 1); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, 5); + int scenario = par("scenario"); + if (scenario < 2) { + ASSERT(failures == 0); + ASSERT(delbas == 0); + ASSERT(linkBreaks == 0); + ASSERT(requestOutcomes <= 1); + ASSERT(!addbaResponseTimer->isScheduled()); + ASSERT(!startRxTimer->isScheduled()); + if (scenario == 0) { + ASSERT(agreement != nullptr); + ASSERT(agreement->getIsAddbaResponseReceived()); + ASSERT(agreement->getBufferSize() == 32); + } + else + ASSERT(agreement == nullptr || !agreement->getIsAddbaResponseReceived()); + } + else { + ASSERT(failures == 1); + ASSERT(linkBreaks == 1); + ASSERT(requestOutcomes == 1); + ASSERT(agreement == nullptr || !agreement->getIsAddbaResponseReceived()); + } + std::cout << "Block Ack response-wait assertions passed.\n"; + endSimulation(); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (signal == linkBrokenSignal) + linkBreaks++; + else if (signal == Ieee80211Mac::frameTransmissionOutcomeSignal) { + auto packet = check_and_cast(object); + if (dynamicPtrCast(packet->peekAtFront())) + requestOutcomes++; + } + else + Hcf::receiveSignal(source, signal, object, details); + } + void handleMessage(cMessage *message) override { + if (message->isName("injectAction")) { + delete message; + ASSERT(frameSequenceHandler->isSequenceRunning()); + ASSERT(frameSequenceHandler->getContext()->getLastStep()->getType() == IFrameSequenceStep::Type::RECEIVE); + ASSERT(startRxTimer->isScheduled()); + int scenario = par("scenario"); + Ptr header; + if (scenario == 3) { + auto request = makeShared(); + request->setTid(6); + request->setDialogToken(91); + request->setBlockAckPolicy(true); + request->setBufferSize(64); + header = request; + } + else { + auto response = makeShared(); + response->setTid(5); + response->setDialogToken(scenario == 2 ? 38 : 37); + response->setStatusCode(scenario == 1 ? 37 : 0); + response->setBlockAckPolicy(true); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(SIMTIME_ZERO); + header = response; + } + header->setTransmitterAddress(peer); + header->setReceiverAddress(mac->getAddress()); + header->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet("incoming-action", header); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared()); + injected = true; + dispatchingAction = true; + processLowerFrame(packet, header); + dispatchingAction = false; + scheduleAfter(SimTime(1, SIMTIME_NS), new cMessage("verify")); + } + else if (message->isName("verify")) { + delete message; + verify(); + } + else + Hcf::handleMessage(message); + } + void originatorProcessTransmittedFrame(Packet *packet) override { + auto request = dynamicPtrCast(packet->peekAtFront()); + Hcf::originatorProcessTransmittedFrame(packet); + if (request) { + attempts++; + scheduleAfter(SimTime(100, SIMTIME_US), new cMessage("injectAction")); + } + } + void originatorProcessFailedFrame(Packet *packet) override { + if (dynamicPtrCast(packet->peekAtFront())) + failures++; + Hcf::originatorProcessFailedFrame(packet); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + if (dynamicPtrCast(packet->peekAtFront())) + ASSERT(!dispatchingAction); + return Hcf::transmitFrame(packet, ifs); + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + acknowledgments++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + delbas++; + delete packet; + } + void sendUp(const std::vector& frames) override { + for (auto packet : frames) { + take(packet); + delete packet; + } + } +}; +Define_Module(ResponseWaitHcf); +class ResponseWaitTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(ResponseWaitTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ResponseWaitHcf extends Hcf { + parameters: + int scenario; + @class(::inet::ieee80211::ResponseWaitHcf); +} +simple ResponseWaitTest { parameters: @class(::inet::ieee80211::ResponseWaitTest); } +network ResponseWaitNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: ResponseWaitTest; +} + +%inifile: omnetpp.ini +[General] +network = ResponseWaitNetwork +ned-path = .;../../../../src +sim-time-limit = 5ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ResponseWaitHcf" +**.wlan[*].mac.hcf.scenario = ${0..3} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.mgmtAndNonQoSRecoveryProcedure.shortRetryLimit = 1 +**.mgmtAndNonQoSRecoveryProcedure.longRetryLimit = 1 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Block Ack response-wait assertions passed. diff --git a/tests/module/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. From 832dc3672c0233e47648423eaca191a0dece6f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:44:26 +0200 Subject: [PATCH 03/17] ieee80211: refactor: let frame sequences own interruption outcomes Before this commit, HCF chooses completion or failure when an accepted Action interrupts a response wait. The frame-sequence handler owns the relevant exchange state, so receive dispatch duplicates that decision. After this commit, HCF requests interruption without an outcome argument. The frame-sequence handler checks whether recipient processing retires the outgoing frame. A retired frame completes without failure feedback. Other exchanges retain the ordinary failure path. HCF cancels the receive timer at sequence completion. For example, a correlated Action can retire outgoing management work during a response wait. The handler completes that retired exchange without failure feedback. Existing fixtures also cover refusals, unrelated Actions, and immediate ACK behavior. Custom handlers must implement the no-argument interruption operation. The migration guide explains this contract. Built-in packet behavior and event order remain unchanged. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | refactor | test migration | ieee80211-compressed-blockack-replacement --- doc/src/migration-guide/index.rst | 2 +- .../ieee80211/mac/contract/IFrameSequenceHandler.h | 4 ++-- .../ieee80211/mac/coordinationfunction/Hcf.cc | 8 ++------ .../mac/framesequence/FrameSequenceHandler.cc | 13 +++++++++---- .../mac/framesequence/FrameSequenceHandler.h | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 138bc9d7fa1..47ead943f5f 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -201,7 +201,7 @@ The optional ``peekEligiblePacket(predicate)`` operation returns a borrowed pack 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(notifyFailure)``. A false argument ends an exchange that an accepted Action already resolves, without obsolete failure feedback. Cleanup must destroy the exchange context before it deletes deferred frame copies. These repairs do not add MAC stop/start recovery or final-transfer teardown. +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/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index 0f61eaa9891..d2002a5ffdc 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -48,8 +48,8 @@ class INET_API IFrameSequenceHandler virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; - // The caller can suppress failure feedback for an exchange that its procedure already resolved. - virtual void interruptFrameSequence(bool notifyFailure) = 0; + // 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/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 7deeca7ef0f..b75ddaa30eb 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -805,17 +805,12 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& handlingAgreementAction = true; emit(packetReceivedFromPeerSignal, packet); std::unique_ptr responseSource(packet->dup()); - auto outgoing = check_and_cast(frameSequenceHandler->getContext()->getStepBeforeLast())->getFrameToTransmit(); auto accepted = recipientDataService->managementFrameReceived(packet, action); for (auto frame : accepted) { take(frame); dispatchAgreementAction(frame->peekAtFront()); } - cancelEvent(startRxTimer); - bool resolved = obsoleteActions.count(outgoing->getId()) || - (dynamicPtrCast(outgoing->peekAtFront()) && - !edcaf->getInProgressFrames()->containsFrame(outgoing)); - frameSequenceHandler->interruptFrameSequence(!resolved); + frameSequenceHandler->interruptFrameSequence(); recipientAckProcedure->processReceivedFrame(responseSource.get(), action, check_and_cast(recipientAckPolicy), this); for (auto frame : accepted) delete frame; @@ -961,6 +956,7 @@ 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(); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index cc6450101f3..c01596ee2ab 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -14,15 +14,20 @@ namespace inet { namespace ieee80211 { -void FrameSequenceHandler::interruptFrameSequence(bool notifyFailure) +void FrameSequenceHandler::interruptFrameSequence() { ASSERT(isSequenceRunning()); ASSERT(dynamic_cast(context->getLastStep())); context->getLastStep()->setCompletion(IFrameSequenceStep::Completion::REJECTED); - if (notifyFailure) - abortFrameSequence(); - else + auto outgoing = check_and_cast(context->getStepBeforeLast())->getFrameToTransmit(); + // Retirement removes DATA and management frames from the queue before this transition. + // Control exchanges retain their ordinary failure path through the recovery callback. + bool retired = dynamicPtrCast(outgoing->peekAtFront()) && + !context->getInProgressFrames()->containsFrame(outgoing); + if (retired) finishFrameSequence(); + else + abortFrameSequence(); } void FrameSequenceHandler::handleStartRxTimeout() diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index fa65175856a..a3d0d6f647b 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -35,7 +35,7 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual void processResponse(Packet *frame) override; virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; - virtual void interruptFrameSequence(bool notifyFailure) override; + virtual void interruptFrameSequence() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } virtual ~FrameSequenceHandler(); From 0c234006883d25a5d47458bdf68abfc29d85f96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:44:29 +0200 Subject: [PATCH 04/17] plan: add: record S3b agreement lifecycle completion Before this commit, the branch lacks committed S3b completion records. Reviewers need component responsibilities, scope limits, and verification results in one record. After this commit, plan/done/ contains the summary and execution record. The summary explains agreement setup, retirement, peer reset, and safe physical completion. The execution record explains allocation indices and separates results by original source revision. Those historical revision identifiers remain unchanged. The records describe debug and release compilation, focused module fixtures, and functional fingerprint checks for the original implementation and later retirement repair. This documentation commit changes no protocol behavior or recorded expectation. S4 and production Compressed exchanges remain outside the completed stage. Plan: plan/done/ieee80211-compressed-blockack-s3b-summary.md Change: plan | behavior.add | - --- ...80211-compressed-blockack-s3b-execution.md | 279 ++++++++++++++++++ ...ee80211-compressed-blockack-s3b-summary.md | 251 ++++++++++++++++ 2 files changed, 530 insertions(+) create mode 100644 plan/done/ieee80211-compressed-blockack-s3b-execution.md create mode 100644 plan/done/ieee80211-compressed-blockack-s3b-summary.md 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. From 23c1939f27cbf8bb449772a2da64a49f9ecad5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:44:56 +0200 Subject: [PATCH 05/17] ieee80211: refactor: expose DATA preview without extraction Before this commit, the shared in-progress queue can obtain a successor only through extraction. The QoS data service already supports preview, but the generic contract does not expose it. BAR selection needs the candidate before queue ownership or sequence state changes. After this commit, the contract exposes preview, sequence lookup, and aggregation capability. The in-progress queue can return a borrowed candidate and prospective sequence number. The ordinary successor query retains extraction as its default. For example, a queued frame retains sequence 77 while preview reports prospective sequence 0. Preview leaves the frame in its queue. The DATA-selection fixture checks this result, ownership, and an empty in-progress set. Existing transmission behavior remains unchanged. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | refactor | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 +++ doc/src/migration-guide/index.rst | 7 +++++ .../contract/IOriginatorQosMacDataService.h | 28 +++++++++++++++++++ .../originator/OriginatorQosMacDataService.h | 10 +++---- .../ieee80211/mac/queue/InProgressFrames.cc | 25 +++++++++++++++-- .../ieee80211/mac/queue/InProgressFrames.h | 4 ++- .../Ieee80211BlockAckDataSelection_1.test | 8 ++++++ 7 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h diff --git a/WHATSNEW b/WHATSNEW index 097c0d79b9e..40cf987fb24 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -444,6 +444,10 @@ Notable backward incompatible changes are the following: Basic remains the default. Compressed selection still stops initialization. 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 QoS DATA preview contract + + The QoS data service now exposes candidate preview through IOriginatorQosMacDataService. The in-progress queue can preview DATA without extraction or sequence assignment. Ordinary successor queries retain extraction as their default. Custom in-progress queues must accept the extract argument in getPendingFrameFor(). The migration guide describes this interface change. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 47ead943f5f..cee7afc4edb 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -14,6 +14,13 @@ 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 Basic Block Ack extension points +------------------------------------------ + +HCF previews the next eligible DATA before BAR selection. Preview must preserve queue ownership and the next sequence number. Custom QoS data services must implement ``IOriginatorQosMacDataService``. This contract extends ``IOriginatorMacDataService`` with ``peekEligibleFrame()``, ``getNextSequenceNumber()``, and ``hasAMsduAggregation()``. + +Custom ``InProgressFrames`` classes must add ``bool extract = true`` to their ``getPendingFrameFor()`` override. When ``extract`` is false, the method must return a borrowed candidate without extraction or sequence assignment. The default value preserves ordinary extraction. Custom candidate sources must also implement ``peekFrameToTransmit()`` without extraction. + TCP Algorithms -------------- diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h new file mode 100644 index 00000000000..e66fa555666 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h @@ -0,0 +1,28 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IORIGINATORQOSMACDATASERVICE_H +#define __INET_IORIGINATORQOSMACDATASERVICE_H + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" + +namespace inet { +namespace ieee80211 { + +class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService +{ + public: + // Preview borrows the frame without extraction or sequence assignment. + virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const = 0; + virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const = 0; + virtual bool hasAMsduAggregation() const = 0; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 40366a32b9c..f6590365817 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -16,14 +16,14 @@ #include "inet/linklayer/ieee80211/mac/contract/IMpduAggregationPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IMsduAggregation.h" #include "inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h" -#include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/contract/ISequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mac/lifetime/EdcaTransmitLifetimeHandler.h" namespace inet { namespace ieee80211 { -class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, public SimpleModule +class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService, public SimpleModule { protected: // Figure 5-1—MAC data plane architecture @@ -58,9 +58,9 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p extractionEligible = eligible; aMsduPermitted = permission; } - Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const; - SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const; - bool hasAMsduAggregation() const { return aMsduAggregationPolicy != nullptr; } + virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const override; + virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const override; + virtual bool hasAMsduAggregation() const override { return aMsduAggregationPolicy != nullptr; } virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index e9918ee20f7..81ec8985ddc 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" + namespace inet { namespace ieee80211 { @@ -95,9 +97,26 @@ Packet *InProgressFrames::getFrameToTransmit() return nullptr; } -Packet *InProgressFrames::getPendingFrameFor(Packet *frame) +Packet *InProgressFrames::peekFrameToTransmit() +{ + for (auto frame : inProgressFrames) { + if (isEligibleToTransmit(frame)) + return frame; + } + return check_and_cast(dataService)->peekEligibleFrame(pendingQueue); +} + +SequenceNumberCyclic InProgressFrames::getCandidateSequenceNumber(Packet *packet) const +{ + auto header = packet->peekAtFront(); + if (containsFrame(packet)) + return header->getSequenceNumber(); + return check_and_cast(dataService)->getNextSequenceNumber(header->getReceiverAddress(), header->getTid()); +} + +Packet *InProgressFrames::getPendingFrameFor(Packet *frame, bool extract) { - auto frameToTransmit = getFrameToTransmit(); + auto frameToTransmit = extract ? getFrameToTransmit() : peekFrameToTransmit(); if (dynamicPtrCast(frame->peekAtFront())) return frameToTransmit; else { @@ -105,6 +124,8 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) if (isEligibleToTransmit(frame) && frameToTransmit != frame) return frame; } + if (!extract) + return check_and_cast(dataService)->peekEligibleFrame(pendingQueue); auto frames = dataService->extractFramesToTransmit(pendingQueue); if (frames) { auto firstFrame = (*frames)[0]; diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 293aebfc3a3..65b13b67aa7 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -53,7 +53,9 @@ class INET_API InProgressFrames : public SimpleModule virtual Packet *getFrames(int i) const { return inProgressFrames[i]; } bool containsFrame(const Packet *packet) const; virtual Packet *getFrameToTransmit(); - virtual Packet *getPendingFrameFor(Packet *frame); + virtual Packet *peekFrameToTransmit(); + SequenceNumberCyclic getCandidateSequenceNumber(Packet *packet) const; + virtual Packet *getPendingFrameFor(Packet *frame, bool extract = true); virtual void dropFrame(Packet *packet); virtual void dropFrames(std::set>> seqAndFragNums); diff --git a/tests/module/Ieee80211BlockAckDataSelection_1.test b/tests/module/Ieee80211BlockAckDataSelection_1.test index d1b8c948262..534e93fb524 100644 --- a/tests/module/Ieee80211BlockAckDataSelection_1.test +++ b/tests/module/Ieee80211BlockAckDataSelection_1.test @@ -177,6 +177,14 @@ class DataSelectionTest : public cSimpleModule, public cListener { ASSERT(queue->getPacket(0) == first); ASSERT(service->peekEligibleFrame(queue) == selected); ASSERT(service->getNextSequenceNumber(MacAddress("02:00:00:00:00:02"), 0) == SequenceNumberCyclic(0)); + auto previewService = check_and_cast(service); + auto frames = edca->getEdcaf(AC_BE)->getInProgressFrames(); + auto owner = selected->getOwner(); + ASSERT(previewService->peekEligibleFrame(queue) == selected); + ASSERT(frames->peekFrameToTransmit() == selected); + ASSERT(frames->getCandidateSequenceNumber(selected) == SequenceNumberCyclic(0)); + ASSERT(frames->getLength() == 0 && selected->getOwner() == owner); + ASSERT(selected->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(77)); wait(SimTime(100, SIMTIME_MS)); auto extracted = service->extractFramesToTransmit(queue); ASSERT(extracted != nullptr && extracted->size() == 1); From 2c454a83d1f0bdb8b9a5cf5260b814f9c25434cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:46:58 +0200 Subject: [PATCH 06/17] ieee80211: change: complete Basic BAR transfer resolution Before this commit, BAR selection can extract DATA and combine counts across peers or TIDs. A short burst can lack a final response when its length remains below the BAR threshold. Duration forecasts can extract or incorrectly reserve an unprepared successor. After this commit, HCF selects BAR work before DATA extraction. The ACK policy groups outstanding DATA by peer and TID within the accepted window. Below the threshold, it defers BAR only for the next DATA within that group and window. EDCA can contend for BAR without a DATA candidate. Matched responses resolve only the selected peer and TID. Resolved DATA advances the window; the response starting sequence number alone cannot advance it. BAR policy calls require an explicit agreement-handler argument. Transmitted-BAR processing also requires an explicit originator agreement. Omitted arguments fail compilation. Explicit null arguments retain their documented behavior. BAR collision and failure update contention state without a DATA retry entry. Retransmitted DATA sets Retry after failed Block Ack feedback. At a safe exchange boundary, final resolution or expiration triggers completion DELBA. HCF first checks queued DATA in the TID category to avoid repeated unrelated scans. Basic single protection reserves only an already prepared successor. The sequence ends the TXOP before further extraction can change aggregation, fragmentation, or setup. Final zero-limit DATA reserves only its immediate required response. Prepared fragments can continue; unprepared successors require another TXOP. This policy can require more contention. Multiple protection retains its successor policy. For example, a window of one and BAR threshold of five require BAR before the next sequence leaves the window. Direct fixtures also cover separate peers and TIDs, timeout, collision, expiration, final Duration, and later-BAR admission. The timing fixture checks the former 40-microsecond overrun and the permitted initial Control-frame exception. Seven previously approved non-graphical fingerprint rows accompany their causal source change. Final ACK reservations, setup order, Retry feedback, and extraction boundaries explain those values. The execution record retains each exact value and its original approval. Graphical values remain unchanged and unverified. BAR selection, Duration forecasts, and continuation share one candidate contract without extraction. They stay together so each recorded expectation describes the complete behavior. Production Compressed exchanges, general Basic DATA admission, and complete NAV conformance remain outside this commit. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change | test whatsnew migration fingerprint | ieee80211-compressed-blockack-replacement --- WHATSNEW | 10 +- doc/src/migration-guide/index.rst | 8 + doc/src/users-guide/ch-80211.rst | 10 +- ...e80211-compressed-blockack-s4-execution.md | 178 +++++++ .../blockack/OriginatorBlockAckAgreement.h | 5 + .../OriginatorBlockAckAgreementHandler.cc | 1 - .../ieee80211/mac/channelaccess/Edca.h | 3 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 5 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 112 ++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 2 + .../ieee80211/mac/framesequence/HcfFs.cc | 27 +- .../framesequence/PrimitiveFrameSequences.cc | 10 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 5 +- .../mac/originator/OriginatorQosAckPolicy.cc | 97 ++-- .../mac/originator/OriginatorQosAckPolicy.h | 7 +- .../ieee80211/mac/originator/QosAckHandler.cc | 18 +- .../ieee80211/mac/originator/QosAckHandler.h | 3 +- .../mac/originator/QosRecoveryProcedure.cc | 14 + .../mac/originator/QosRecoveryProcedure.h | 1 + .../ieee80211/mac/queue/InProgressFrames.cc | 3 +- tests/fingerprint/examples.csv | 4 +- tests/fingerprint/showcases.csv | 10 +- .../Ieee80211BlockAckCompletionCost_1.test | 210 ++++++++ tests/module/Ieee80211BlockAckDeferral_1.test | 34 +- ...Ieee80211BlockAckFragmentRetirement_1.test | 57 ++- .../module/Ieee80211BlockAckScheduling_1.test | 455 ++++++++++++++++++ tests/module/Ieee80211HcfHeader_1.test | 2 +- .../Ieee80211HcfMultipleProtection_2.test | 11 +- tests/unit/Ieee80211BlockAckScheduling_1.test | 153 ++++++ tests/unit/Ieee80211MultipleProtection_1.test | 3 +- 30 files changed, 1342 insertions(+), 116 deletions(-) create mode 100644 plan/pending/ieee80211-compressed-blockack-s4-execution.md create mode 100644 tests/module/Ieee80211BlockAckCompletionCost_1.test create mode 100644 tests/module/Ieee80211BlockAckScheduling_1.test create mode 100644 tests/unit/Ieee80211BlockAckScheduling_1.test diff --git a/WHATSNEW b/WHATSNEW index 40cf987fb24..eecb155d1e0 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -444,9 +444,15 @@ Notable backward incompatible changes are the following: Basic remains the default. Compressed selection still stops initialization. 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 QoS DATA preview contract +51. IEEE 802.11 Basic Block Ack transfer completion - The QoS data service now exposes candidate preview through IOriginatorQosMacDataService. The in-progress queue can preview DATA without extraction or sequence assignment. Ordinary successor queries retain extraction as their default. Custom in-progress queues must accept the extract argument in getPendingFrameFor(). The migration guide describes this interface change. + 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. Notable backward compatible changes are the following: diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index cee7afc4edb..a3d5e9cb492 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -19,6 +19,14 @@ 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 diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index f4569882f96..b526cdbf248 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -700,9 +700,13 @@ station then sets up an agreement with its peer automatically (with ADDBA frames) when it sends a QoS data frame shorter than :par:`maxBlockAckPolicyFrameLength` (default 1000 bytes). After that, the frames of that traffic class go out without an immediate acknowledgement, -and the station sends a Block Ack Request after :par:`blockAckReqThreshold` -(default 5) unacknowledged frames. Both parameters are on -``mac.hcf.originatorAckPolicy``. +and the station normally sends BAR after :par:`blockAckReqThreshold` unacknowledged frames. The default threshold is five. Both parameters are on ``mac.hcf.originatorAckPolicy``. + +The station applies the threshold separately to each peer and TID. It sends BAR below the threshold when the next eligible DATA belongs to another agreement or cannot fit the transmit window. The station also sends BAR when no eligible DATA remains. For example, a burst of four frames ends with BAR even with the default threshold of five. A negotiated buffer size of one requires BAR before the second sequence number. + +BAR can obtain channel access without a queued DATA frame. A later BAR within a TXOP must fit the applicable TXOP and TXNAV bounds with its response. If BAR cannot fit, the station retains that work for another contention. + +After the final exchange, the originator sends DELBA when its agreement has no queued DATA, in-progress DATA, or active BAR exchange. A zero inactivity timeout does not disable this transfer-completion rule. DATA that arrives after this boundary waits for DELBA completion. A later transfer can require another ADDBA exchange. If the last unresolved DATA expires, the originator uses the same teardown path. Ordinary recipient teardown releases complete units behind a receive gap before it releases agreement resources. The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size diff --git a/plan/pending/ieee80211-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/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index 8ef73652db3..adc12872b27 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -47,6 +47,11 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual ~OriginatorBlockAckAgreement() {} virtual int getBufferSize() const { return bufferSize; } + int getWindowSize() const { return std::min(bufferSize, 64); } + bool containsSequence(SequenceNumberCyclic sequence) const + { + return (sequence.get() - startingSequenceNumber.get() + 4096) % 4096 < getWindowSize(); + } virtual SequenceNumberCyclic getStartingSequenceNumber() { return startingSequenceNumber; } virtual void setStartingSequenceNumber(SequenceNumberCyclic sequenceNumber) { startingSequenceNumber = sequenceNumber; } virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index fdc8c44dd1e..e136518a229 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -170,7 +170,6 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); if (agreement) { - agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h index b7b9e4d6bc2..4b038c0c914 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h @@ -30,11 +30,10 @@ class INET_API Edca : public SimpleModule virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - virtual AccessCategory mapTidToAc(Tid tid); - public: virtual ~Edca(); + virtual AccessCategory mapTidToAc(Tid tid); virtual AccessCategory classifyFrame(const Ptr& header); virtual Edcaf *getEdcaf(AccessCategory ac) const { return edcafs[ac]; } EdcaTransmitLifetimeHandler *getLifetimeHandler() const { return lifetimeHandler; } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 94a583a111a..641e21e8ecd 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -10,6 +10,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" @@ -23,9 +24,9 @@ class INET_API IOriginatorQoSAckPolicy virtual bool isAckNeeded(const Ptr& header) const = 0; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const = 0; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const = 0; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const = 0; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b75ddaa30eb..7c87cbc0622 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -204,6 +204,7 @@ void Hcf::expireDataFrames(bool preserveSequence) frames->clearDroppedFrames(); } scheduleDataLifetimeTimer(); + advanceOriginatorWindows(); } void Hcf::forEachChild(cVisitor *v) @@ -228,6 +229,8 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == dataLifetimeTimer) { expireDataFrames(); + if (!frameSequenceHandler->isSequenceRunning()) + completeTransfers(); updateAgreementProcedures(); updateContention(); } @@ -433,7 +436,10 @@ bool Hcf::hasQueuedData(MacAddress peer, Tid tid) const return data && data->getType() == ST_DATA_WITH_QOS && data->getReceiverAddress() == peer && data->getTid() == tid && !edca->getLifetimeHandler()->isExpired(packet); }; - for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + // Check the TID's category first. Other categories retain support for custom classification. + int first = edca->mapTidToAc(tid); + for (int offset = 0; offset < AC_NUMCATEGORIES; offset++) { + int ac = (first + offset) % AC_NUMCATEGORIES; auto queue = edca->getEdcaf(static_cast(ac))->getPendingQueue(); if (auto selected = dynamic_cast(queue)) { try { @@ -522,6 +528,48 @@ void Hcf::updateContention() } } +void Hcf::advanceOriginatorWindows() +{ + if (!originatorBlockAckAgreementHandler) + return; + auto service = check_and_cast(originatorDataService); + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + auto agreement = entry.second; + if (!agreement->getIsAddbaResponseReceived()) + continue; + auto start = agreement->getStartingSequenceNumber(); + auto next = service->getNextSequenceNumber(entry.first.first, entry.first.second); + int distance = (next.get() - start.get() + 4096) % 4096; + 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) + distance = std::min(distance, (data->getSequenceNumber().get() - start.get() + 4096) % 4096); + } + } + // IEEE Std 802.11-2024, 10.25.6.7.1: resolved positions permit transmit-window progress. + agreement->setStartingSequenceNumber(start + distance); + } +} + +void Hcf::completeTransfers() +{ + if (!originatorBlockAckAgreementHandler) + return; + std::vector> completed; + for (const auto& entry : originatorBlockAckAgreementHandler->getAgreements()) { + auto agreement = entry.second; + if (agreement->getIsAddbaResponseReceived() && agreement->getFirstDataTransmitted() && + !hasNumberedData(entry.first.first, entry.first.second) && !hasQueuedData(entry.first.first, entry.first.second)) + completed.push_back(entry.first); + } + // IEEE Std 802.11-2024, 10.25.4; INET uses the accepted queue predicate at an exchange boundary. + for (const auto& id : completed) + beginTeardown(id.first, id.second, true, 37); +} + void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) { for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { @@ -902,6 +950,11 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) for (auto edcaf : internallyCollidedEdcafs) { AccessCategory ac = edcaf->getAccessCategory(); auto dataRecoveryProcedure = edcaf->getRecoveryProcedure(); + if (originatorAckPolicy->isBlockAckReqNeeded(edcaf->getInProgressFrames(), edcaf->getTxopProcedure(), originatorBlockAckAgreementHandler)) { + dataRecoveryProcedure->controlFrameTransmissionFailed(); + edcaf->requestChannel(this); + continue; + } Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); if (internallyCollidedFrame == nullptr) continue; @@ -963,6 +1016,8 @@ void Hcf::frameSequenceFinished() if (edcaf) { // The context still owns references. dropFrame defers deletion until context disposal. expireDataFrames(false); + advanceOriginatorWindows(); + completeTransfers(); updateAgreementProcedures(); bool startContention = !handlingAgreementAction && hasFrameToTransmit(); edcaf->releaseChannel(this); @@ -1291,8 +1346,12 @@ void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& controlHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) - edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq); + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), blockAckReq->getTidInfo()) : nullptr; + if (agreement) + edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq, agreement); + } else if (auto rtsFrame = dynamicPtrCast(controlHeader)) rtsProcedure->processTransmittedRts(rtsFrame); else @@ -1332,6 +1391,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else if (auto blockAckReq = dynamicPtrCast(failedHeader)) { edcaf->getAckHandler()->processFailedBlockAckReq(blockAckReq); + edcaf->getRecoveryProcedure()->controlFrameTransmissionFailed(); return; } else @@ -1426,6 +1486,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); + advanceOriginatorWindows(); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { actionTerminated(lastTransmittedPacket, true); @@ -1433,8 +1494,12 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { + if (!originatorBlockAckAgreementHandler || + !originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo())) + return; EV_INFO << "BasicBlockAck has arrived" << std::endl; edcaf->getRecoveryProcedure()->blockAckFrameReceived(); auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); @@ -1444,7 +1509,9 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(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); @@ -1652,6 +1732,19 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) } auto mode = dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); setFrameMode(packet, header, mode); + if (auto bar = dynamicPtrCast(header)) { + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), bar->getTidInfo()) : nullptr; + if (!agreement || !agreement->getIsAddbaResponseReceived()) + return false; + auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + response; + // IEEE Std 802.11-2024, 10.23.2.8 and 10.23.2.9: a later BAR must fit both bounds. + if (context->getNumSteps() > 1 && + ((txop->getLimit() > 0 && ifs + cost > txop->getRemaining()) || + (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()))) + return false; + } if (scopedSingle) { auto sifs = modeSet->getSifsTime(); auto cost = mode->getDuration(packet->getDataLength()); @@ -1704,7 +1797,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) packet->insertAtFront(updated); } else if (txop->getProtectionMechanism() == TxopProcedure::ProtectionMechanism::SINGLE_PROTECTION) { - auto pendingPacket = channelOwner->getInProgressFrames()->getPendingFrameFor(packet); + auto pendingPacket = dynamicPtrCast(header) ? nullptr : + channelOwner->getInProgressFrames()->getPendingFrameFor(packet, false); + if (dynamicPtrCast(header) && txop->getLimit() == 0) { + // The zero-limit frame sequence ends after this exchange; it has no successor reservation. + pendingPacket = nullptr; + } + else if (pendingPacket && !channelOwner->getInProgressFrames()->containsFrame(pendingPacket)) { + // HcfFs ends this TXOP before extraction can change the successor's size or setup state. + pendingPacket = nullptr; + } const auto& pendingHeader = pendingPacket == nullptr ? nullptr : pendingPacket->peekAtFront(); auto duration = singleProtectionMechanism->computeDurationField(packet, header, pendingPacket, pendingHeader, txop, recipientAckPolicy); auto header = packet->removeAtFront(); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index aaa5c20cad6..03d7bb93a45 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -159,6 +159,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: bool hasNumberedData(MacAddress peer, Tid tid) const; bool hasObsoleteActionInFlight(MacAddress peer, Tid tid, bool originator) const; void updateAgreementProcedures(); + void advanceOriginatorWindows(); + void completeTransfers(); void updateContention(); void scheduleAddbaResponseTimer(); void actionTerminated(Packet *packet, bool acknowledged); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index b12ce4d54f4..7a49e0ece7c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -34,8 +34,12 @@ HcfFs::HcfFs() : int HcfFs::selectHcfSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) { - auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - return frameToTransmit->peekAtFront()->getReceiverAddress().isMulticast() ? 0 : 1; + auto qos = context->getQoSContext(); + auto frameToTransmit = qos->blockAckAgreementHandler ? context->getInProgressFrames()->peekFrameToTransmit() : + context->getInProgressFrames()->getFrameToTransmit(); + if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) + return 1; + return frameToTransmit && frameToTransmit->peekAtFront()->getReceiverAddress().isMulticast() ? 0 : 1; } int HcfFs::selectDataOrManagementSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) @@ -57,15 +61,21 @@ bool HcfFs::isSelfCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *con bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *context) { - bool hasFrameToTransmit = context->getInProgressFrames()->hasInProgressFrames(); + auto frames = context->getInProgressFrames(); + auto qos = context->getQoSContext(); + bool bar = qos->ackPolicy->isBlockAckReqNeeded(frames, qos->txopProcedure, qos->blockAckAgreementHandler); + bool hasFrameToTransmit = qos->blockAckAgreementHandler ? + bar || frames->peekFrameToTransmit() != nullptr : frames->hasInProgressFrames(); if (hasFrameToTransmit) { - auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& nextHeader = nextFrameToTransmit->peekAtFront(); + auto nextFrameToTransmit = qos->blockAckAgreementHandler ? frames->peekFrameToTransmit() : frames->getFrameToTransmit(); if (frameSequence->getCount() == 0) return true; - auto qos = context->getQoSContext(); auto txop = qos->txopProcedure; - if (nextHeader->getReceiverAddress().isMulticast() || txop->getRemaining() <= 0) + // Basic single protection reserves only prepared MPDUs. Unextracted DATA starts a new TXOP. + if (qos->blockAckAgreementHandler && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && + !bar && !frames->containsFrame(nextFrameToTransmit)) + return false; + if ((!bar && nextFrameToTransmit->peekAtFront()->getReceiverAddress().isMulticast()) || txop->getRemaining() <= 0) return false; if (txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION) { auto deadline = txop->getStart() + txop->getLimit(); @@ -77,8 +87,7 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte if (projectedStart >= deadline) return false; // BAR precedes DATA in TxOpFs and does not use RTS. - if (!qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), txop) && - context->getRtsPolicy()->isRtsNeeded(nextFrameToTransmit, nextHeader)) + if (!bar && context->getRtsPolicy()->isRtsNeeded(nextFrameToTransmit, nextFrameToTransmit->peekAtFront())) { // Integer legacy RTS airtime requires its start on the deadline's microsecond grid. if ((projectedStart - deadline).raw() % SimTime(1, SIMTIME_US).raw() != 0) diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 3fda1fcd52c..f9a3de4a6e5 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -412,7 +412,8 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con { switch (step) { case 0: { - auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure); + auto qos = context->getQoSContext(); + auto blockAckReqParams = qos->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler); auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); @@ -445,7 +446,12 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK; + auto response = dynamicPtrCast(receivedHeader); + auto requestStep = check_and_cast(context->getStep(firstStep)); + auto request = requestStep->getFrameToTransmit()->peekAtFront(); + return context->isForUs(receivedHeader) && response && + response->getTransmitterAddress() == request->getReceiverAddress() && + response->getTidInfo() == request->getTidInfo(); } default: throw cRuntimeError("Unknown step"); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc index 02420634437..e0daefabaa7 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -44,10 +44,11 @@ int TxOpFs::selectMgmtOrDataQap(AlternativesFs *frameSequence, FrameSequenceCont int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceContext *context) { + auto qos = context->getQoSContext(); + if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) + return 2; auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); - if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure)) - return 2; if (dynamicPtrCast(macHeader)) return 3; else { diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index b99e0a3339b..bf624914ed6 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -31,67 +31,55 @@ bool OriginatorQosAckPolicy::isAckNeeded(const Ptr& h return !header->getReceiverAddress().isMulticast(); } -std::map> OriginatorQosAckPolicy::getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const +bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { - auto outstandingFrames = inProgressFrames->getOutstandingFrames(); - std::map> outstandingFramesPerReceiver; - for (auto frame : outstandingFrames) - outstandingFramesPerReceiver[frame->peekAtFront()->getReceiverAddress()].push_back(frame); - return outstandingFramesPerReceiver; + return std::get<2>(computeBlockAckReqParameters(frames, txop, agreements)) != -1; } -SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const std::vector& outstandingFrames) const +std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *frames, TxopProcedure *txop, IOriginatorBlockAckAgreementHandler *agreements) const { - ASSERT(outstandingFrames.size() > 0); - auto startingSequenceNumber = outstandingFrames[0]->peekAtFront()->getSequenceNumber(); - for (size_t i = 1; i < outstandingFrames.size(); i++) { - auto seqNum = outstandingFrames[i]->peekAtFront()->getSequenceNumber(); - if (seqNum < startingSequenceNumber) - startingSequenceNumber = seqNum; + using Id = std::pair; + std::map> groups; + for (auto frame : frames->getOutstandingFrames()) { + auto header = frame->peekAtFront(); + auto agreement = agreements ? agreements->getAgreement(header->getReceiverAddress(), header->getTid()) : nullptr; + if (agreement && agreement->getIsAddbaResponseReceived() && agreement->containsSequence(header->getSequenceNumber())) + groups[{header->getReceiverAddress(), header->getTid()}].push_back(frame); } - return startingSequenceNumber; -} - -bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const -{ - // The Compressed Bitmap subfield of the BA Control field or BAR Control field shall be set to 1 in all - // BlockAck and BlockAckReq frames sent from one HT STA to another HT STA and shall be set to 0 otherwise. - return false; // non-HT STA -// for (auto frame : outstandingFrames) -// if (frame->getSequenceNumber() >= startingSequenceNumber && frame->getFragmentNumber() > 0) -// return false; -// return true; -} - -// FIXME -bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const -{ - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) - return true; - } - return false; -} - -// FIXME -std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const -{ - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) { - auto largestOutstandingFrames = outstandingFramesPerReceiver.begin(); - for (auto it = outstandingFramesPerReceiver.begin(); it != outstandingFramesPerReceiver.end(); it++) { - if (it->second.size() > largestOutstandingFrames->second.size()) - largestOutstandingFrames = it; + if (groups.empty()) + return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(0), Tid(-1)); + auto candidate = frames->peekFrameToTransmit(); + auto data = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + auto result = std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(0), Tid(-1)); + simtime_t oldestArrival = SIMTIME_MAX; + for (const auto& entry : groups) { + const auto& id = entry.first; + auto agreement = agreements->getAgreement(id.first, id.second); + // INET BAR policy defers a short group only for DATA within that group's window. + if ((int)entry.second.size() < blockAckReqThreshold && data && + data->getReceiverAddress() == id.first && data->getTid() == id.second && + agreement->containsSequence(frames->getCandidateSequenceNumber(candidate))) + continue; + auto start = agreement->getStartingSequenceNumber(); + int oldestOffset = 4096; + simtime_t arrival = SIMTIME_MAX; + for (auto frame : entry.second) { + auto sequence = frame->peekAtFront()->getSequenceNumber(); + int offset = (sequence.get() - start.get() + 4096) % 4096; + if (offset < oldestOffset) { + oldestOffset = offset; + arrival = frame->getArrivalTime(); } - MacAddress receiverAddress = largestOutstandingFrames->first; - SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); - Tid tid = largestOutstandingFrames->second.at(0)->peekAtFront()->getTid(); - return std::make_tuple(receiverAddress, startingSequenceNumber, tid); + else if (offset == oldestOffset) + arrival = std::min(arrival, frame->getArrivalTime()); + } + // Map order supplies the peer/TID tie break. + if (std::get<2>(result) == -1 || arrival < oldestArrival) { + oldestArrival = arrival; + result = std::make_tuple(id.first, start + oldestOffset, id.second); } } - return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(), -1); + return result; } AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const @@ -117,10 +105,9 @@ bool OriginatorQosAckPolicy::isBlockAckPolicyEligibleFrame(Packet *packet, const bool OriginatorQosAckPolicy::checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const { - bool bufferFull = agreement->getBufferSize() == agreement->getNumSentBaPolicyFrames(); bool aMsduOk = agreement->isAMsduPermitted() || !header->getAMsduPresent(); // TODO bool baPolicy = agreement->getIsDelayedBlockAckPolicySupported() || !frame->getAckPolicy(); - return !bufferFull && aMsduOk && (header->getSequenceNumber() >= agreement->getStartingSequenceNumber()); // TODO && baPolicy + return aMsduOk && agreement->containsSequence(header->getSequenceNumber()); } // diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index bcd9f1b2ae7..cefd7008567 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -31,16 +31,13 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat virtual void initialize(int stage) override; virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; - virtual std::map> getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const; - virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; - virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; public: virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const override; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const override; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *agreements) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 2fa02655743..fbd5c8d624b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -147,6 +147,13 @@ std::set>> QosAckHand else { throw cRuntimeError("Multi-TID BlockReq is unimplemented"); } + auto basic = dynamicPtrCast(blockAck); + auto compressed = dynamicPtrCast(blockAck); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + for (auto& entry : ackStatuses) { + if (entry.first.first == receiverAddr && entry.first.second.first == tid && entry.second == Status::WAITING_FOR_BLOCK_ACK) + entry.second = Status::BLOCK_ACK_ARRIVED_UNACKED; + } return ackedFrames; } @@ -195,23 +202,28 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const PtrgetReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()))] = Status::WAITING_FOR_NORMAL_ACK; } -void QosAckHandler::processTransmittedBlockAckReq(const Ptr& blockAckReq) +void QosAckHandler::processTransmittedBlockAckReq(const Ptr& blockAckReq, const OriginatorBlockAckAgreement *agreement) { for (auto& ackStatus : ackStatuses) { + if (ackStatus.first.first != blockAckReq->getReceiverAddress()) + continue; auto tid = ackStatus.first.second.first; auto seqCtrlField = ackStatus.first.second.second; + auto sequence = SequenceNumberCyclic(seqCtrlField.getSequenceNumber()); + if (agreement && !agreement->containsSequence(sequence)) + continue; auto& status = ackStatus.second; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { if (basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && (sequence.get() - startingSeqNum.get() + 4096) % 4096 < 64) status = Status::WAITING_FOR_BLOCK_ACK; } } else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { if (compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum && seqCtrlField.getFragmentNumber() == 0) // TODO ASSERT(seqCtrlField.second == 0)? + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && (sequence.get() - startingSeqNum.get() + 4096) % 4096 < 64 && seqCtrlField.getFragmentNumber() == 0) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 7c1e0bce61c..96f1026d85f 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -13,6 +13,7 @@ #include "inet/linklayer/ieee80211/mac/common/SequenceControlField.h" #include "inet/linklayer/ieee80211/mac/contract/IAckHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -60,7 +61,7 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); - virtual void processTransmittedBlockAckReq(const Ptr& blockAckReq); + virtual void processTransmittedBlockAckReq(const Ptr& blockAckReq, const OriginatorBlockAckAgreement *agreement); virtual void processFailedFrame(const Ptr& dataOrMgmtHeader); virtual void dropFrame(const Ptr& dataOrMgmtHeader); virtual void dropFrames(std::set>> seqAndFragNums); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc index e0b177a94b2..e52f6b72652 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc @@ -110,6 +110,20 @@ void QosRecoveryProcedure::ctsFrameReceived() void QosRecoveryProcedure::blockAckFrameReceived() { resetStationSrc(); + resetContentionWindow(); +} + +void QosRecoveryProcedure::controlFrameTransmissionFailed() +{ + // IEEE Std 802.11-2024, 10.23.2.2: control failures update EDCA backoff without DATA retry feedback. + if (stationShortRetryCounter < shortRetryLimit) { + stationShortRetryCounter++; + incrementContentionWindow(); + } + else { + resetStationSrc(); + resetContentionWindow(); + } } // diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h index 69435073efc..e1804fcc323 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h @@ -66,6 +66,7 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce virtual void ctsFrameReceived(); virtual void ackFrameReceived(Packet *packet, const Ptr& ackedHeader); virtual void blockAckFrameReceived(); + virtual void controlFrameTransmissionFailed(); virtual void rtsFrameTransmissionFailed(const Ptr& protectedHeader); virtual void dataFrameTransmissionFailed(Packet *packet, const Ptr& failedHeader); diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 81ec8985ddc..416e9709ad3 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -179,7 +179,8 @@ std::vector InProgressFrames::getOutstandingFrames() { std::vector outstandingFrames; for (auto frame : inProgressFrames) { - if (ackHandler->isOutstandingFrame(frame->peekAtFront())) + if ((!lifetimeHandler || !lifetimeHandler->isExpired(frame)) && + ackHandler->isOutstandingFrame(frame->peekAtFront())) outstandingFrames.push_back(frame); } return outstandingFrames; diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 88293c812bb..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, 5fbb-9fd9/tplx;c0a4-97f6/~tNl;2373-df01/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 4fd2-bb56/tplx;ecaf-89f7/~tNl;9398-0fae/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 2f2d-73b1/tplx;cc9e-d1a6/~tNl;e740-f7d4/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 2c80-6844/tplx;4e9e-cf86/~tNl;ea3d-e802/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -658,7 +658,7 @@ /examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, d929-5913/tplx;f602-4424/~tNl;f2fb-4d98/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, 0473-f9d2/tplx;e92c-7352/~tNl;317d-aa4f/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, e07a-4c5f/tplx;9f6c-7889/~tNl;29ea-169c/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, f8a4-9474/tplx;1cba-cccc/~tNl;538e-67df/~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 836d42b5183..9baf2771fb6 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -199,9 +199,9 @@ /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;7b7c-2bfb/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;d680-b8ad/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, b828-6bd6/tplx;026f-2c6d/~tNl;2cd4-4795/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 021a-d29a/tplx;1597-ec6a/~tNl;feff-7e88/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 5b9e-801f/tplx;8731-2504/~tNl;9724-d1ce/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 9161-625d/tplx;6a02-09aa/~tNl;cbc0-3b0a/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 36f4-5c22/tplx;66b0-94cd/~tNl;45f3-af4f/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 351b-d4ba/tplx;5c1f-69a8/~tNl;6182-6da3/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;fc60-7eb7/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;6f3a-074a/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -267,7 +267,7 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;dbb5-67b0/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 8df2-40a3/tplx;dab9-5e8d/~tNl;2e7b-0729/~tND;f985-34fb/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, c99f-354d/tplx;8e10-a3f0/~tNl;ac4b-3ba1/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, f509-65f1/tplx;6b3a-0d65/~tNl;c474-7174/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 2597-1e2e/tplx;d269-c6ae/~tNl;2f5c-4cca/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, 6150-7a8b/tplx;bcc9-11d2/~tNl;99fd-923d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -341,5 +341,5 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb5-d354/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, f58d-d2fd/tplx;38f7-f642/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, ab91-87f3/tplx;f30c-6aa4/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 diff --git a/tests/module/Ieee80211BlockAckCompletionCost_1.test b/tests/module/Ieee80211BlockAckCompletionCost_1.test new file mode 100644 index 00000000000..358aee3883e --- /dev/null +++ b/tests/module/Ieee80211BlockAckCompletionCost_1.test @@ -0,0 +1,210 @@ +%description: +Measure production Basic setup and transfer completion with equal UDP payload totals. +One run sends twelve packets continuously. The other run sends three bursts of four packets. +Both runs use seed zero, 6 Mbit/s, zero inactivity timeout, and a fixed 100 ms interval. +The measurements describe these traffic shapes. They do not estimate agreement-reuse performance. + +%extraargs: -r 0..1 + +%file: CompletionCost.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/common/TimeTag_m.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class CompletionCostHcf : public Hcf { + simtime_t deferredAt = -1; + simtime_t requestQueuedAt = -1; + simtime_t requestSentAt = -1; + simtime_t delbaQueuedAt = -1; + public: + simtime_t deferral; + simtime_t requestContention; + simtime_t responseAfterRequest; + simtime_t delbaCompletion; + protected: + void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& header, AccessCategory ac) override { + Hcf::originatorProcessTransmittedDataFrame(packet, header, ac); + const auto& procedures = originatorBlockAckAgreementHandler->getProcedures(); + if (procedures.count({header->getReceiverAddress(), header->getTid()}) && deferredAt < 0) + deferredAt = simTime(); + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) + requestQueuedAt = simTime(); + if (dynamicPtrCast(header)) + delbaQueuedAt = simTime(); + Hcf::processMgmtFrame(packet, header); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto request = dynamicPtrCast(packet->peekAtFront()); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (accepted && request) { + requestContention += simTime() + ifs - requestQueuedAt; + requestSentAt = simTime() + ifs; + } + return accepted; + } + void processLowerFrame(Packet *packet, const Ptr& header) override { + bool response = dynamicPtrCast(header) != nullptr; + Hcf::processLowerFrame(packet, header); + if (response && deferredAt >= 0) { + deferral += simTime() - deferredAt; + responseAfterRequest += simTime() - requestSentAt; + deferredAt = -1; + } + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + bool delba = dynamicPtrCast(transmitted->peekAtFront()) != nullptr; + Hcf::originatorProcessReceivedFrame(packet, transmitted); + if (delba) + delbaCompletion += simTime() - delbaQueuedAt; + } +}; +Define_Module(CompletionCostHcf); +class CompletionCostTest : public cSimpleModule, public cListener { + cModule *sender = nullptr; + cModule *sink = nullptr; + int sent = 0; + int received = 0; + int requests = 0; + int responses = 0; + int delbas = 0; + int drops = 0; + int64_t payloadBytes = 0; + simtime_t managementAirtime; + simtime_t deliveryDelay; + protected: + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + auto packet = check_and_cast(value); + if (source == sender) { + sent++; + return; + } + if (source == sink) { + received++; + payloadBytes += packet->getByteLength(); + auto creation = packet->peekAtFront()->getTag()->getCreationTime(); + deliveryDelay += simTime() - creation; + return; + } + if (signal == packetDroppedSignal) { + drops++; + return; + } + auto header = packet->peekAtFront(); + if (dynamicPtrCast(header)) + requests++; + if (dynamicPtrCast(header)) + responses++; + if (dynamicPtrCast(header)) + delbas++; + if (dynamicPtrCast(header)) + managementAirtime += packet->getTag()->getMode()->getDuration(packet->getDataLength()); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + sender = getModuleByPath("^.host1.app[0]"); + sink = getModuleByPath("^.host2.app[0]"); + sender->subscribe(packetSentSignal, this); + sink->subscribe(packetReceivedSignal, this); + for (const char *path : {"^.host1.wlan[0].mac.hcf", "^.host2.wlan[0].mac.hcf"}) { + getModuleByPath(path)->subscribe(packetSentToPeerSignal, this); + getModuleByPath(path)->subscribe(packetDroppedSignal, this); + } + scheduleAt(SimTime(99, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + auto hcf = check_and_cast(getModuleByPath("^.host1.wlan[0].mac.hcf")); + std::cout << "Completion cost: gaps=" << par("gaps").boolValue() << " sent=" << sent + << " received=" << received << " bytes=" << payloadBytes << " requests=" << requests + << " responses=" << responses << " delbas=" << delbas << " drops=" << drops + << " managementAirtime=" << managementAirtime << " deferral=" << hcf->deferral + << " requestContention=" << hcf->requestContention << " responseAfterRequest=" << hcf->responseAfterRequest + << " delbaCompletion=" << hcf->delbaCompletion << " deliveryDelaySum=" << deliveryDelay + << " goodputBps=" << payloadBytes * 8 / 0.1 << "\n"; + ASSERT(sent == 12 && received == 12 && payloadBytes == 8400 && drops == 0); + ASSERT(requests == (par("gaps").boolValue() ? 3 : 1)); + ASSERT(responses == requests && delbas == requests); + std::cout << "Block Ack completion cost assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(CompletionCostTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.AdhocHost; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module CompletionCostHcf extends Hcf { parameters: @class(::inet::ieee80211::CompletionCostHcf); } +simple CompletionCostTest { parameters: bool gaps; @class(::inet::ieee80211::CompletionCostTest); } +network CompletionCostNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: AdhocHost; + host2: AdhocHost; + test: CompletionCostTest; +} + +%inifile: omnetpp.ini +[General] +network = CompletionCostNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.test.gaps = ${gaps=false,true} +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicBurst" +*.host1.app[0].destAddresses = "host2" +*.host1.app[0].destPort = 1000 +*.host1.app[0].messageLength = 700B +*.host1.app[0].chooseDestAddrMode = "once" +*.host1.app[0].startTime = 10ms +*.host1.app[0].stopTime = ${gaps} ? 52ms : 11.15ms +*.host1.app[0].sendInterval = 100us +*.host1.app[0].burstDuration = ${gaps} ? 350us : 0s +*.host1.app[0].sleepDuration = ${gaps} ? 20ms : 0s +*.host2.numApps = 1 +*.host2.app[0].typename = "UdpSink" +*.host2.app[0].localPort = 1000 +*.host*.ipv4.arp.typename = "GlobalArp" +*.host*.wlan[*].opMode = "a" +*.host*.wlan[*].bitrate = 6Mbps +*.host*.wlan[*].classifier.typename = "QosClassifier" +*.host*.wlan[*].mac.qosStation = true +*.host*.wlan[*].mac.hcf.typename = "CompletionCostHcf" +*.host*.wlan[*].mac.hcf.isBlockAckSupported = true +*.host*.wlan[*].mac.hcf.blockAckVariant = "basic" +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +*.host*.wlan[*].radio.receiver.energyDetection = -105dBm +*.host*.wlan[*].radio.receiver.sensitivity = -105dBm +*.host*.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.host1.mobility.initialX = 0m +*.host2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack completion cost assertions passed. diff --git a/tests/module/Ieee80211BlockAckDeferral_1.test b/tests/module/Ieee80211BlockAckDeferral_1.test index 89da8e14084..58f6614bc96 100644 --- a/tests/module/Ieee80211BlockAckDeferral_1.test +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -29,6 +29,8 @@ class DeferralHcf : public Hcf { int dataVoice = 0; int dataVideo = 0; int dataAfterRequest = 0; + int dataWithAgreement = 0; + int dataAfterDelba = 0; int aggregatesAccepted = 0; int requestStart = -1; int firstData = -1; @@ -118,6 +120,12 @@ class DeferralHcf : public Hcf { if (data->getTid() == 6) dataVoice++; if (data->getTid() == 5) dataVideo++; if (requestStart >= 0 && data->getTid() == 6) { + if (hasAgreement(6)) + dataWithAgreement++; + if (delbas > 0 && !hasAgreement(6)) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + dataAfterDelba++; + } if (firstData < 0) firstData = data->getSequenceNumber().get(); dataAfterRequest++; } @@ -203,7 +211,7 @@ class DeferralTest : public cSimpleModule, public cListener { << ", aggregates=" << sender->aggregatesAccepted << ", request start=" << sender->requestStart << ", first DATA=" << sender->firstData << ".\n"; ASSERT(deferredBeforeExtraction); - ASSERT(sender->requests == 1 && sender->hasAgreement(6)); + ASSERT(sender->requests == 1 && !sender->hasAgreement(6) && sender->delbas == 1); ASSERT(sender->requestStart == 1 && sender->firstData == sender->requestStart); ASSERT(sender->dataAfterRequest == 1); ASSERT(sender->aggregatesAccepted == (scenario == 11 ? 2 : 0)); @@ -249,10 +257,16 @@ class DeferralTest : public cSimpleModule, public cListener { wait(SimTime(560, SIMTIME_MS) - simTime()); ASSERT(sender->rtsFailures >= 1); ASSERT(sender->firstRtsSequence == sender->requestStart); - ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); + ASSERT(sender->dataWithAgreement == 0); + if (scenario == 8) { + ASSERT(sender->dataAfterRequest == 1 && sender->dataAfterDelba == 1); + ASSERT(sender->firstData == (sender->requestStart + 1) % 4096); + } + else + ASSERT(sender->dataAfterRequest == 0 && sender->firstData == -1); ASSERT(!sender->hasAgreement(6)); ASSERT(sender->delbas == 1); - ASSERT(lifetimeDrops == (scenario == 9 ? 1 : 0)); + ASSERT(lifetimeDrops == (scenario == 9 ? 2 : 0)); } else if (scenario >= 5) { auto edca = check_and_cast(sender->getSubmodule("edca")); @@ -271,7 +285,8 @@ class DeferralTest : public cSimpleModule, public cListener { accept(sender, 6); } wait(SimTime(513, SIMTIME_MS) - simTime()); - ASSERT(lifetimeDrops == 1 && lifetime->findFrame(deferredId) == nullptr); + // Preview leaves both unsent frames under their original lifetime deadlines. + ASSERT(lifetimeDrops == 2 && lifetime->findFrame(deferredId) == nullptr); if (scenario == 6) ASSERT(sender->delbas == 1 && sender->dataAfterRequest == 0); else { @@ -284,7 +299,7 @@ class DeferralTest : public cSimpleModule, public cListener { << ", response deadline=" << sender->observedDeadline << ", original DATA deadline=0.512.\n"; } else if (scenario == 2) { - ASSERT(sender->dataAfterRequest == 1); + ASSERT(sender->dataAfterRequest == 2); ASSERT(sender->firstData == sender->requestStart); } else { @@ -315,9 +330,12 @@ class DeferralTest : public cSimpleModule, public cListener { ASSERT(!sender->hasAgreement(6)); } else { - ASSERT(sender->delbas == 0); - ASSERT(sender->hasAgreement(6)); - ASSERT(sender->dataAfterRequest == 1); + // The accepted response releases the final DATA before completion causes DELBA. + ASSERT(simTime() < sender->observedDeadline); + ASSERT(sender->delbas == 1); + ASSERT(!sender->hasAgreement(6)); + std::cout << "Completed deferred DATA: " << sender->dataAfterRequest << ".\n"; + ASSERT(sender->dataAfterRequest == 2); ASSERT(sender->firstData == sender->requestStart); } } diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test index 33429c70e6a..90b92add024 100644 --- a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -2,7 +2,7 @@ Check the sequence boundary after ordinary originator teardown of fragmented DATA. IEEE Std 802.11-2024, 11.5.3.4 requires the next MPDU to use a later sequence when the originator discards the last transmitted MPDU. -The fixture supplies accepted Basic agreements and injects immediate ACKs. +The fixture supplies accepted Basic agreements and injects immediate ACKs and Basic Block Ack responses. Production fragmentation, inactivity expiry, retirement, and transmission remain active. Runs cover sequence 0 and wraparound at 4095, with expiry during transmission or after the first fragment receives its Normal Ack. @@ -10,6 +10,7 @@ Later intact DATA and an unrelated peer must retain their transmissions. Additional runs transmit an earlier DATA unit before a gap in sequence allocation. The fixture extracts and disposes of the skipped DATA through the production allocator. Distances 2047, 2048, 2049, and 4096 cover the half-range boundary and a full wrap. +A deferred successor keeps the transfer open while the fixture advances the allocator. %extraargs: -r 0..19 @@ -45,6 +46,9 @@ class FragmentRetirementHcf : public Hcf { bool retired = false; bool successorSent = false; bool otherSent = false; + bool otherRetired = false; + Ptr pendingBar; + Packet *deferredSuccessor = nullptr; int delbas = 0; void skipData(int count) { @@ -75,7 +79,7 @@ class FragmentRetirementHcf : public Hcf { scheduleAfter(SimTime(acknowledgedFirst ? 650 : 974, SIMTIME_US), new cMessage("queue-data")); } - void queueData(const char *name, MacAddress address, int length) { + Packet *queueData(const char *name, MacAddress address, int length) { auto header = makeShared(); header->setType(ST_DATA_WITH_QOS); header->addChunkLength(QOSCONTROL_PART_LENGTH); @@ -87,6 +91,7 @@ class FragmentRetirementHcf : public Hcf { packet->insertAtBack(makeShared(B(length))); packet->insertAtBack(makeShared()); processUpperFrame(packet, header); + return packet; } public: @@ -106,9 +111,13 @@ class FragmentRetirementHcf : public Hcf { if (distance == 0) startRetirement(); else { + check_and_cast(originatorDataService)->setEligibility( + [this](Packet *packet) { return !packet->isName("deferred-successor") && isFrameEligible(packet, false); }, + [this](Packet *packet) { return isAMsduPermitted(packet); }); // Force Normal Ack for the earlier DATA, independent of the fragment ACK policy. handler->getAgreement(peer, 5)->setStartingSequenceNumber(SequenceNumberCyclic(start) + 1); queueData("earlier-data", peer, 20); + deferredSuccessor = queueData("deferred-successor", peer, 20); } scheduleAfter(SimTime(10, SIMTIME_MS), new cMessage("verify")); } @@ -119,6 +128,13 @@ class FragmentRetirementHcf : public Hcf { Hcf::receiveSignal(source, signal, object, details); return; } + auto agreement = check_and_cast(object); + if (agreement->getReceiverAddr() == other) { + ASSERT(otherSent); + ASSERT(!hasQueuedData(other, 5) && !hasNumberedData(other, 5)); + otherRetired = true; + return; + } ASSERT(!retired); retired = true; ASSERT(firstCompleted == acknowledgedFirst); @@ -164,6 +180,11 @@ class FragmentRetirementHcf : public Hcf { void transmissionComplete(Packet *packet, const Ptr& header) override { Enter_Method("transmissionComplete"); auto data = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); + if (bar) { + ASSERT(bar->getReceiverAddress() == other); + pendingBar = bar; + } bool earlier = packet->isName("earlier-data"); bool first = !earlier && data && data->getReceiverAddress() == peer && data->getSequenceNumber() == SequenceNumberCyclic(start); bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); @@ -174,6 +195,8 @@ class FragmentRetirementHcf : public Hcf { Hcf::transmissionComplete(packet, header); if (ack) scheduleAfter(SimTime(20, SIMTIME_US), new cMessage(earlier ? "ack-earlier" : first ? "ack-first" : "ack")); + if (bar) + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("block-ack")); } void handleMessage(cMessage *message) override { @@ -183,6 +206,24 @@ class FragmentRetirementHcf : public Hcf { queueData("intact-successor", peer, 20); queueData("other-peer", other, 20); } + else if (message->isName("block-ack")) { + delete message; + auto header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + header->setTransmitterAddress(other); + header->setTidInfo(5); + header->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + BitVector bits; + bits.appendBit(true); + for (int i = 1; i < 16; i++) + bits.appendBit(false); + header->setBlockAckBitmap(0, bits); + auto packet = new Packet("block-ack", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getMode(bps(24000000))); + pendingBar = nullptr; + processLowerFrame(packet, header); + } else if (message->isName("ack") || message->isName("ack-first") || message->isName("ack-earlier")) { bool first = message->isName("ack-first"); bool earlier = message->isName("ack-earlier"); @@ -196,6 +237,12 @@ class FragmentRetirementHcf : public Hcf { if (first) firstAcknowledged = true; if (earlier) { + auto queue = edca->getEdcaf(AC_VI)->getPendingQueue(); + queue->removePacket(deferredSuccessor); + take(deferredSuccessor); + edca->getLifetimeHandler()->forgetFrame(deferredSuccessor->getId()); + delete deferredSuccessor; + deferredSuccessor = nullptr; skipData(distance - 1); start = (start + distance) % 4096; startRetirement(); @@ -203,9 +250,11 @@ class FragmentRetirementHcf : public Hcf { } else if (message->isName("verify")) { delete message; + std::cout << "Fragment retirement state: retired=" << retired << " firstCompleted=" << firstCompleted + << " successorSent=" << successorSent << " otherSent=" << otherSent << " delbas=" << delbas << "\n"; ASSERT(retired && firstCompleted && successorSent && otherSent); - ASSERT(delbas == 1); - ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) != nullptr); + ASSERT(delbas == 2 && otherRetired); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(other, 5) == nullptr); unsubscribe(blockAckAgreementDeletedSignal, this); std::cout << "Fragment retirement passed: start=" << start << ", acknowledgedFirst=" << acknowledgedFirst << ", distance=" << distance << std::endl; diff --git a/tests/module/Ieee80211BlockAckScheduling_1.test b/tests/module/Ieee80211BlockAckScheduling_1.test new file mode 100644 index 00000000000..3fe472f6f68 --- /dev/null +++ b/tests/module/Ieee80211BlockAckScheduling_1.test @@ -0,0 +1,455 @@ +%description: +Exercise Basic BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. +The fixture supplies accepted agreements and injects responses at the HCF receive boundary. +Response events include SIFS and the selected response airtime. +Cases cover short bursts, windows smaller than the threshold, two peers, BAR timeout, +negative bitmap bits, Normal Ack window progress, and late BAR admission. + +%extraargs: -r 0..29 + +%file: Scheduling.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class SchedulingRecipientAgreements : public RecipientBlockAckAgreementHandler { + public: + void accept(MacAddress peer) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + agreement->addbaResposneSent(); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + } +}; +class SchedulingRecipient : public Hcf { + public: + std::vector delivered; + bool checkedBitmap = false; + void prepare() { + Enter_Method("prepare"); + delete recipientBlockAckAgreementHandler; + auto agreements = new SchedulingRecipientAgreements(); + recipientBlockAckAgreementHandler = agreements; + agreements->accept(MacAddress("02:00:00:00:00:01")); + subscribe(blockAckAgreementDeletedSignal, this); + } + void receive(Packet *packet) { + Enter_Method("receive"); + auto header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + if (data->getSequenceNumber() == SequenceNumberCyclic(0) && data->getFragmentNumber() != 0) { + delete packet; + return; + } + } + auto mode = packet->getTag()->getMode(); + packet->addTagIfAbsent()->setMode(mode); + processLowerFrame(packet, header); + } + bool retired() const { return recipientBlockAckAgreementHandler->getAgreements().empty(); } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == blockAckAgreementDeletedSignal) + ASSERT((delivered == std::vector{1, 2})); + else + Hcf::receiveSignal(source, signal, value, details); + } + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr& header, + Packet *, const Ptr&) override { + if (auto response = dynamicPtrCast(header)) { + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(response->getBlockAckBitmap(0).getBit(0)); + ASSERT(!response->getBlockAckBitmap(0).getBit(1)); + if (response->getBlockAckBitmap(1).getBit(0) && response->getBlockAckBitmap(2).getBit(0)) { + checkedBitmap = true; + ASSERT(delivered.empty()); + } + } + delete packet; + } +}; +Define_Module(SchedulingRecipient); +class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { + public: + void accept(MacAddress peer, int size) { + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTid(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBufferSize(size); + request->setBlockAckPolicy(true); + createAgreement(request); + auto agreement = getAgreement(peer, 5); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + finishProcedure(peer, 5); + } +}; +class SchedulingHcf : public Hcf { + const MacAddress peer{"02:00:00:00:00:02"}; + const MacAddress other{"02:00:00:00:00:03"}; + int dataCount = 0; + int bars = 0; + int delbas = 0; + int refusedBars = 0; + int retries = 0; + int dataInTxop = 0; + bool responsePending = false; + Ptr pendingBar; + const physicallayer::IIeee80211Mode *pendingResponseMode = nullptr; + bool firstBarWasStandalone = false; + int expired = 0; + int collisions = 0; + SchedulingRecipient *recipient = nullptr; + simtime_t responseEnd; + simtime_t txopDeadline; + int scenario() const { return par("scenario"); } + int burst() const { + switch (scenario()) { + case 0: return 1; + case 1: return 4; + case 2: return 6; + case 3: return 9; + case 4: return 4; + case 5: return 1; + case 6: return 1; + case 7: return 2; + case 9: return 3; + case 10: return 1; + case 11: return 8; + case 12: return 1; + case 13: return 1; + case 14: return 1; + default: return 4; + } + } + int window() const { return scenario() == 2 || scenario() == 7 ? 1 : scenario() == 3 ? 4 : 64; } + public: + void begin() { + Enter_Method("begin"); + delete originatorBlockAckAgreementHandler; + auto agreements = new SchedulingAgreements(); + originatorBlockAckAgreementHandler = agreements; + agreements->accept(peer, window()); + if (scenario() == 9) { + recipient = check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf")); + recipient->prepare(); + subscribe(packetDroppedSignal, this); + } + if (scenario() == 4) + agreements->accept(other, 64); + rx->setContentionBlocked(true); + for (int i = 0; i < burst(); i++) + queueData(peer, scenario() == 9 ? (i == 0 ? 200 : 10) : scenario() == 7 && i == 0 ? 1100 : 100); + if (scenario() == 12 || scenario() == 14) { + auto frames = edca->getEdcaf(AC_VI)->getInProgressFrames(); + auto frame = frames->getFrameToTransmit(); + auto header = frame->removeAtFront(); + header->setAckPolicy(BLOCK_ACK); + frame->insertAtFront(header); + edca->getEdcaf(AC_VI)->getAckHandler()->processTransmittedDataOrMgmtFrame(header); + originatorBlockAckAgreementHandler->getAgreement(peer, 5)->recordTransmittedData( + frame->getTag()->getAllocationIndex()); + dataCount = 1; + queueData(other, 1100, 6); + } + if (scenario() == 4) { + for (int i = 0; i < 8; i++) + queueData(other, 100); + } + rx->setContentionBlocked(false); + scheduleAfter(SimTime(40, SIMTIME_MS), new cMessage("verify")); + } + protected: + void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { + if (signal == packetDroppedSignal) { + auto drop = dynamic_cast(details); + if (drop && drop->getReason() == LIFETIME_EXPIRED) + expired++; + if (source == this) + return; + } + Hcf::receiveSignal(source, signal, value, details); + } + void queueData(MacAddress receiver, int length, Tid tid = 5) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setAddress3(receiver); + auto packet = new Packet("data", header); + packet->insertAtBack(makeShared(B(length))); + packet->insertAtBack(makeShared()); + processUpperFrame(packet, header); + } + void frameSequenceStarted() override { + dataInTxop = 0; + Hcf::frameSequenceStarted(); + } + void channelGranted(IChannelAccess *access) override { + auto collided = edca->getInternallyCollidedEdcafs(); + if (scenario() == 12 || scenario() == 14) + collisions += collided.size(); + Hcf::channelGranted(access); + if (scenario() == 14 && !collided.empty()) { + auto edcaf = edca->getEdcaf(AC_VI); + ASSERT(edcaf->getCw() == 1); + auto frame = edcaf->getInProgressFrames()->getFrames(0); + bool absent = false; + try { + edcaf->getRecoveryProcedure()->getRetryCount(frame, frame->peekAtFront()); + } + catch (const cRuntimeError& error) { + absent = std::string(error.what()).find("retry counter entry doesn't exist") != std::string::npos; + } + ASSERT(absent); + } + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + auto bar = dynamicPtrCast(header); + if (bar) { + auto frames = edca->getChannelOwner()->getInProgressFrames(); + auto candidate = frames->peekFrameToTransmit(); + if (candidate && !frames->containsFrame(candidate)) { + auto sequence = candidate->peekAtFront()->getSequenceNumber(); + auto size = frames->getLength(); + ASSERT(frames->peekFrameToTransmit() == candidate); + ASSERT(frames->getLength() == size); + ASSERT(candidate->peekAtFront()->getSequenceNumber() == sequence); + } + } + bool accepted = Hcf::transmitFrame(packet, ifs); + if (bar && !accepted) { + refusedBars++; + return false; + } + if (!accepted) + return false; + header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) { + auto txop = edca->getChannelOwner()->getTxopProcedure(); + if (!par("multiple").boolValue() && txop->getLimit() == 0 && data->getAckPolicy() == BLOCK_ACK) + ASSERT(data->getDurationField() == SIMTIME_ZERO); + if (!par("multiple").boolValue() && scenario() == 3) { + ASSERT(dataInTxop == 0); + ASSERT(data->getDurationField() == SIMTIME_ZERO); + } + if (scenario() == 4 && data->getReceiverAddress() == other) + ASSERT(bars >= 1); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + if ((scenario() == 12 || scenario() == 14) && data->getTid() == 6) + ASSERT(!data->getRetry()); + else + ASSERT(agreement != nullptr && agreement->containsSequence(data->getSequenceNumber())); + if (scenario() != 7 && data->getSequenceNumber().get() >= window() && data->getReceiverAddress() == peer) + ASSERT(bars >= 1); + if (data->getRetry()) + retries++; + dataCount++; + dataInTxop++; + } + if (bar) { + bars++; + if (bars == 1) { + firstBarWasStandalone = dataInTxop == 0; + ASSERT(bar->getReceiverAddress() == peer); + ASSERT(bar->getTidInfo() == 5); + if (scenario() != 9) + ASSERT(dataCount == (scenario() == 2 ? 1 : scenario() == 3 ? 4 : scenario() == 7 || scenario() == 12 || scenario() == 14 ? 2 : scenario() == 11 ? 5 : burst())); + } + auto mode = packet->getTag()->getMode(); + auto responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_BASIC_BLOCKACK); + auto txop = edca->getChannelOwner()->getTxopProcedure(); + auto end = simTime() + ifs + cost; + if (dataInTxop > 1 && txop->getLimit() > 0) + ASSERT(end <= txop->getStart() + txop->getLimit()); + std::cout << "BAR timing: scenario=" << scenario() << " multiple=" << par("multiple").boolValue() + << " start=" << simTime() << " ifs=" << ifs << " bytes=" << packet->getByteLength() + << " rate=" << mode->getDataMode()->getNetBitrate() << " responseRate=" << responseMode->getDataMode()->getNetBitrate() + << " response=" << responseMode->getDuration(LENGTH_BASIC_BLOCKACK) << " end=" << end + << " deadline=" << txop->getStart() + txop->getLimit() << " data=" << dataInTxop << "\n"; + } + if (auto delba = dynamicPtrCast(header)) { + ASSERT(delba->getInitiator()); + ASSERT(!hasQueuedData(delba->getReceiverAddress(), delba->getTid())); + ASSERT(!hasNumberedData(delba->getReceiverAddress(), delba->getTid())); + delbas++; + } + return true; + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + Enter_Method("transmissionComplete"); + auto bar = dynamicPtrCast(header); + auto data = dynamicPtrCast(header); + if (recipient && (data || bar || dynamicPtrCast(header))) + recipient->receive(packet->dup()); + bool ack = dynamicPtrCast(header) || (data && data->getAckPolicy() == NORMAL_ACK); + if (bar || ack) { + if (!(bar && scenario() == 5 && bars == 1)) { + pendingBar = bar; + responsePending = true; + pendingResponseMode = bar ? + rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT) : + rateSelection->computeResponseAckFrameMode(packet, dynamicPtrCast(header), ResponseQueryRole::PREDICT); + auto duration = pendingResponseMode->getDuration(bar ? LENGTH_BASIC_BLOCKACK : LENGTH_ACK); + responseEnd = simTime() + modeSet->getSifsTime() + duration; + txopDeadline = edca->getChannelOwner()->getTxopProcedure()->getStart() + edca->getChannelOwner()->getTxopProcedure()->getLimit(); + scheduleAt(responseEnd, new cMessage("response")); + } + } + Hcf::transmissionComplete(packet, header); + } + bool isReceptionInProgress() override { return responsePending || Hcf::isReceptionInProgress(); } + void handleMessage(cMessage *message) override { + if (message->isName("response")) { + delete message; + responsePending = false; + Ptr header; + if (pendingBar) { + auto response = makeShared(); + response->setTransmitterAddress(pendingBar->getReceiverAddress()); + response->setTidInfo(pendingBar->getTidInfo()); + response->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + for (int i = 0; i < 64; i++) { + BitVector bits; + for (int j = 0; j < 16; j++) + bits.appendBit(scenario() == 9 ? (i != 0 || j == 0) : !(scenario() == 6 && bars == 1)); + response->setBlockAckBitmap(i, bits); + } + header = response; + } + else + header = makeShared(); + header->setReceiverAddress(mac->getAddress()); + auto packet = new Packet("response", header); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(pendingResponseMode); + pendingBar = nullptr; + processLowerFrame(packet, header); + } + else if (message->isName("verify")) { + delete message; + std::cout << "Scheduling totals: data=" << dataCount << " bars=" << bars << " delbas=" << delbas << " refused=" << refusedBars << "\n"; + ASSERT(bars > 0); + ASSERT(delbas == (scenario() == 4 ? 2 : 1)); + if (scenario() != 9) + ASSERT(dataCount == burst() + (scenario() == 4 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 ? 1 : 0)); + else { + ASSERT(recipient->checkedBitmap && recipient->retired()); + ASSERT((recipient->delivered == std::vector{1, 2})); + ASSERT(dataCount > 3); + ASSERT(expired > 0); + } + ASSERT(originatorBlockAckAgreementHandler->getAgreements().empty()); + ASSERT(originatorBlockAckAgreementHandler->getProcedures().empty()); + ASSERT(edca->getLifetimeHandler()->getNumFrames() == 0); + ASSERT(!frameSequenceHandler->isSequenceRunning()); + if (scenario() == 5 || scenario() == 6) + ASSERT(retries == 1 && bars == 2); + if (scenario() == 8 || scenario() == 10 || scenario() == 13) + ASSERT(firstBarWasStandalone); + if (scenario() == 12 || scenario() == 14) + ASSERT(collisions == 1 && retries == 0); + std::cout << "Block Ack scheduling assertions passed.\n"; + endSimulation(); + } + else + Hcf::handleMessage(message); + } +}; +Define_Module(SchedulingHcf); +class SchedulingTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("begin")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->begin(); + } +}; +Define_Module(SchedulingTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module SchedulingHcf extends Hcf { parameters: int scenario; bool multiple; @class(::inet::ieee80211::SchedulingHcf); } +module SchedulingRecipient extends Hcf { parameters: @class(::inet::ieee80211::SchedulingRecipient); } +simple SchedulingTest { parameters: @class(::inet::ieee80211::SchedulingTest); } +network SchedulingNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + recipient: WirelessHost; + test: SchedulingTest; +} + +%inifile: omnetpp.ini +[General] +network = SchedulingNetwork +ned-path = .;../../../../src +sim-time-limit = 50ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "a" +**.wlan[*].bitrate = 6Mbps +*.host.wlan[*].address = "02:00:00:00:00:01" +*.recipient.wlan[*].address = "02:00:00:00:00:02" +**.wlan[*].mac.qosStation = true +*.recipient.wlan[*].mac.hcf.typename = "SchedulingRecipient" +*.host.wlan[*].mac.hcf.typename = "SchedulingHcf" +**.wlan[*].mac.hcf.scenario = ${scenario=0..14} +**.wlan[*].mac.hcf.multiple = ${multiple=false,true} +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms +**.wlan[*].mac.hcf.originatorAckPolicy.blockAckTimeout = 1ms +**.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 2346B +**.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 ? 128B : 2346B +**.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 10ms : 512ms +**.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[2].cwMax = ${scenario} == 14 ? 3 : ${scenario} == 12 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[2].recoveryProcedure.shortRetryLimit = ${scenario} == 14 ? 1 : 7 +**.wlan[*].mac.hcf.edca.edcaf[*].cwMax = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 +**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = ${multiple} ? "multiple" : "single" +**.wlan[*].mac.hcf.edca.edcaf[2].txopProcedure.txopLimit = ${scenario} == 8 ? 900us : ${scenario} == 10 ? 0s : ${scenario} == 11 ? 1160us : ${scenario} == 13 ? 50us : 10ms +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 2000000m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.recipient.mobility.initialX = 1000000m +*.host.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Block Ack scheduling assertions passed. diff --git a/tests/module/Ieee80211HcfHeader_1.test b/tests/module/Ieee80211HcfHeader_1.test index 81703858adf..595c2b1b6b7 100644 --- a/tests/module/Ieee80211HcfHeader_1.test +++ b/tests/module/Ieee80211HcfHeader_1.test @@ -20,7 +20,7 @@ class HeaderAckPolicy : public OriginatorQosAckPolicy { selections++; return BLOCK_ACK; } - bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *) const override { return false; } + bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return false; } }; Define_Module(HeaderAckPolicy); class HeaderAgreementHandler : public OriginatorBlockAckAgreementHandler { diff --git a/tests/module/Ieee80211HcfMultipleProtection_2.test b/tests/module/Ieee80211HcfMultipleProtection_2.test index 748c19d83ef..1d278478756 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_2.test +++ b/tests/module/Ieee80211HcfMultipleProtection_2.test @@ -116,6 +116,7 @@ Define_Module(RateProtectionMac); class RateProtectionHcf : public Hcf { public: int acknowledgments = 0; + int managementAcknowledgments = 0; int blockAcks = 0; int failures = 0; void seedOriginator() { @@ -168,7 +169,12 @@ class RateProtectionHcf : public Hcf { } void originatorProcessReceivedFrame(Packet *packet, Packet *request) override { auto type = packet->peekAtFront()->getType(); - if (type == ST_ACK) acknowledgments++; + if (type == ST_ACK) { + if (dynamicPtrCast(request->peekAtFront())) + acknowledgments++; + else + managementAcknowledgments++; + } if (type == ST_BLOCKACK) blockAcks++; Hcf::originatorProcessReceivedFrame(packet, request); } @@ -208,12 +214,13 @@ class RateProtectionTest : public cSimpleModule { else { sender->inject(66, scenario == 6); if (scenario == 0 || scenario == 1) - sender->inject(); // The existing BAR branch needs a next selected frame. + sender->inject(); } wait(SimTime(5, SIMTIME_MS)); if (scenario == 0 || scenario == 1) { ASSERT(sender->blockAcks >= 1); ASSERT(sender->acknowledgments == 0); + ASSERT(sender->managementAcknowledgments == 1); ASSERT(std::find(senderMac->types.begin(), senderMac->types.end(), ST_BLOCKACK_REQ) != senderMac->types.end()); ASSERT(receiverMac->types[0] == ST_CTS); auto firstRtsEndpoint = senderMac->ends[0] + senderMac->durations[0]; diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test new file mode 100644 index 00000000000..c0fd4049ab9 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -0,0 +1,153 @@ +%description: +Check peer/TID BAR arbitration, window boundaries, cyclic sequence order, and ACK-state isolation. +The production policy reads the production ACK handler through an in-progress queue. +These helper checks supplement the HCF module fixture. + +%includes: +#include +#include +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +using namespace inet; +using namespace inet::ieee80211; + +namespace Ieee80211BlockAckScheduling_1 { +template struct ImplicitBarQuery : std::false_type {}; +template struct ImplicitBarQuery().isBlockAckReqNeeded(nullptr, nullptr))>> : std::true_type {}; +template struct ImplicitBarParameters : std::false_type {}; +template struct ImplicitBarParameters().computeBlockAckReqParameters(nullptr, nullptr))>> : std::true_type {}; +template struct ImplicitBarWindow : std::false_type {}; +template struct ImplicitBarWindow().processTransmittedBlockAckReq(std::declval>()))>> : std::true_type {}; + +static_assert(!ImplicitBarQuery::value, "The interface requires an explicit agreement handler"); +static_assert(!ImplicitBarQuery::value, "The concrete policy requires an explicit agreement handler"); +static_assert(!ImplicitBarParameters::value, "The interface requires an explicit agreement handler"); +static_assert(!ImplicitBarParameters::value, "The concrete policy requires an explicit agreement handler"); +static_assert(!ImplicitBarWindow::value, "The ACK handler requires an explicit agreement window"); + +class SchedulingCw : public IRecoveryProcedure::ICwCalculator { + public: + int cw = 0; + void incrementCw() override { cw = std::min(7, 2 * cw + 1); } + void resetCw() override { cw = 0; } + int getCw() override { return cw; } +}; +class SchedulingRecovery : public QosRecoveryProcedure { + public: + void configure(ICwCalculator *calculator) { cwCalculator = calculator; shortRetryLimit = 1; } + int getStationSrc() const { return stationShortRetryCounter; } + bool hasDataRetry() const { return !shortRetryCounter.empty() || !longRetryCounter.empty(); } +}; +class SchedulingPolicy : public OriginatorQosAckPolicy { + public: + SchedulingPolicy() { blockAckReqThreshold = 5; } +}; +class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { + public: + OriginatorBlockAckAgreement *accept(MacAddress peer, Tid tid, int start, int size) { + auto agreement = new OriginatorBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), size, false, false); + agreement->setIsAddbaResponseReceived(true); + blockAckAgreements[{peer, tid}] = agreement; + return agreement; + } +}; +class SchedulingFrames : public InProgressFrames { + public: + Packet *candidate = nullptr; + void configure(QosAckHandler *handler) { ackHandler = handler; } + Packet *peekFrameToTransmit() override { return candidate; } + Ptr add(MacAddress peer, Tid tid, int sequence, bool sent = true) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(peer); + header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setAckPolicy(BLOCK_ACK); + auto packet = new Packet("data", header); + take(packet); + packet->setArrivalTime(simTime()); + inProgressFrames.push_back(packet); + ackHandler->frameGotInProgress(header); + if (sent) + check_and_cast(ackHandler)->processTransmittedDataOrMgmtFrame(header); + return header; + } +}; +} + +%activity: +SchedulingCw cw; +SchedulingRecovery recovery; +recovery.configure(&cw); +recovery.controlFrameTransmissionFailed(); +ASSERT(recovery.getStationSrc() == 1 && cw.getCw() == 1); +recovery.controlFrameTransmissionFailed(); +ASSERT(recovery.getStationSrc() == 0 && cw.getCw() == 0); +recovery.controlFrameTransmissionFailed(); +recovery.blockAckFrameReceived(); +ASSERT(recovery.getStationSrc() == 0 && cw.getCw() == 0 && !recovery.hasDataRetry()); +const MacAddress peer("02:00:00:00:00:02"); +const MacAddress other("02:00:00:00:00:03"); +SchedulingPolicy policy; +SchedulingAgreements agreements; +QosAckHandler ack; +SchedulingFrames frames; +frames.configure(&ack); +agreements.accept(peer, 2, 100, 8); +auto selectedAgreement = agreements.accept(peer, 5, 4094, 4); +auto otherAgreement = agreements.accept(other, 5, 4094, 4); +std::vector> unrelated; +for (int i = 100; i < 104; i++) + unrelated.push_back(frames.add(peer, 2, i)); +auto selected = frames.add(peer, 5, 4095); +auto wrap = frames.add(peer, 5, 0); +auto outside = frames.add(peer, 5, 2); +auto separatePeer = frames.add(other, 5, 4095); +// A candidate inside TID 2 defers only that group. +frames.add(peer, 2, 104, false); +frames.candidate = frames.getFrames(frames.getLength() - 1); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, nullptr)); +ASSERT(std::get<2>(policy.computeBlockAckReqParameters(&frames, nullptr, nullptr)) == -1); +auto choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 5); +ASSERT(std::get<1>(choice) == SequenceNumberCyclic(4095)); +ASSERT(selectedAgreement->containsSequence(SequenceNumberCyclic(1))); +ASSERT(!selectedAgreement->containsSequence(SequenceNumberCyclic(2))); +auto bar = makeShared(); +bar->setReceiverAddress(peer); +bar->setTidInfo(5); +bar->setStartingSequenceNumber(SequenceNumberCyclic(4095)); +ack.processTransmittedBlockAckReq(bar, selectedAgreement); +ASSERT(ack.getQoSDataAckStatus(selected) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +ASSERT(ack.getQoSDataAckStatus(wrap) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); +for (auto header : unrelated) + ASSERT(ack.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ASSERT(ack.getQoSDataAckStatus(outside) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ASSERT(ack.getQoSDataAckStatus(separatePeer) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +ack.processFailedBlockAckReq(bar); +ASSERT(ack.isEligibleToTransmit(selected)); +ASSERT(ack.isEligibleToTransmit(wrap)); +ASSERT(ack.getQoSDataAckStatus(separatePeer) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +frames.candidate = nullptr; +choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); +ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 2); +ASSERT(std::get<1>(choice) == SequenceNumberCyclic(100)); +ASSERT(otherAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(4094)); +auto duration = SimTime(1, SIMTIME_US); +EdcaTransmitLifetimeHandler lifetime(duration, duration, duration, duration); +frames.setLifetimeHandler(&lifetime); +for (int i = 0; i < frames.getLength(); i++) + lifetime.registerFrame(frames.getFrames(i), AC_VI); +ASSERT(policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +wait(duration); +ASSERT(!policy.isBlockAckReqNeeded(&frames, nullptr, &agreements)); +ASSERT(ack.getQoSDataAckStatus(unrelated.front()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); +std::cout << "Block Ack scheduling isolation assertions passed.\n"; + +%contains: stdout +Block Ack scheduling isolation assertions passed. diff --git a/tests/unit/Ieee80211MultipleProtection_1.test b/tests/unit/Ieee80211MultipleProtection_1.test index 1867858da01..d777b2e0e56 100644 --- a/tests/unit/Ieee80211MultipleProtection_1.test +++ b/tests/unit/Ieee80211MultipleProtection_1.test @@ -39,11 +39,12 @@ class PredicateFrames : public InProgressFrames { int reads = 0; bool hasInProgressFrames() override { return true; } Packet *getFrameToTransmit() override { reads++; return selected; } + Packet *peekFrameToTransmit() override { reads++; return selected; } }; class PredicateAckPolicy : public OriginatorQosAckPolicy { public: bool bar = false; - bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *) const override { return bar; } + bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return bar; } }; class PredicateRtsPolicy : public IRtsPolicy { public: From 728d3dd911018b1ae8d3ae4f7fa0c3184fe15d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:47:24 +0200 Subject: [PATCH 07/17] ieee80211: refactor: centralize originator window operations Before this commit, HCF calculates window advancement, while ACK callers repeat modulo-4096 distance calculations. Their results are correct, but separate definitions make later maintenance harder. After this commit, OriginatorBlockAckAgreement owns sequence distance, bitmap membership, and window advancement. HCF supplies unresolved sequence numbers for the same peer and TID. ACK policy and feedback use the shared operations. Existing virtual window accessors remain in the advancement path. For example, a window at 4094 advances to unresolved sequence 4095 and next to sequence 0 after resolution. With no unresolved DATA, it advances to the next allocation. Boundary tests also check the distinct 64-position bitmap range. Packet behavior, ownership, event order, and recorded expectations remain unchanged. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | refactor | test | ieee80211-compressed-blockack-replacement --- .../blockack/OriginatorBlockAckAgreement.h | 22 ++++++++++++++++++- .../ieee80211/mac/coordinationfunction/Hcf.cc | 7 +++--- .../mac/originator/OriginatorQosAckPolicy.cc | 6 ++--- .../ieee80211/mac/originator/QosAckHandler.cc | 4 ++-- tests/unit/Ieee80211BlockAckScheduling_1.test | 11 ++++++++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index adc12872b27..c760205173a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -8,6 +8,8 @@ #ifndef __INET_ORIGINATORBLOCKACKAGREEMENT_H #define __INET_ORIGINATORBLOCKACKAGREEMENT_H +#include + #include "inet/linklayer/ieee80211/mac/common/SequenceControlField.h" namespace inet { @@ -48,9 +50,27 @@ class INET_API OriginatorBlockAckAgreement : public cObject 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 (sequence.get() - startingSequenceNumber.get() + 4096) % 4096 < getWindowSize(); + 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; } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 7c87cbc0622..e968aabbee7 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -537,20 +537,19 @@ void Hcf::advanceOriginatorWindows() auto agreement = entry.second; if (!agreement->getIsAddbaResponseReceived()) continue; - auto start = agreement->getStartingSequenceNumber(); auto next = service->getNextSequenceNumber(entry.first.first, entry.first.second); - int distance = (next.get() - start.get() + 4096) % 4096; + 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) - distance = std::min(distance, (data->getSequenceNumber().get() - start.get() + 4096) % 4096); + unresolvedSequences.push_back(data->getSequenceNumber()); } } // IEEE Std 802.11-2024, 10.25.6.7.1: resolved positions permit transmit-window progress. - agreement->setStartingSequenceNumber(start + distance); + agreement->advanceWindow(next, unresolvedSequences); } } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index bf624914ed6..dbdfdf50d0b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -61,12 +61,12 @@ std::tuple OriginatorQosAckPolicy::comput agreement->containsSequence(frames->getCandidateSequenceNumber(candidate))) continue; auto start = agreement->getStartingSequenceNumber(); - int oldestOffset = 4096; + int oldestOffset = -1; simtime_t arrival = SIMTIME_MAX; for (auto frame : entry.second) { auto sequence = frame->peekAtFront()->getSequenceNumber(); - int offset = (sequence.get() - start.get() + 4096) % 4096; - if (offset < oldestOffset) { + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(start, sequence); + if (oldestOffset == -1 || offset < oldestOffset) { oldestOffset = offset; arrival = frame->getArrivalTime(); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index fbd5c8d624b..c79877d4a72 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -216,14 +216,14 @@ void QosAckHandler::processTransmittedBlockAckReq(const Ptr(blockAckReq)) { if (basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && (sequence.get() - startingSeqNum.get() + 4096) % 4096 < 64) + 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 && (sequence.get() - startingSeqNum.get() + 4096) % 4096 < 64 && seqCtrlField.getFragmentNumber() == 0) + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && OriginatorBlockAckAgreement::isInBitmap(sequence, startingSeqNum) && seqCtrlField.getFragmentNumber() == 0) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test index c0fd4049ab9..aeb1b29a0f3 100644 --- a/tests/unit/Ieee80211BlockAckScheduling_1.test +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -93,6 +93,17 @@ 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; From 1f3edba0c62f93e708ce270bc2dc48a702ccbd14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:48:05 +0200 Subject: [PATCH 08/17] physicallayer: add: retain copied physical timing evidence Before this commit, interval consumers need retained transmission pointers or new propagation calculations. Cache expiry removes the relevant objects, so later MAC decisions cannot safely use those pointers. After this commit, radio media supply copied transmission endpoints and cached arrival endpoints. A snapshot survives cache expiry without object pointers. The receiver includes the transmission identifier in its local signal-time indication. Later delivery can use that identity to locate the evidence. For example, a recipient can retain arrival endpoints after the medium removes the reception. Const cache queries return absent results without insertion. Missing or identical transmission identities supply no interval evidence. The direct fixture checks copied values and absent states. The shared mechanism supplies no protocol-specific interval decision. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.physicallayer.wireless.common | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 + doc/src/migration-guide/index.rst | 7 + .../common/base/packetlevel/ReceiverBase.cc | 1 + .../MapCommunicationCache.cc | 10 ++ .../MapCommunicationCache.h | 1 + .../ReferenceCommunicationCache.cc | 10 ++ .../ReferenceCommunicationCache.h | 1 + .../VectorCommunicationCache.cc | 14 +- .../VectorCommunicationCache.h | 1 + .../packetlevel/ICommunicationCache.h | 2 + .../contract/packetlevel/IRadioMedium.h | 21 +++ .../common/contract/packetlevel/SignalTag.msg | 1 + .../packetlevel/TransmissionTimingSnapshot.h | 31 ++++ .../wireless/common/medium/RadioMedium.cc | 34 +++++ .../wireless/common/medium/RadioMedium.h | 3 + tests/unit/TransmissionTimingSnapshot_1.test | 138 ++++++++++++++++++ 16 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h create mode 100644 tests/unit/TransmissionTimingSnapshot_1.test diff --git a/WHATSNEW b/WHATSNEW index eecb155d1e0..95420dffc08 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -454,6 +454,10 @@ Notable backward incompatible changes are the following: 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. +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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index a3d5e9cb492..7dc296dc880 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -14,6 +14,13 @@ 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. +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 Basic Block Ack extension points ------------------------------------------ diff --git a/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc b/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc index a3c828e2457..e1aceee6450 100644 --- a/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc +++ b/src/inet/physicallayer/wireless/common/base/packetlevel/ReceiverBase.cc @@ -102,6 +102,7 @@ const IReceptionResult *ReceiverBase::computeReceptionResult(const IListening *l auto signalTimeInd = packet->addTagIfAbsent(); signalTimeInd->setStartTime(reception->getStartTime()); signalTimeInd->setEndTime(reception->getEndTime()); + signalTimeInd->setTransmissionId(reception->getTransmission()->getId()); if (snir->getMax() == 0) { auto errorRateInd = packet->addTagIfAbsent(); errorRateInd->setSymbolErrorRate(1); diff --git a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc index b5632d21994..c5032d10a4d 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.cc @@ -14,6 +14,16 @@ namespace physicallayer { Define_Module(MapCommunicationCache); +const IArrival *MapCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + auto entry = transmissionCache.find(transmission->getId()); + if (entry == transmissionCache.end() || !entry->second.receptionCacheEntries) + return nullptr; + const auto& receptions = *entry->second.receptionCacheEntries; + auto reception = receptions.find(receiver->getId()); + return reception == receptions.end() ? nullptr : reception->second.arrival; +} + MapCommunicationCache::~MapCommunicationCache() { for (auto& it : transmissionCache) diff --git a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h index e9d9c8fa0e5..8ea9821446c 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h @@ -47,6 +47,7 @@ class INET_API MapCommunicationCache : public CommunicationCacheBase public: virtual ~MapCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "MapCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc index b735ca369e3..2fa4300abc4 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.cc @@ -14,6 +14,16 @@ namespace physicallayer { Define_Module(ReferenceCommunicationCache); +const IArrival *ReferenceCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + int transmissionIndex = transmission->getId(); + int radioIndex = receiver->getId(); + if (transmissionIndex < 0 || transmissionIndex >= int(transmissionCache.size()) || radioIndex < 0) + return nullptr; + auto receptions = transmissionCache[transmissionIndex].receptionCacheEntries; + return receptions && radioIndex < int(receptions->size()) ? receptions->at(radioIndex).arrival : nullptr; +} + ReferenceCommunicationCache::~ReferenceCommunicationCache() { for (auto& transmissionCacheEntry : transmissionCache) diff --git a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h index 8c27a6322ad..b4de8fa4887 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h @@ -42,6 +42,7 @@ class INET_API ReferenceCommunicationCache : public CommunicationCacheBase public: virtual ~ReferenceCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "ReferenceCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc index 3351dc55b87..5e2f96da7b2 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc +++ b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.cc @@ -14,6 +14,17 @@ namespace physicallayer { Define_Module(VectorCommunicationCache); +const IArrival *VectorCommunicationCache::findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const +{ + int transmissionIndex = transmission->getId() - baseTransmissionId; + int radioIndex = receiver->getId() - baseRadioId; + if (baseTransmissionId < 0 || baseRadioId < 0 || transmissionIndex < 0 || + transmissionIndex >= int(transmissionCache.size()) || radioIndex < 0) + return nullptr; + auto receptions = transmissionCache[transmissionIndex].receptionCacheEntries; + return receptions && radioIndex < int(receptions->size()) ? receptions->at(radioIndex).arrival : nullptr; +} + VectorCommunicationCache::~VectorCommunicationCache() { for (auto& transmissionCacheEntry : transmissionCache) @@ -141,7 +152,8 @@ void VectorCommunicationCache::removeTransmission(const ITransmission *transmiss const ITransmission *VectorCommunicationCache::getTransmission(int id) const { - ASSERT(baseTransmissionId != -1); + if (baseTransmissionId == -1) + return nullptr; int transmissionIndex = id - baseTransmissionId; if (0 <= transmissionIndex && transmissionIndex < (int)transmissionCache.size()) return transmissionCache[transmissionIndex].transmission; diff --git a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h index 3b40aee60bf..3f8e0dbeea0 100644 --- a/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h @@ -60,6 +60,7 @@ class INET_API VectorCommunicationCache : public CommunicationCacheBase public: virtual ~VectorCommunicationCache(); + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const override; virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "VectorCommunicationCache"; } diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h index 8688c8d06e0..55f2385206c 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/ICommunicationCache.h @@ -65,6 +65,8 @@ class INET_API ICommunicationCache : public virtual IPrintableObject /** @name Reception Cache */ //@{ virtual const IArrival *getCachedArrival(const IRadio *receiver, const ITransmission *transmission) = 0; + // This lookup returns nullptr for absent state. It allocates no cache entry. + virtual const IArrival *findCachedArrival(const IRadio *receiver, const ITransmission *transmission) const = 0; virtual void setCachedArrival(const IRadio *receiver, const ITransmission *transmission, const IArrival *arrival) = 0; virtual void removeCachedArrival(const IRadio *receiver, const ITransmission *transmission) = 0; diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h index a56ffb5d8af..0a70986456a 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h @@ -8,6 +8,8 @@ #ifndef __INET_IRADIOMEDIUM_H #define __INET_IRADIOMEDIUM_H +#include + #include "inet/physicallayer/wireless/common/contract/packetlevel/IWirelessSignal.h" #include "inet/common/packet/Packet.h" #include "inet/environment/contract/IMaterial.h" @@ -25,6 +27,7 @@ #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IReceptionDecision.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/ISnir.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h" namespace inet { namespace physicallayer { @@ -171,6 +174,24 @@ class INET_API IRadioMedium : public virtual IPrintableObject */ virtual const ITransmission *getTransmission(int id) const = 0; + /** + * Copies the physical endpoints of a live transmission without new propagation calculations. + * An absent transmission returns a snapshot with transmissionId equal to -1. + * An absent radio entry means that the medium has no cached arrival for that radio. + * The caller owns the returned value. Later cache removal does not change it. + */ + virtual TransmissionTimingSnapshot getTransmissionTimingSnapshot(int id) const = 0; + + /** + * Returns the interval before the current transmission at its transmitter. + * For the same transmitter, the interval starts at the previous transmission end. + * For different transmitters, it starts at the previous signal arrival end at the current transmitter. + * Missing current transmission or arrival data returns no value. + * The method uses the supplied snapshot and changes no cache or propagation state. + */ + virtual std::optional computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const = 0; + /** * Returns the listening decision that describes what the receiver detects * on the medium. diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg b/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg index abca21a254b..cf9802507b8 100644 --- a/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/SignalTag.msg @@ -126,4 +126,5 @@ class SignalTimeInd extends SignalTagBase { simtime_t startTime; simtime_t endTime; + int transmissionId = -1; // Medium transmission identity; -1 means unavailable. } diff --git a/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h b/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h new file mode 100644 index 00000000000..312e9bc7476 --- /dev/null +++ b/src/inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_TRANSMISSIONTIMINGSNAPSHOT_H +#define __INET_TRANSMISSIONTIMINGSNAPSHOT_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +/** + * This value copies the physical endpoints of one transmission from the medium cache. + * The endpoints retain the propagation result that the medium used for each radio. + * No packet, transmission, arrival, or radio pointer remains in this value. + * A transmissionId of -1 means that the medium no longer has the transmission. + */ +struct INET_API TransmissionTimingSnapshot +{ + int transmissionId = -1; + int transmitterRadioId = -1; + simtime_t startTime = -1; // First transmitted bit, in seconds. + simtime_t endTime = -1; // Last transmitted bit, in seconds. + std::map arrivalEndTimes; // Radio ID to last received bit, in seconds. +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc b/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc index 1366587e9fb..48fe60a49d6 100644 --- a/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc +++ b/src/inet/physicallayer/wireless/common/medium/RadioMedium.cc @@ -656,6 +656,40 @@ const ITransmission *RadioMedium::getTransmission(int id) const return communicationCache->getTransmission(id); } +TransmissionTimingSnapshot RadioMedium::getTransmissionTimingSnapshot(int id) const +{ + TransmissionTimingSnapshot snapshot; + auto transmission = communicationCache->getTransmission(id); + if (!transmission) + return snapshot; + snapshot.transmissionId = transmission->getId(); + snapshot.transmitterRadioId = transmission->getTransmitterRadioId(); + snapshot.startTime = transmission->getStartTime(); + snapshot.endTime = transmission->getEndTime(); + communicationCache->mapRadios([&](const IRadio *radio) { + if (!radio || !radio->getReceiver() || radio->getId() == snapshot.transmitterRadioId) + return; + auto arrival = communicationCache->findCachedArrival(radio, transmission); + if (arrival) + snapshot.arrivalEndTimes[radio->getId()] = arrival->getEndTime(); + }); + return snapshot; +} + +std::optional RadioMedium::computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const +{ + auto current = communicationCache->getTransmission(currentTransmissionId); + if (!current || preceding.transmissionId < 0 || preceding.transmissionId == currentTransmissionId) + return std::nullopt; + if (current->getTransmitterRadioId() == preceding.transmitterRadioId) + return current->getStartTime() - preceding.endTime; + auto arrival = preceding.arrivalEndTimes.find(current->getTransmitterRadioId()); + if (arrival == preceding.arrivalEndTimes.end()) + return std::nullopt; + return current->getStartTime() - arrival->second; +} + const IListeningDecision *RadioMedium::listenOnMedium(const IRadio *radio, const IListening *listening) const { Enter_Method("listenOnMedium"); diff --git a/src/inet/physicallayer/wireless/common/medium/RadioMedium.h b/src/inet/physicallayer/wireless/common/medium/RadioMedium.h index 3952b247ddd..659b86c067e 100644 --- a/src/inet/physicallayer/wireless/common/medium/RadioMedium.h +++ b/src/inet/physicallayer/wireless/common/medium/RadioMedium.h @@ -337,6 +337,9 @@ class INET_API RadioMedium : public SimpleModule, public cListener, public IRadi virtual IWirelessSignal *transmitPacket(const IRadio *transmitter, Packet *packet) override; virtual Packet *receivePacket(const IRadio *receiver, IWirelessSignal *signal) override; virtual const ITransmission *getTransmission(int id) const override; + virtual TransmissionTimingSnapshot getTransmissionTimingSnapshot(int id) const override; + virtual std::optional computePrecedingSignalGap(int currentTransmissionId, + const TransmissionTimingSnapshot& preceding) const override; virtual const IListeningDecision *listenOnMedium(const IRadio *receiver, const IListening *listening) const override; diff --git a/tests/unit/TransmissionTimingSnapshot_1.test b/tests/unit/TransmissionTimingSnapshot_1.test new file mode 100644 index 00000000000..ff99ca1b5e5 --- /dev/null +++ b/tests/unit/TransmissionTimingSnapshot_1.test @@ -0,0 +1,138 @@ +%description: +Check shared transmission timing without a WLAN consumer or a propagation model. +Use generic transmissions and cached arrivals with exact endpoints. +Require snapshots to retain their values after cache expiry deletes the original objects. +Check positive, zero, and negative gaps for the same transmitter and different transmitters. +Check missing transmissions, missing arrivals, and the same transmission identity. +Require absent arrival queries to leave all three cache implementations unchanged before their first transmission. +This unit test does not establish physical reception or interframe protocol behavior. + +%includes: +#include +#include "inet/physicallayer/wireless/common/antenna/IsotropicAntenna.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/FlatReceiverBase.h" +#include "inet/physicallayer/wireless/common/base/packetlevel/TransmissionBase.h" +#include "inet/physicallayer/wireless/common/communicationcache/MapCommunicationCache.h" +#include "inet/physicallayer/wireless/common/communicationcache/ReferenceCommunicationCache.h" +#include "inet/physicallayer/wireless/common/communicationcache/VectorCommunicationCache.h" +#include "inet/physicallayer/wireless/common/medium/RadioMedium.h" +#include "inet/physicallayer/wireless/common/radio/packetlevel/Radio.h" +#include "inet/physicallayer/wireless/common/signal/Arrival.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TimingMedium : public RadioMedium { + public: + void attach(ICommunicationCache *cache) { communicationCache = cache; } +}; + +// These adapters supply identity and component references. No radio procedure executes. +class TimingRadio : public Radio { + int identity; + const IRadioMedium *attachedMedium; + const IAntenna *attachedAntenna; + const IReceiver *attachedReceiver; + public: + TimingRadio(int identity, const IRadioMedium *medium, const IAntenna *antenna, const IReceiver *receiver) : + identity(identity), attachedMedium(medium), attachedAntenna(antenna), attachedReceiver(receiver) {} + int getId() const override { return identity; } + const IRadioMedium *getMedium() const override { return attachedMedium; } + const IAntenna *getAntenna() const override { return attachedAntenna; } + const IReceiver *getReceiver() const override { return attachedReceiver; } +}; + +class TimingCache : public MapCommunicationCache { + public: + size_t receptionSlots() const { + size_t count = 0; + for (const auto& entry : transmissionCache) + if (entry.second.receptionCacheEntries) + count += entry.second.receptionCacheEntries->size(); + return count; + } +}; + +static TransmissionBase *makeTiming(const IRadio *radio, const Packet *packet, int startUs, int endUs) +{ + return new TransmissionBase(radio, packet, SimTime(startUs, SIMTIME_US), SimTime(endUs, SIMTIME_US), + SIMTIME_ZERO, SIMTIME_ZERO, SimTime(endUs - startUs, SIMTIME_US), Coord(), Coord(), + Quaternion(), Quaternion(), nullptr, nullptr, nullptr, nullptr, nullptr); +} + +template +static void checkAbsentArrival(const IRadio *radio, const ITransmission *transmission) +{ + Cache cache; + ASSERT(cache.getNumRadios() == 0 && cache.getNumTransmissions() == 0); + for (int i = 0; i < 3; i++) + ASSERT(cache.findCachedArrival(radio, transmission) == nullptr); + ASSERT(cache.getNumRadios() == 0 && cache.getNumTransmissions() == 0); +} + +%activity: +Packet packet("timing-input"); +IsotropicAntenna antenna; +FlatReceiverBase receiver; +TimingMedium medium; +TimingRadio transmitter(1, &medium, &antenna, &receiver); +TimingRadio recipient(2, &medium, &antenna, &receiver); +TimingRadio absentArrival(3, &medium, &antenna, &receiver); +TimingCache cache; +medium.attach(&cache); +auto previous = std::unique_ptr(makeTiming(&transmitter, &packet, 100, 140)); +checkAbsentArrival(&recipient, previous.get()); +checkAbsentArrival(&recipient, previous.get()); +checkAbsentArrival(&recipient, previous.get()); +int previousId = previous->getId(); +ASSERT(medium.getTransmissionTimingSnapshot(previousId).transmissionId == -1); +ASSERT(!medium.computePrecedingSignalGap(previousId, TransmissionTimingSnapshot()).has_value()); +cache.addRadio(&transmitter); +cache.addRadio(&recipient); +cache.addRadio(&absentArrival); +auto previousPointer = previous.release(); +cache.addTransmission(previousPointer); +cache.setCachedArrival(&recipient, previousPointer, new Arrival(SimTime(7, SIMTIME_US), SimTime(7, SIMTIME_US), + SimTime(107, SIMTIME_US), SimTime(147, SIMTIME_US), SIMTIME_ZERO, SIMTIME_ZERO, + SimTime(40, SIMTIME_US), Coord(), Coord(), Quaternion(), Quaternion())); +auto snapshot = medium.getTransmissionTimingSnapshot(previousId); +ASSERT(snapshot.transmissionId == previousId && snapshot.transmitterRadioId == 1); +ASSERT(snapshot.startTime == SimTime(100, SIMTIME_US) && snapshot.endTime == SimTime(140, SIMTIME_US)); +ASSERT(snapshot.arrivalEndTimes.size() == 1 && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +ASSERT(cache.findCachedArrival(&absentArrival, previousPointer) == nullptr); +ASSERT(cache.receptionSlots() == 1); +cache.setCachedInterferenceEndTime(previousPointer, SimTime(160, SIMTIME_US)); +wait(SimTime(200, SIMTIME_US)); +int expired = 0; +cache.removeNonInterferingTransmissions([&](const ITransmission *transmission) { + ASSERT(transmission->getId() == previousId); + expired++; +}); +ASSERT(expired == 1 && cache.getNumTransmissions() == 0); +ASSERT(medium.getTransmissionTimingSnapshot(previousId).transmissionId == -1); +ASSERT(!medium.computePrecedingSignalGap(previousId, snapshot).has_value()); +ASSERT(snapshot.endTime == SimTime(140, SIMTIME_US) && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +for (const TimingRadio *source : {&transmitter, &recipient}) { + int precedingEndUs = source == &transmitter ? 140 : 147; + for (int gapUs : {10, 0, -5}) { + auto current = makeTiming(source, &packet, precedingEndUs + gapUs, precedingEndUs + gapUs + 10); + cache.addTransmission(current); + auto gap = medium.computePrecedingSignalGap(current->getId(), snapshot); + ASSERT(gap.has_value() && *gap == SimTime(gapUs, SIMTIME_US)); + ASSERT(!medium.computePrecedingSignalGap(current->getId(), TransmissionTimingSnapshot()).has_value()); + auto sameId = snapshot; + sameId.transmissionId = current->getId(); + ASSERT(!medium.computePrecedingSignalGap(current->getId(), sameId).has_value()); + auto noArrival = snapshot; + noArrival.arrivalEndTimes.clear(); + auto withoutArrival = medium.computePrecedingSignalGap(current->getId(), noArrival); + ASSERT(source == &transmitter ? withoutArrival.has_value() : !withoutArrival.has_value()); + cache.removeTransmission(current); + } +} +ASSERT(snapshot.transmissionId == previousId && snapshot.arrivalEndTimes.at(2) == SimTime(147, SIMTIME_US)); +EV << "Shared transmission timing snapshot checks passed\n"; + +%contains: stdout +Shared transmission timing snapshot checks passed From ef9ecba78c8ae3ded35465fdcbfeb83bf34a7908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:48:34 +0200 Subject: [PATCH 09/17] ieee80211: fix: include the HT signal extension at 2.4 GHz Before this commit, HT duration omits the six-microsecond signal extension at 2.4 GHz. A waveform therefore ends early and supplies incorrect airtime and interval evidence. After this commit, HT duration includes that extension. Mode queries supply its nominal duration. Transmission queries derive the actual extension from the waveform so interval consumers cannot count it twice. The mode fixture checks the 2.4 GHz extension and the other-band control. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.physicallayer.wireless.ieee80211 | behavior.change.fix | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 ++++ doc/src/migration-guide/index.rst | 5 +++++ .../wireless/ieee80211/mode/IIeee80211Mode.h | 2 ++ .../wireless/ieee80211/mode/IIeee80211Mode.msg | 1 + .../wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h | 3 ++- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 4 +++- .../wireless/ieee80211/mode/Ieee80211ModeBase.h | 1 + .../ieee80211/packetlevel/Ieee80211Transmission.cc | 12 ++++++++++++ .../ieee80211/packetlevel/Ieee80211Transmission.h | 5 +++++ tests/unit/Ieee80211HtModeSet_1.test | 8 ++++++++ 10 files changed, 43 insertions(+), 2 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 95420dffc08..4b922a488bc 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -458,6 +458,10 @@ Notable backward incompatible changes are the following: 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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 7dc296dc880..9dde89f3b3b 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -21,6 +21,11 @@ Custom radio media must implement ``getTransmissionTimingSnapshot()`` and ``comp ``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 Basic Block Ack extension points ------------------------------------------ diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index cecad21b7ad..4224c730c4e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -83,6 +83,8 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject virtual const simtime_t getSlotTime() const = 0; virtual const simtime_t getSifsTime() const = 0; virtual const simtime_t getRifsTime() const = 0; + // Default extension after the PPDU, in seconds. A received waveform can omit it for RIFS. + virtual const simtime_t getSignalExtensionTime() const = 0; virtual const simtime_t getCcaTime() const = 0; virtual const simtime_t getPhyRxStartDelay() const = 0; virtual const simtime_t getRxTxTurnaroundTime() const = 0; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg index 52c42112f65..425faaaa0cd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.msg @@ -57,6 +57,7 @@ class IIeee80211Mode extends cObject simtime_t slotTime; simtime_t sifsTime; simtime_t rifsTime; + simtime_t signalExtensionTime; simtime_t ccaTime; simtime_t phyRxStartDelay; simtime_t rxTxTurnaroundTime; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h index e5c644ff0fc..e0f11ce9eba 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h @@ -33,7 +33,8 @@ class INET_API Ieee80211ErpOfdmMode : public Ieee80211OfdmMode virtual const simtime_t getSifsTime() const override { return 10E-6; } // For ERP-OFDM modes, an ERP packet is followed by a period of no transmission // with a length of 6 μs called the signal extension. - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + signalMode->getDuration() + dataMode->getDuration(dataLength) + 6E-6; } + virtual const simtime_t getSignalExtensionTime() const override { return SimTime(6, SIMTIME_US); } + virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + signalMode->getDuration() + dataMode->getDuration(dataLength) + getSignalExtensionTime(); } }; class INET_API Ieee80211ErpOfdmCompliantModes diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 62bc6aa98c0..ee6c0a76685 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -258,6 +258,8 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual const simtime_t getShortSlotTime() const; virtual const simtime_t getSifsTime() const override; virtual const simtime_t getRifsTime() const override { return 2E-6; } + // IEEE Std 802.11-2024, Table 19-25: the 2.4 GHz HT PPDU includes a 6 us extension. + virtual const simtime_t getSignalExtensionTime() const override { return centerFrequencyMode == BAND_2_4GHZ ? SimTime(6, SIMTIME_US) : SIMTIME_ZERO; } virtual const simtime_t getCcaTime() const override { return 4E-6; } // < 4 virtual const simtime_t getPhyRxStartDelay() const override { return 24E-6; } virtual const simtime_t getRxTxTurnaroundTime() const override { return 2E-6; } // < 2 @@ -266,7 +268,7 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength); } + virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength) + getSignalExtensionTime(); } }; // A specification of the high-throughput (HT) physical layer (PHY) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h index 36edd5ba0e7..39b7af724dd 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeBase.h @@ -29,6 +29,7 @@ class INET_API Ieee80211ModeBase : public IIeee80211Mode virtual PreambleType getLegacyPreambleType() const override { return PreambleType::UNKNOWN; } virtual int getHtMcsIndex() const override { return -1; } virtual bool isHtShortGuardInterval() const override { return false; } + virtual const simtime_t getSignalExtensionTime() const override { return SIMTIME_ZERO; } virtual const char *getName() const override { return name.c_str(); } }; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc index 4f1be224443..a5eac8b7868 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.cc @@ -6,6 +6,7 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" namespace inet { @@ -27,6 +28,17 @@ std::ostream& Ieee80211Transmission::printToStream(std::ostream& stream, int lev return TransmissionBase::printToStream(stream, level); } +std::optional Ieee80211Transmission::getSignalExtensionDuration() const +{ + auto header = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(getPacket()); + auto defaultExtension = mode->getSignalExtensionTime(); + // The PHY Length field contains MAC octets. The mode duration already includes PHY headers and padding. + auto extension = getEndTime() - getStartTime() - mode->getDuration(B(header->getLengthField())) + defaultExtension; + if (extension == SIMTIME_ZERO || extension == defaultExtension) + return extension; + return std::nullopt; +} + } // namespace physicallayer } // namespace inet diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h index 2bf3eae2a82..f1a9e9bbce1 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211TRANSMISSION_H #define __INET_IEEE80211TRANSMISSION_H +#include + #include "inet/physicallayer/wireless/common/base/packetlevel/TransmissionBase.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" @@ -30,6 +32,9 @@ class INET_API Ieee80211Transmission : public TransmissionBase virtual const IIeee80211Mode *getMode() const { return mode; } virtual const Ieee80211Channel *getChannel() const { return channel; } + // The actual waveform can omit the default extension through NO_SIG_EXTN for RIFS. + // An unsupported duration returns no value; the method changes no state. + std::optional getSignalExtensionDuration() const; }; } // namespace physicallayer diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 0dc059d2d40..552c8f54549 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -235,6 +235,14 @@ for (int mcs = 0; mcs < 32; mcs++) { ASSERT(typedHt->getSlotTime() == SimTime(20, SIMTIME_US)); ASSERT(typedAcHt->getSifsTime() == SimTime(16, SIMTIME_US)); ASSERT(typedAcHt->getSlotTime() == SimTime(9, SIMTIME_US)); + ASSERT(typedHt->getSignalExtensionTime() == SimTime(6, SIMTIME_US)); + ASSERT(typedAcHt->getSignalExtensionTime() == SIMTIME_ZERO); + ASSERT(typedHt->getDuration(B(100)) - typedAcHt->getDuration(B(100)) == SimTime(6, SIMTIME_US)); + if (mcs == 0 && width == MHz(20) && gi == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) { + // MCS 0: 36 us preamble + ceil((16 + 800 + 6) / 26) * 4 us + band extension. + ASSERT(typedHt->getDuration(B(100)) == SimTime(170, SIMTIME_US)); + ASSERT(typedAcHt->getDuration(B(100)) == SimTime(164, SIMTIME_US)); + } ASSERT(acHt->getDataMode()->getNetBitrate() == ht->getDataMode()->getNetBitrate()); ASSERT(acHt->getDataMode()->getNumberOfSpatialStreams() == ht->getDataMode()->getNumberOfSpatialStreams()); ASSERT(vhtOnlyModeSet->getIsMandatory(acHt) == htModeSet->getIsMandatory(ht)); From 2c329da06cb795237bab6b4bbd4d2ea8f1b63d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:48:50 +0200 Subject: [PATCH 10/17] ieee80211: fix: preserve advertised RIFS permission Before this commit, HT management conversion loses RIFS Mode. A recipient therefore cannot use received permission to identify a permitted RIFS relation. After this commit, HT Operation retains RIFS Mode through conversion and equality checks. The MIB permits RIFS for unprotected HT operation and clears the permission for VHT. For example, an unprotected HT operation survives conversion with permission intact. The capability fixture checks these states. This commit adds no RIFS transmission scheduler. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change.fix | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 ++++ doc/src/migration-guide/index.rst | 5 +++++ .../ieee80211/mgmt/Ieee80211HtMgmtElements.h | 2 ++ .../ieee80211/mib/Ieee80211HtCapabilities.h | 3 ++- src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc | 6 ++++++ tests/unit/Ieee80211HtCapabilities_1.test | 12 ++++++++++++ 6 files changed, 31 insertions(+), 1 deletion(-) diff --git a/WHATSNEW b/WHATSNEW index 4b922a488bc..748f4369c4f 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -462,6 +462,10 @@ Notable backward incompatible changes are the following: 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. +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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 9dde89f3b3b..475339b9164 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -26,6 +26,11 @@ 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 Basic Block Ack extension points ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h index 6a851b507e4..fb62285460e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h @@ -101,6 +101,7 @@ inline Ieee80211HtOperationElement makeHtOperationElement(const physicallayer::I element.secondaryChannelOffset = operation.secondaryChannelOffset; element.staChannelWidth40Mhz = operation.operatingChannelWidth == MHz(40); element.protectionMode = static_cast(operation.protectionMode); + element.rifsMode = operation.rifsMode; for (int i = 0; i < 77; i++) element.basicMcsSupported[i] = operation.basicMcsSupported[i]; return element; @@ -119,6 +120,7 @@ inline Ieee80211HtOperation makeHtOperation(const physicallayer::IIeee80211Band operation.secondaryChannelOffset = element.secondaryChannelOffset; operation.operatingChannelWidth = element.staChannelWidth40Mhz ? MHz(40) : MHz(20); operation.protectionMode = static_cast(element.protectionMode); + operation.rifsMode = element.rifsMode; for (int i = 0; i < 77; i++) operation.basicMcsSupported[i] = element.basicMcsSupported[i]; return operation; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index b2b02ad5a4d..26a27315c11 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -70,12 +70,13 @@ struct Ieee80211HtOperation int primaryChannel = 0; int secondaryChannelOffset = 0; Ieee80211HtProtectionMode protectionMode = Ieee80211HtProtectionMode::NO_PROTECTION; + bool rifsMode = false; std::array basicMcsSupported = {}; bool operator==(const Ieee80211HtOperation& other) const { return operatingChannelWidth == other.operatingChannelWidth && primaryChannel == other.primaryChannel && secondaryChannelOffset == other.secondaryChannelOffset && protectionMode == other.protectionMode && - basicMcsSupported == other.basicMcsSupported; + rifsMode == other.rifsMode && basicMcsSupported == other.basicMcsSupported; } }; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 9c4db7b2026..f57837295d2 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -162,8 +162,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS modeSet->isHtShortGuardIntervalSupported(MHz(20)); localHtCapabilities.shortGi40 = localHtCapabilities.supportedChannelWidths.count(MHz(40)) != 0 && modeSet->isHtShortGuardIntervalSupported(MHz(40)); + bool vhtSupported = false; for (int index = 0; index < modeSet->getNumModes(); index++) { const auto *mode = modeSet->getMode(index); + vhtSupported |= mode->getModulationClass() == physicallayer::IIeee80211Mode::ModulationClass::VHT; int mcs = mode->getHtMcsIndex(); if (mcs >= 0 && mcs < 77 && operationalChannelWidths.count(mode->getDataMode()->getBandwidth()) != 0 && mode->getDataMode()->getNumberOfSpatialStreams() <= operationalHtSpatialStreamLimit) @@ -199,6 +201,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS if (protectionMode < 0 || protectionMode > 3) throw cRuntimeError("htProtectionMode must be between 0 and 3"); htOperation.protectionMode = static_cast(protectionMode); + // IEEE Std 802.11-2024, 10.27.3.3: the protected modes permit the conservative zero policy. + // IEEE Std 802.11-2024, 11.38.1: a VHT AP advertises zero. + htOperation.rifsMode = !vhtSupported && (htOperation.protectionMode == Ieee80211HtProtectionMode::NO_PROTECTION || + htOperation.protectionMode == Ieee80211HtProtectionMode::TWENTY_MHZ_PROTECTION); bool peerStateChanged = false; for (auto& entry : peerHtStates) { if (entry.second.valid) { diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..455332ef964 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -33,6 +33,18 @@ peer.maxAmpduLengthExponent = 3; Ieee80211HtOperation operation; operation.operatingChannelWidth = MHz(20); +for (bool permitted : {false, true}) { + auto advertised = operation; + advertised.primaryChannel = 0; + advertised.rifsMode = permitted; + auto band = physicallayer::Ieee80211CompliantBands::getBand("2.4 GHz"); + auto element = makeHtOperationElement(band, advertised); + ASSERT(element.rifsMode == permitted); + auto received = makeHtOperation(band, element); + ASSERT(received == advertised); + received.rifsMode = !permitted; + ASSERT(!(received == advertised)); +} operation.basicMcsSupported[0] = true; auto negotiated = negotiateHtCapabilities(local, peer, operation); From b7fb7a52ff3c7d720124c7569f4b1614ac651bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:49:00 +0200 Subject: [PATCH 11/17] ieee80211: fix: release temporary Block Ack bitmap allocations Before this commit, Basic and Compressed decoders allocate temporary BitVector objects and copy their values without deletion. Repeated Block Ack decode therefore loses those allocations. After this commit, the decoders construct bitmap values directly. The generated fields retain their own values without an unowned temporary. For example, independent Basic and Compressed octets retain the same bitmap bits after decode. The direct wire check covers both paths. Type interpretation and frame acceptance remain unchanged. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change.fix | test whatsnew | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 +++ .../mac/Ieee80211MacHeaderSerializer.cc | 5 ++- tests/unit/Ieee80211BlockAckWire_1.test | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/unit/Ieee80211BlockAckWire_1.test diff --git a/WHATSNEW b/WHATSNEW index 748f4369c4f..bb72a72d652 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -466,6 +466,10 @@ Notable backward incompatible changes are the following: 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. Block Ack bitmap ownership + + Block Ack decoders now use bitmap values without unowned temporary allocations. Repeated Basic and Compressed decode no longer loses those temporary objects. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index 2a482cded2f..d841c8be5aa 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -664,8 +664,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre std::vector bytes; bytes.push_back(stream.readByte()); bytes.push_back(stream.readByte()); - BitVector *blockAckBitmap = new BitVector(bytes); - basicBlockAck->setBlockAckBitmap(i, *blockAckBitmap); + basicBlockAck->setBlockAckBitmap(i, BitVector(bytes)); } return basicBlockAck; } @@ -684,7 +683,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre for (size_t i = 0; i < 8; ++i) { bytes.push_back(stream.readByte()); } - compressedBlockAck->setBlockAckBitmap(*(new BitVector(bytes))); + compressedBlockAck->setBlockAckBitmap(BitVector(bytes)); return compressedBlockAck; } else { diff --git a/tests/unit/Ieee80211BlockAckWire_1.test b/tests/unit/Ieee80211BlockAckWire_1.test new file mode 100644 index 00000000000..0cd32ee3a46 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckWire_1.test @@ -0,0 +1,36 @@ +%description: +Check Basic and Compressed bitmap values after independent byte decode. +The decoder must preserve the same bitmap contents without unowned temporary objects. +Type validation remains outside this prerequisite. + +%includes: +#include "inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.h" + +using namespace inet; +using namespace inet::ieee80211; + +%activity: +for (bool compressed : {false, true}) { + std::vector bytes(compressed ? 28 : 148, 0); + bytes[0] = 0x94; + bytes[16] = compressed ? 4 : 0; + bytes[20] = 1; + MemoryInputStream stream(bytes); + auto decoded = Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); + if (compressed) { + auto response = dynamicPtrCast(decoded); + ASSERT(response && response->getBlockAckBitmap().getSize() == 64); + ASSERT(response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(63)); + } + else { + auto response = dynamicPtrCast(decoded); + ASSERT(response && response->getBlockAckBitmapArraySize() == 64); + ASSERT(response->getBlockAckBitmap(0).getBit(0)); + ASSERT(!response->getBlockAckBitmap(63).getBit(15)); + } +} +EV << "Bitmap decode checks passed.\n"; + +%contains: stdout +Bitmap decode checks passed. From 96be3ee415447138c06dbfc3f2f0a68a4f1dd2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:49:54 +0200 Subject: [PATCH 12/17] ieee80211: add: serialize Basic and Compressed Block Ack Types Before this commit, Block Ack serialization decodes only part of the Type field. Compressed bitmap and fragment fields lack complete wire validation. The preceding prerequisite already removes the unowned bitmap temporaries. After this commit, the serializer decodes all four Type bits. Type 0 retains Basic compatibility. Type 2 supplies a 64-bit Compressed bitmap and a 32-byte response with FCS. Both BAR profiles occupy 24 bytes with FCS. Invalid Types and malformed fragment fields fail validation. For example, Type 2 with a nonzero fragment field fails validation instead of entering the supported Compressed profile. Independent octets cover both Types, malformed fields, reserved bits, lengths, and sequence wrap. The existing Compressed BAR builder supplies its complete header. Synthetic fixtures provide valid FCS lengths and every Basic bitmap entry. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 8 +- doc/src/migration-guide/index.rst | 7 + .../ieee80211/mac/Ieee80211Frame.msg | 8 +- .../mac/Ieee80211MacHeaderSerializer.cc | 40 ++- .../blockack/OriginatorBlockAckProcedure.cc | 12 +- .../Ieee80211BlockAckActionDispatch_1.test | 1 + ...Ieee80211BlockAckFragmentRetirement_1.test | 2 + tests/serializer/REMAINING_GAPS.md | 4 + .../lib/fillers/ChunkFillers_ieee80211.cc | 25 +- .../serializer_chunk_roundtrip.test | 13 +- tests/unit/Ieee80211BlockAckWire_1.test | 238 ++++++++++++++++-- 11 files changed, 290 insertions(+), 68 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index bb72a72d652..ec626754ce9 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -462,13 +462,13 @@ Notable backward incompatible changes are the following: 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. -56. IEEE 802.11 RIFS permission +55. IEEE 802.11 Block Ack wire types - 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. + 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. -57. Block Ack bitmap ownership +56. IEEE 802.11 RIFS permission - Block Ack decoders now use bitmap values without unowned temporary allocations. Repeated Basic and Compressed decode no longer loses those temporary objects. + 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. Notable backward compatible changes are the following: diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 475339b9164..7a2be4ab354 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -31,6 +31,13 @@ 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 Basic Block Ack extension points ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index e139deaf75d..7d1367944d4 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -43,6 +43,8 @@ const b LENGTH_ADDBAREQ = LENGTH_MGMT + B(9); // mgmt length + action body lengt const b LENGTH_ADDBARESP = LENGTH_MGMT + B(9); // mgmt length + action body length const b LENGTH_DELBA = LENGTH_MGMT + B(6); // mgmt length + action body length const b LENGTH_BASIC_BLOCKACK = B(16 + 2 + (2 + 128) + 4); // header + ba control + ba information + fcs +const b LENGTH_COMPRESSED_BLOCKACK = B(16 + 2 + 2 + 8 + 4); // header + control + sequence + bitmap + FCS +const b LENGTH_BLOCKACKREQ = B(16 + 2 + 2 + 4); // header + control + sequence + FCS const b DATAFRAME_HEADER_MINLENGTH = B(2 + 2 + 3 * 6 + 2); //bits without QoS, without Address4: 2 + 2 + 3*6(addresses) + 2 const b QOSCONTROL_PART_LENGTH = b(2 * 8); // QoS Control field length (bits) const short int MAX_NUM_FRAGMENTS = 16; @@ -356,7 +358,7 @@ class Ieee80211BlockAckReq extends Ieee80211TwoAddressHeader class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(16 + 2 + 2); // header + BAR control + BAR information + chunkLength = LENGTH_BLOCKACKREQ - B(4); int tidInfo @bit(4); // The TID_INFO subfield of the BAR Control field of the Basic BlockAckReq frame contains the TID for which a Basic BlockAck frame is requested. @@ -373,7 +375,7 @@ class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq class Ieee80211CompressedBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(16 + 2 + 2); // header + BAR control + BAR information + chunkLength = LENGTH_BLOCKACKREQ - B(4); int tidInfo @bit(4); // The TID_INFO subfield of the BAR Control field of the Compressed BlockAckReq frame contains the TID for which a BlockAck frame is requested. // The BAR Information field of the Compressed BlockAckReq frame contains the Block Ack Starting @@ -455,7 +457,7 @@ class Ieee80211BasicBlockAck extends Ieee80211BlockAck // class Ieee80211CompressedBlockAck extends Ieee80211BlockAck { - // chunkLength TODO + chunkLength = LENGTH_COMPRESSED_BLOCKACK - B(4); multiTid = 0; compressedBitmap = 1; diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index d841c8be5aa..d2b1ae642fe 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -331,10 +331,13 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeMacAddress(blockAckReq->getTransmitterAddress()); bool multiTid = blockAckReq->getMultiTid(); bool compressedBitmap = blockAckReq->getCompressedBitmap(); - // BAR Control is a 16-bit little-endian field: bit 0 BAR Ack Policy, bit 1 - // Multi-TID, bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO + // IEEE Std 802.11-2024, 9.3.1.7: B1-B4 encode the complete BAR Type. + if (!multiTid && !compressedBitmap && (blockAckReq->getReserved() & 3)) + throw cRuntimeError("Basic BAR reserved fields must encode Type 0"); uint16_t barControl = (blockAckReq->getBarAckPolicy() ? 1 : 0) | ((multiTid ? 1 : 0) << 1) | ((compressedBitmap ? 1 : 0) << 2) | ((blockAckReq->getReserved() & 0x1FF) << 3); + if (!multiTid && compressedBitmap) + barControl = 2 << 1; if (!multiTid && !compressedBitmap) { auto basicBlockAckReq = CHK(dynamicPtrCast(chunk)); stream.writeUint16Le(barControl | ((basicBlockAckReq->getTidInfo() & 0xF) << 12)); @@ -347,6 +350,8 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAckReq = CHK(dynamicPtrCast(chunk)); + if (compressedBlockAckReq->getFragmentNumber() != 0) + throw cRuntimeError("Compressed BAR Fragment Number must be zero"); stream.writeUint16Le(barControl | ((compressedBlockAckReq->getTidInfo() & 0xF) << 12)); writeSequenceControl(stream, compressedBlockAckReq->getFragmentNumber(), compressedBlockAckReq->getStartingSequenceNumber().get()); if (stream.getLength() - startPos != compressedBlockAckReq->getChunkLength()) @@ -367,10 +372,13 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeMacAddress(blockAck->getTransmitterAddress()); bool multiTid = blockAck->getMultiTid(); bool compressedBitmap = blockAck->getCompressedBitmap(); - // BA Control is a 16-bit little-endian field: bit 0 Ack Policy, bit 1 Multi-TID, - // bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO + // IEEE Std 802.11-2024, 9.3.1.8: B1-B4 encode the complete BA Type. + if (!multiTid && !compressedBitmap && (blockAck->getReserved() & 3)) + throw cRuntimeError("Basic Block Ack reserved fields must encode Type 0"); uint16_t baControl = (blockAck->getBlockAckPolicy() ? 1 : 0) | ((multiTid ? 1 : 0) << 1) | ((compressedBitmap ? 1 : 0) << 2) | ((blockAck->getReserved() & 0x1FF) << 3); + if (!multiTid && compressedBitmap) + baControl = 2 << 1; if (!multiTid && !compressedBitmap) { auto basicBlockAck = CHK(dynamicPtrCast(chunk)); stream.writeUint16Le(baControl | ((basicBlockAck->getTidInfo() & 0xF) << 12)); @@ -386,6 +394,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAck = CHK(dynamicPtrCast(chunk)); + if (compressedBlockAck->getFragmentNumber() != 0) + throw cRuntimeError("Compressed Block Ack Fragment Number must be zero"); + if (compressedBlockAck->getBlockAckBitmap().getSize() != 64) + throw cRuntimeError("Compressed Block Ack bitmap must contain 64 bits"); stream.writeUint16Le(baControl | ((compressedBlockAck->getTidInfo() & 0xF) << 12)); // Block Ack Starting Sequence Control = 4-bit Fragment Number (reserved) + 12-bit sequence number writeSequenceControl(stream, compressedBlockAck->getFragmentNumber(), compressedBlockAck->getStartingSequenceNumber().get()); @@ -600,6 +612,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setReceiverAddress(stream.readMacAddress()); blockAckReq->setTransmitterAddress(stream.readMacAddress()); uint16_t barControl = stream.readUint16Le(); + auto barType = (barControl >> 1) & 0xF; bool multiTid = (barControl >> 1) & 1; bool compressedBitmap = (barControl >> 2) & 1; blockAckReq->setBarAckPolicy(barControl & 1); @@ -608,7 +621,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setReserved((barControl >> 3) & 0x1FF); int fragmentNumber = 0; SequenceNumberCyclic startingSequenceNumber; - if (!multiTid && !compressedBitmap) { + if (barType == 0) { auto basicBlockAckReq = makeShared(); copyBasicFields(basicBlockAckReq, macHeader); copyBlockAckReqFrameFields(basicBlockAckReq, blockAckReq); @@ -618,14 +631,18 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre basicBlockAckReq->setStartingSequenceNumber(startingSequenceNumber); return basicBlockAckReq; } - else if (!multiTid && compressedBitmap) { + else if (barType == 2) { auto compressedBlockAckReq = makeShared(); copyBasicFields(compressedBlockAckReq, macHeader); copyBlockAckReqFrameFields(compressedBlockAckReq, blockAckReq); + compressedBlockAckReq->setBarAckPolicy(false); + compressedBlockAckReq->setReserved(0); compressedBlockAckReq->setTidInfo((barControl >> 12) & 0xF); readSequenceControl(stream, fragmentNumber, startingSequenceNumber); compressedBlockAckReq->setFragmentNumber(fragmentNumber); compressedBlockAckReq->setStartingSequenceNumber(startingSequenceNumber); + if (fragmentNumber != 0) + compressedBlockAckReq->markIncorrect(); return compressedBlockAckReq; } else @@ -640,9 +657,8 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setDurationField(SimTime(stream.readUint16Le(), SIMTIME_US)); blockAck->setReceiverAddress(stream.readMacAddress()); blockAck->setTransmitterAddress(stream.readMacAddress()); - // BA Control is a 16-bit little-endian field: bit 0 Ack Policy, bit 1 Multi-TID, - // bit 2 Compressed Bitmap, bits 3-11 reserved, bits 12-15 TID_INFO uint16_t baControl = stream.readUint16Le(); + auto baType = (baControl >> 1) & 0xF; bool multiTid = (baControl >> 1) & 1; bool compressedBitmap = (baControl >> 2) & 1; uint8_t tidInfo = (baControl >> 12) & 0xF; @@ -650,7 +666,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setMultiTid(multiTid); blockAck->setCompressedBitmap(compressedBitmap); blockAck->setReserved((baControl >> 3) & 0x1FF); - if (!multiTid && !compressedBitmap) { + if (baType == 0) { auto basicBlockAck = makeShared(); copyBasicFields(basicBlockAck, macHeader); copyBlockAckFrameFields(basicBlockAck, blockAck); @@ -668,10 +684,12 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre } return basicBlockAck; } - else if (!multiTid && compressedBitmap) { + else if (baType == 2) { auto compressedBlockAck = makeShared(); copyBasicFields(compressedBlockAck, macHeader); copyBlockAckFrameFields(compressedBlockAck, blockAck); + compressedBlockAck->setBlockAckPolicy(false); + compressedBlockAck->setReserved(0); compressedBlockAck->setTidInfo(tidInfo); // Block Ack Starting Sequence Control = 4-bit Fragment Number (reserved) + 12-bit @@ -684,6 +702,8 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre bytes.push_back(stream.readByte()); } compressedBlockAck->setBlockAckBitmap(BitVector(bytes)); + if (fragmentNumber != 0) + compressedBlockAck->markIncorrect(); return compressedBlockAck; } else { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc index d02e412b644..ee1960c3fb0 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc @@ -12,13 +12,11 @@ namespace ieee80211 { const Ptr OriginatorBlockAckProcedure::buildCompressedBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const { - throw cRuntimeError("Unsupported feature"); - // TODO implement - // auto blockAckReq = makeShared(); - // blockAckReq->setReceiverAddress(receiverAddress); - // blockAckReq->setStartingSequenceNumber(startingSequenceNumber); - // blockAckReq->setTidInfo(tid); - // return blockAckReq; + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(receiverAddress); + blockAckReq->setStartingSequenceNumber(startingSequenceNumber); + blockAckReq->setTidInfo(tid); + return blockAckReq; } const Ptr OriginatorBlockAckProcedure::buildBasicBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const diff --git a/tests/module/Ieee80211BlockAckActionDispatch_1.test b/tests/module/Ieee80211BlockAckActionDispatch_1.test index 1ec9db351e9..e09a34d1ab8 100644 --- a/tests/module/Ieee80211BlockAckActionDispatch_1.test +++ b/tests/module/Ieee80211BlockAckActionDispatch_1.test @@ -62,6 +62,7 @@ class ActionDispatchHcf : public Hcf { bar->setStartingSequenceNumber(SequenceNumberCyclic(104)); bar->setFragmentNumber(0); auto request = new Packet("bar-before-data", bar); + request->insertAtBack(makeShared()); request->addTag()->setMode(modeSet->getMode(bps(6000000))); processLowerFrame(request, bar); ASSERT(delivered == 0); diff --git a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test index 90b92add024..5a15b2b964a 100644 --- a/tests/module/Ieee80211BlockAckFragmentRetirement_1.test +++ b/tests/module/Ieee80211BlockAckFragmentRetirement_1.test @@ -218,6 +218,8 @@ class FragmentRetirementHcf : public Hcf { for (int i = 1; i < 16; i++) bits.appendBit(false); header->setBlockAckBitmap(0, bits); + for (int position = 1; position < 64; position++) + header->setBlockAckBitmap(position, BitVector(std::vector{0, 0})); auto packet = new Packet("block-ack", header); packet->insertAtBack(makeShared()); packet->addTag()->setMode(modeSet->getMode(bps(24000000))); diff --git a/tests/serializer/REMAINING_GAPS.md b/tests/serializer/REMAINING_GAPS.md index c112225ab8d..78180de2e35 100644 --- a/tests/serializer/REMAINING_GAPS.md +++ b/tests/serializer/REMAINING_GAPS.md @@ -432,6 +432,10 @@ anyway. ## Untested serializers (registered, but no real-capture coverage) +The Block Ack producers retain Basic BAR and Block Ack with Type 0. Their `reserved` fields clear the two bits that overlap Type. The Compressed producers use Type 2 and leave ACK-policy and `reserved` fields at zero. These Compressed fields have no field-preserving wire contract. The serializer writes zero for reserved bits B0 and B5–B11 and ignores their received values. + +`Ieee80211BlockAckWire_1.test` checks independent octets for both variants and both frame types. It also checks unsupported Types, malformed Compressed fragment subfields, and response lengths. Basic response headers contain 148 bytes; Compressed response headers contain 28 bytes. The four-byte FCS gives MPDU lengths of 152 and 32 bytes. These checks do not establish a complete frame exchange or complete historical Basic conformance. + The tests cover different things. `serializer_chunk_roundtrip` **constructs** an instance of every registered chunk type — from hand-written typed fillers in `tests/serializer/lib/fillers/`, one or more per type, a variant per branch the serializer takes diff --git a/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc b/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc index 158b12c5620..163bfaa38c3 100644 --- a/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc +++ b/tests/serializer/lib/fillers/ChunkFillers_ieee80211.cc @@ -432,13 +432,8 @@ void addFillers_ieee80211(std::vector& fillers) setChunkLength(c, LENGTH_DELBA); }}); - // --- Block Ack request / response, Basic and Compressed variants: multiTid and - // compressedBitmap are the serializer's variant discriminator (see - // Ieee80211MacHeaderSerializer's ST_BLOCKACK_REQ/ST_BLOCKACK cases), pinned per - // subclass by the .msg -- kept at that pinned pair. fragmentNumber is the Fragment - // Number subfield of the Block Ack (Req) Starting Sequence Control, which 802.11 - // always sets to 0 in a Block Ack context (no fragmentation) -- kept at the .msg - // default in all four subtypes. + // The concrete classes fix the Type bits. Basic reserved values must retain Type 0. + // Compressed reserved fields write zero. All producers retain fragmentNumber = 0. fillers.push_back({"inet::ieee80211::Ieee80211BasicBlockAckReq", "", [](Chunk *c) { auto h = check_and_cast(c); FillValues v; @@ -451,7 +446,7 @@ void addFillers_ieee80211(std::vector& fillers) h->setMultiTid(false); // pinned by the class: selects the (non-multi-TID) Basic/Compressed branch h->setCompressedBitmap(false); // pinned by the class: selects the Basic (non-compressed) branch h->setBarAckPolicy(true); - h->setReserved(v.uint(9)); + h->setReserved(v.uint(9) & ~3u); h->setTidInfo((int)v.uint(4)); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); }}); @@ -466,8 +461,8 @@ void addFillers_ieee80211(std::vector& fillers) h->setType(ST_BLOCKACK_REQ); h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(true); // pinned by the class: selects the Compressed branch - h->setBarAckPolicy(true); - h->setReserved(v.uint(9)); + h->setBarAckPolicy(false); + h->setReserved(0); h->setTidInfo((int)v.uint(4)); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); }}); @@ -483,7 +478,7 @@ void addFillers_ieee80211(std::vector& fillers) h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(false); // pinned by the class: selects the Basic bitmap branch h->setBlockAckPolicy(true); - h->setReserved(v.uint(9)); + h->setReserved(v.uint(9) & ~3u); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); // The Block Ack bitmap: 64 elements of 2 bytes (16 bits) each; never leave any at // the default empty BitVector -- getBytes()[0]/[1] would read out of bounds. @@ -502,15 +497,11 @@ void addFillers_ieee80211(std::vector& fillers) h->setType(ST_BLOCKACK); h->setMultiTid(false); // pinned by the class h->setCompressedBitmap(true); // pinned by the class: selects the Compressed bitmap branch - h->setBlockAckPolicy(true); - h->setReserved(v.uint(9)); + h->setBlockAckPolicy(false); + h->setReserved(0); h->setStartingSequenceNumber(SequenceNumberCyclic(v.uint(12))); h->setBlockAckBitmap(BitVector(std::vector{v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8(), v.u8()})); h->setTidInfo((int)v.uint(4)); - // the .msg leaves chunkLength unset ("TODO"); the wire layout is fixed at - // 16 (header) + 2 (BA control) + 2 (starting-sequence control) + 8 (compressed - // bitmap) = 28 bytes. - setChunkLength(c, B(28)); }}); // --- management frame bodies. Each is content-length-dependent (SSID, rates, the diff --git a/tests/serializer/serializer_chunk_roundtrip.test b/tests/serializer/serializer_chunk_roundtrip.test index 18fa3f2e235..3ab712fe87c 100644 --- a/tests/serializer/serializer_chunk_roundtrip.test +++ b/tests/serializer/serializer_chunk_roundtrip.test @@ -420,11 +420,10 @@ inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgBackoffTime inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgErrorRate inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.avgWaitTime inet::ieee80211::Ieee80211ProbeResponseFrame::handoverParameters.estAvailBW -// -- 802.11 Block Ack: multiTid/compressedBitmap are the pair of bits that select -// the concrete Basic/Compressed/MultiTid variant, so each concrete class pins them; -// the Starting Sequence Control fragment-number subfield is always 0 (802.11 does -// not fragment the frames a Block Ack covers), and ADDBA's blockAckAction is the -// action code its own class fixes +// The concrete Block Ack classes fix their Type bits. These producers use fragmentNumber = 0. +// Compressed ACK-policy and reserved fields have no field-preserving wire contract. +// The serializer writes zero for these fields and ignores received reserved bits. +// ADDBA fixes its action code through its concrete class. inet::ieee80211::Ieee80211AddbaRequest::_fragmentNumber inet::ieee80211::Ieee80211AddbaRequest::blockAckAction inet::ieee80211::Ieee80211BasicBlockAck::compressedBitmap @@ -434,11 +433,15 @@ inet::ieee80211::Ieee80211BasicBlockAckReq::compressedBitmap inet::ieee80211::Ieee80211BasicBlockAckReq::fragmentNumber inet::ieee80211::Ieee80211BasicBlockAckReq::multiTid inet::ieee80211::Ieee80211CompressedBlockAck::compressedBitmap +inet::ieee80211::Ieee80211CompressedBlockAck::blockAckPolicy inet::ieee80211::Ieee80211CompressedBlockAck::fragmentNumber inet::ieee80211::Ieee80211CompressedBlockAck::multiTid +inet::ieee80211::Ieee80211CompressedBlockAck::reserved +inet::ieee80211::Ieee80211CompressedBlockAckReq::barAckPolicy inet::ieee80211::Ieee80211CompressedBlockAckReq::compressedBitmap inet::ieee80211::Ieee80211CompressedBlockAckReq::fragmentNumber inet::ieee80211::Ieee80211CompressedBlockAckReq::multiTid +inet::ieee80211::Ieee80211CompressedBlockAckReq::reserved // -- MRP TLV length bytes: the serializer derives them from what it writes, so // the model field is not the value on the wire inet::MrpEnd::valueLength diff --git a/tests/unit/Ieee80211BlockAckWire_1.test b/tests/unit/Ieee80211BlockAckWire_1.test index 0cd32ee3a46..41c8138da32 100644 --- a/tests/unit/Ieee80211BlockAckWire_1.test +++ b/tests/unit/Ieee80211BlockAckWire_1.test @@ -1,36 +1,230 @@ %description: -Check Basic and Compressed bitmap values after independent byte decode. -The decoder must preserve the same bitmap contents without unowned temporary objects. -Type validation remains outside this prerequisite. +Check Basic Type 0 and Compressed Type 2 BAR and Block Ack against independent octets. +Check reserved bits, unsupported Types, malformed fragment subfields, and fixed lengths. +The wire oracle follows the C1 contract in the Compressed Block Ack replacement plan. +Basic fields preserve the retained Type 0 compatibility contract. +Compressed uses the non-HE, 64-position format from IEEE Std 802.11-2024, 9.3.1.7.2 and 9.3.1.8.2. +This test does not establish complete exchanges or response airtime. %includes: #include "inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" +%global: using namespace inet; using namespace inet::ieee80211; -%activity: -for (bool compressed : {false, true}) { - std::vector bytes(compressed ? 28 : 148, 0); - bytes[0] = 0x94; - bytes[16] = compressed ? 4 : 0; - bytes[20] = 1; +class BlockAckWireSerializer : public Ieee80211MacHeaderSerializer +{ + public: + using Ieee80211MacHeaderSerializer::serializeFields; + using Ieee80211MacHeaderSerializer::deserializeFields; +}; + +static std::vector encode(const Ptr& header) +{ + BlockAckWireSerializer serializer; + MemoryOutputStream stream; + serializer.serializeFields(stream, header); + return stream.getData(); +} + +static Ptr decode(const std::vector& bytes) +{ MemoryInputStream stream(bytes); - auto decoded = Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); - if (compressed) { - auto response = dynamicPtrCast(decoded); - ASSERT(response && response->getBlockAckBitmap().getSize() == 64); - ASSERT(response->getBlockAckBitmap().getBit(0)); - ASSERT(!response->getBlockAckBitmap().getBit(63)); + return Chunk::deserialize(stream, typeid(Ieee80211MacHeader)); +} + +static void requireWriteFailure(const Ptr& header) +{ + bool rejected = false; + try { encode(header); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); +} + +static void addresses(const Ptr& header) +{ + header->setReceiverAddress(MacAddress("02:11:22:33:44:55")); + header->setTransmitterAddress(MacAddress("06:aa:bb:cc:dd:ee")); + header->setDurationField(SimTime(0x1234, SIMTIME_US)); +} + +// Each vector is independent of the serializer output. +static std::vector golden(bool response, bool compressed) +{ + std::vector bytes = { + uint8_t(response ? 0x94 : 0x84), 0x00, 0x34, 0x12, + 0x02, 0x11, 0x22, 0x33, 0x44, 0x55, + 0x06, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, + uint8_t(compressed ? 0x04 : 0x21), 0x50, 0xe0, 0xff + }; + if (response) { + bytes.resize(compressed ? 28 : 148, 0); + // SSN 4094: positions 0, 1, 2, and 63 represent 4094, 4095, 0, and 61. + if (compressed) { + bytes[20] = 0x07; + bytes[27] = 0x80; + } + else { + // Basic position 0 reports fragments 0 and 15; position 63 reports fragment 8. + bytes[20] = 0x01; + bytes[21] = 0x80; + bytes[147] = 0x01; + } + } + return bytes; +} + +%activity: +OriginatorBlockAckProcedure procedure; +auto built = dynamicPtrCast( + procedure.buildCompressedBlockAckReqFrame(MacAddress("02:11:22:33:44:55"), 5, SequenceNumberCyclic(4094))); +ASSERT(built && built->getReceiverAddress() == MacAddress("02:11:22:33:44:55")); +ASSERT(built->getTidInfo() == 5 && built->getStartingSequenceNumber().get() == 4094); +ASSERT(built->getFragmentNumber() == 0 && built->getChunkLength() == B(20)); +addresses(built); +ASSERT(encode(built) == golden(false, true)); + +for (bool response : {false, true}) { + for (bool compressed : {false, true}) { + Ptr header; + if (response) { + if (compressed) { + auto ba = makeShared(); + ba->setBlockAckPolicy(true); + ba->setReserved(0x1ff); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + ba->setBlockAckBitmap(BitVector(std::vector{7, 0, 0, 0, 0, 0, 0, 0x80})); + ASSERT(ba->getChunkLength() == B(28)); + header = ba; + } + else { + auto ba = makeShared(); + ba->setBlockAckPolicy(true); + ba->setReserved(4); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + for (int i = 0; i < 64; i++) + ba->setBlockAckBitmap(i, BitVector(std::vector{uint8_t(i == 0), uint8_t(i == 0 ? 0x80 : i == 63 ? 1 : 0)})); + ASSERT(ba->getChunkLength() == B(148)); + header = ba; + } + } + else { + if (compressed) { + auto bar = makeShared(); + bar->setBarAckPolicy(true); + bar->setReserved(0x1ff); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + header = bar; + } + else { + auto bar = makeShared(); + bar->setBarAckPolicy(true); + bar->setReserved(4); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(4094)); + header = bar; + } + } + addresses(header); + auto expected = golden(response, compressed); + ASSERT(encode(header) == expected); + ASSERT(header->getChunkLength() == B(expected.size())); + if (response) { + auto fcs = makeShared(); + ASSERT(header->getChunkLength() + fcs->getChunkLength() == B(compressed ? 32 : 152)); + } + auto result = decode(expected); + ASSERT(result->isCorrect()); + ASSERT(result->isComplete()); + ASSERT(encode(result) == expected); + if (response) { + if (compressed) { + auto ba = dynamicPtrCast(result); + ASSERT(ba && ba->getTidInfo() == 5 && ba->getStartingSequenceNumber().get() == 4094); + ASSERT(ba->getBlockAckBitmap().getSize() == 64); + ASSERT(ba->getBlockAckBitmap().getBit(0) && ba->getBlockAckBitmap().getBit(2)); + ASSERT(ba->getBlockAckBitmap().getBit(63) && !ba->getBlockAckBitmap().getBit(62)); + } + else { + auto ba = dynamicPtrCast(result); + ASSERT(ba && ba->getReserved() == 4 && ba->getBlockAckPolicy()); + ASSERT(ba->getBlockAckBitmap(0).getBit(15)); + ASSERT(ba->getBlockAckBitmap(63).getBit(8)); + } + } + else { + if (compressed) { + auto bar = dynamicPtrCast(result); + ASSERT(bar && bar->getTidInfo() == 5 && bar->getStartingSequenceNumber().get() == 4094); + } + else { + auto bar = dynamicPtrCast(result); + ASSERT(bar && bar->getReserved() == 4 && bar->getBarAckPolicy()); + } + } + if (compressed) { + auto reserved = expected; + reserved[16] |= 0xe1; + reserved[17] |= 0x0f; + auto ignored = decode(reserved); + ASSERT(ignored->isCorrect()); + ASSERT(encode(ignored) == expected); + if (response) { + auto ba = dynamicPtrCast(ignored); + ASSERT(ba && !ba->getBlockAckPolicy() && ba->getReserved() == 0); + } + else { + auto bar = dynamicPtrCast(ignored); + ASSERT(bar && !bar->getBarAckPolicy() && bar->getReserved() == 0); + } + auto malformed = expected; + malformed[18] |= 1; + ASSERT(!decode(malformed)->isCorrect()); + if (response) + dynamicPtrCast(header)->setFragmentNumber(1); + else + dynamicPtrCast(header)->setFragmentNumber(1); + requireWriteFailure(header); + } + else { + for (int reserved : {1, 2, 3}) { + if (response) + dynamicPtrCast(header)->setReserved(reserved); + else + dynamicPtrCast(header)->setReserved(reserved); + requireWriteFailure(header); + } + } + for (size_t length : {size_t(17), expected.size() - 1}) { + auto shortFrame = expected; + shortFrame.resize(length); + ASSERT(!decode(shortFrame)->isComplete()); + } } - else { - auto response = dynamicPtrCast(decoded); - ASSERT(response && response->getBlockAckBitmapArraySize() == 64); - ASSERT(response->getBlockAckBitmap(0).getBit(0)); - ASSERT(!response->getBlockAckBitmap(63).getBit(15)); + for (int type = 0; type < 16; type++) { + if (type == 0 || type == 2) + continue; + auto unsupported = golden(response, true); + unsupported[16] = type << 1; + auto result = decode(unsupported); + ASSERT(!result->isCorrect()); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); + ASSERT(!dynamicPtrCast(result)); } } -EV << "Bitmap decode checks passed.\n"; +for (int bits : {0, 63, 65}) { + auto ba = makeShared(); + ba->setBlockAckBitmap(bits == 0 ? BitVector() : BitVector(0u, bits)); + requireWriteFailure(ba); +} +EV << "Block Ack wire checks passed\n"; %contains: stdout -Bitmap decode checks passed. +Block Ack wire checks passed From ca60df56ee0004bccb70dd58c4b80f1fdf77888e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:50:08 +0200 Subject: [PATCH 13/17] ieee80211: add: prepare QoS DATA without queue extraction Before this commit, QoS data services must extract source frames before they can create an aggregate. A caller cannot inspect that aggregate before queue removal and sequence assignment. After this commit, the service creates an owned preview while original MSDUs remain in their queue. Preparation copies the next sequence value without consumption. Successful commitment validates source identity, eligibility, lifetime, aggregation permission, and sequence state. It transfers the same preview and assigns its sequence number. Refusal preserves the preview and all source frames. Ordinary commitment uses normal dequeue for the head or selected extraction for a later eligible frame. The service copies completed region metadata into the preview before source deletion. Queue service effects occur once. Aggregation retains its existing source-removal behavior. For example, ordinary DATA that waits 2 ms retains 2 ms of queueing time and one queue departure after commitment. Direct service tests cover head and non-head commitment, refusal, stale state, and cancellation. Existing extraction remains available. Later runtime activation uses this contract to admit the complete DATA exchange before destructive preparation. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 6 + doc/src/migration-guide/index.rst | 5 + .../contract/IOriginatorQosMacDataService.h | 20 ++ .../originator/OriginatorQosMacDataService.cc | 155 +++++++++ .../originator/OriginatorQosMacDataService.h | 3 + tests/module/Ieee80211PreparedQosData_1.test | 294 ++++++++++++++++++ 6 files changed, 483 insertions(+) create mode 100644 tests/module/Ieee80211PreparedQosData_1.test diff --git a/WHATSNEW b/WHATSNEW index ec626754ce9..47096b8c45d 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -470,6 +470,12 @@ Notable backward incompatible changes are the following: 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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 7a2be4ab354..5a2ce312bea 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -38,6 +38,11 @@ The serializer decodes the complete four-bit Type field. Type 0 retains Basic co 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. + IEEE 802.11 Basic Block Ack extension points ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h index e66fa555666..04f0ecda71b 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h @@ -7,12 +7,27 @@ #ifndef __INET_IORIGINATORQOSMACDATASERVICE_H #define __INET_IORIGINATORQOSMACDATASERVICE_H +#include + #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h" namespace inet { namespace ieee80211 { +/** Owns a reversible DATA preview. Source packet identities refer to frames still owned by the queue. */ +struct INET_API PreparedQosData : public cObject +{ + std::unique_ptr packet; + queueing::IPacketQueue *queue = nullptr; + std::vector sourceIds; + bool aggregate = false; + + ~PreparedQosData() override; + void setPacket(Packet *value); + Packet *releasePacket(); +}; + class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService { public: @@ -20,6 +35,11 @@ class INET_API IOriginatorQosMacDataService : public IOriginatorMacDataService virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const = 0; virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const = 0; virtual bool hasAMsduAggregation() const = 0; + // These QoS DATA methods separate reversible preparation from extraction and sequence assignment. + virtual std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) = 0; + virtual bool isPreparationValid(const PreparedQosData& preparation, simtime_t start) const = 0; + // Success transfers the preview to the caller. Failure retains the preview and all queued sources. + virtual Packet *commitPreparedFrame(PreparedQosData& preparation) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index 40f7e33106c..7986ca4f13c 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -22,6 +22,27 @@ namespace ieee80211 { Define_Module(OriginatorQosMacDataService); +PreparedQosData::~PreparedQosData() +{ + if (packet) + drop(packet.get()); +} + +void PreparedQosData::setPacket(Packet *value) +{ + if (packet) + drop(packet.get()); + packet.reset(value); + if (packet) + take(packet.get()); +} + +Packet *PreparedQosData::releasePacket() +{ + drop(packet.get()); + return packet.release(); +} + void OriginatorQosMacDataService::initialize() { aMsduAggregationPolicy = dynamic_cast(getSubmodule("msduAggregationPolicy")); @@ -35,6 +56,140 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } +std::unique_ptr OriginatorQosMacDataService::prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) +{ + Enter_Method("prepareFrame"); + if (expireFrames) + expireFrames(); + auto selected = peekEligibleFrame(queue); + if (!selected) + return nullptr; + auto selectedHeader = dynamicPtrCast(selected->peekAtFront()); + if (!selectedHeader || selectedHeader->getType() != ST_DATA_WITH_QOS) + return nullptr; + auto result = std::make_unique(); + result->queue = queue; + std::vector sources{selected}; + auto module = dynamic_cast(queue); + auto head = queue->canPullPacket(module ? module->gate("out") : nullptr); + if (allowAggregation && selected == head && aMsduAggregationPolicy && (!aMsduPermitted || aMsduPermitted(selected))) { + std::unique_ptr> proposed(aMsduAggregationPolicy->computeAggregateFrames(queue)); + bool valid = proposed && !proposed->empty() && proposed->front() == selected; + std::set unique; + if (proposed) { + for (auto frame : *proposed) { + bool present = false; + for (int i = 0; i < queue->getNumPackets(); i++) + present |= queue->getPacket(i) == frame; + valid &= present && unique.insert(frame).second && + (!extractionEligible || extractionEligible(frame)) && (!aMsduPermitted || aMsduPermitted(frame)); + } + } + if (valid) { + sources = *proposed; + result->aggregate = true; + } + } + for (auto source : sources) + result->sourceIds.push_back(source->getId()); + if (result->aggregate) { + std::vector duplicates; + for (auto source : sources) + duplicates.push_back(source->dup()); + result->setPacket(aMsduAggregation->aggregateFrames(&duplicates)); + } + else + result->setPacket(selected->dup()); + // The preview carries the next value without consuming the sequence counter. + auto header = result->packet->removeAtFront(); + header->setSequenceNumber(getNextSequenceNumber(header->getReceiverAddress(), header->getTid())); + result->packet->insertAtFront(header); + return result; +} + +bool OriginatorQosMacDataService::isPreparationValid(const PreparedQosData& preparation, simtime_t start) const +{ + if (!preparation.packet || !preparation.queue || preparation.sourceIds.empty()) + return false; + auto header = preparation.packet->peekAtFront(); + if (header->getSequenceNumber() != getNextSequenceNumber(header->getReceiverAddress(), header->getTid())) + return false; + for (auto id : preparation.sourceIds) { + Packet *source = nullptr; + for (int i = 0; i < preparation.queue->getNumPackets(); i++) { + auto candidate = preparation.queue->getPacket(i); + if (candidate->getId() == id) { + source = candidate; + break; + } + } + if (!source || (lifetimeHandler && lifetimeHandler->isExpired(source, start)) || + (extractionEligible && !extractionEligible(source)) || + (preparation.aggregate && aMsduPermitted && !aMsduPermitted(source))) + return false; + } + return true; +} + +Packet *OriginatorQosMacDataService::commitPreparedFrame(PreparedQosData& preparation) +{ + Enter_Method("commitPreparedFrame"); + if (!isPreparationValid(preparation, simTime())) + return nullptr; + std::vector sources; + std::vector trackedIds; + simtime_t deadline = SIMTIME_MAX; + for (auto id : preparation.sourceIds) { + for (int i = 0; i < preparation.queue->getNumPackets(); i++) { + auto source = preparation.queue->getPacket(i); + if (source->getId() == id) { + sources.push_back(source); + if (lifetimeHandler && lifetimeHandler->getDeadline(source) != SIMTIME_MAX) { + trackedIds.push_back(id); + deadline = lifetimeHandler->getDeadline(source); + } + break; + } + } + } + for (auto source : sources) { + if (preparation.aggregate) + preparation.queue->removePacket(source); + else { + auto module = dynamic_cast(preparation.queue); + auto next = preparation.queue->canPullPacket(module ? module->gate("out") : nullptr); + 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; +} + Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *selected) { auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index f6590365817..73bda528989 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -61,6 +61,9 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService virtual Packet *peekEligibleFrame(queueing::IPacketQueue *queue) const override; virtual SequenceNumberCyclic getNextSequenceNumber(MacAddress peer, Tid tid) const override; virtual bool hasAMsduAggregation() const override { return aMsduAggregationPolicy != nullptr; } + virtual std::unique_ptr prepareFrame(queueing::IPacketQueue *queue, bool allowAggregation) override; + virtual bool isPreparationValid(const PreparedQosData& preparation, simtime_t start) const override; + virtual Packet *commitPreparedFrame(PreparedQosData& preparation) override; virtual ~OriginatorQosMacDataService(); void setLifetimeHandler(EdcaTransmitLifetimeHandler *handler, std::function expiration) { lifetimeHandler = handler; diff --git a/tests/module/Ieee80211PreparedQosData_1.test b/tests/module/Ieee80211PreparedQosData_1.test new file mode 100644 index 00000000000..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: From 1f7b8f76ea114634ca8797df281dce8870675a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:50:34 +0200 Subject: [PATCH 14/17] ieee80211: add: retain explicit Compressed receipt state Before this commit, recipient receipt state uses the Basic fragment bitmap. Delivery cleanup cannot supply the independent complete-MPDU receipt evidence that a Compressed response needs. After this commit, the record retains explicit complete receipts within its Compressed window. The agreement selects that record profile. The response builder copies receipt truth into a 64-bit bitmap. Positive receipts survive delivery cleanup, while absent positions remain zero. For example, a window at 4094 retains sequence 4094 after delivery and moves across sequence wrap without false positive bits. The direct record fixture checks window movement, the preceding receipt, absent positions, and response construction. HCF still refuses Compressed selection. This prerequisite activates no incomplete exchange. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 ++ .../ieee80211/mac/blockack/BlockAckRecord.cc | 44 ++++++++++++ .../ieee80211/mac/blockack/BlockAckRecord.h | 9 +++ .../mac/blockack/RecipientBlockAckAgreement.h | 8 +++ .../blockack/RecipientBlockAckProcedure.cc | 14 ++++ .../Ieee80211CompressedBlockAckRecord_1.test | 71 +++++++++++++++++++ 6 files changed, 150 insertions(+) create mode 100644 tests/unit/Ieee80211CompressedBlockAckRecord_1.test diff --git a/WHATSNEW b/WHATSNEW index 47096b8c45d..0c95bd28073 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -476,6 +476,10 @@ Notable backward incompatible changes are the following: 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. Compressed receipt helpers + + Compressed receipt helpers now retain positive receipt state independently of delivery. HCF still refuses the Compressed profile until complete exchange support exists. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 919bd813a6f..fa285580703 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -53,6 +53,50 @@ void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) } } +void BlockAckRecord::initializeCompressedWindow(SequenceNumberCyclic start, int size) +{ + ASSERT(start.isSpecified() && size > 0 && size <= 64); + windowStart = start; + windowSize = size; + completeReceipts.clear(); +} + +void BlockAckRecord::advanceCompressedWindow(SequenceNumberCyclic start) +{ + // IEEE Std 802.11-2024, 10.25.6.3: retain explicit receipt truth at WinStartR - 1. + if (!(windowStart < start)) + return; + windowStart = start; + for (auto it = completeReceipts.begin(); it != completeReceipts.end();) { + auto distance = (*it - windowStart.get() + 4096) % 4096; + if (distance >= windowSize && distance != 4095) + it = completeReceipts.erase(it); + else + ++it; + } +} + +void BlockAckRecord::compressedFrameReceived(const Ptr& header) +{ + ASSERT(header->getFragmentNumber() == 0 && !header->getMoreFragments()); + auto sequence = header->getSequenceNumber(); + // IEEE Std 802.11-2024, 10.25.6.3: advance before duplicate removal or reorder insertion. + if (windowStart <= sequence && !(sequence < windowStart + windowSize)) + advanceCompressedWindow(sequence - windowSize + 1); + if (windowStart <= sequence && sequence < windowStart + windowSize) + completeReceipts.insert(sequence.get()); +} + +bool BlockAckRecord::hasCompleteReceipt(SequenceNumberCyclic sequenceNumber) const +{ + return completeReceipts.count(sequenceNumber.get()) != 0; +} + +bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) const +{ + return windowStart <= sequenceNumber && sequenceNumber < windowStart + windowSize && hasCompleteReceipt(sequenceNumber); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h index 971be8a8af9..1c90bf392dc 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -25,6 +25,9 @@ class INET_API BlockAckRecord MacAddress originatorAddress = MacAddress::UNSPECIFIED_ADDRESS; Tid tid = -1; std::map acknowledgmentState; + SequenceNumberCyclic windowStart; + int windowSize = 64; // Sequence positions; the Compressed profile permits 1..64. + std::set completeReceipts; public: BlockAckRecord(MacAddress originatorAddress, Tid tid); @@ -33,6 +36,12 @@ class INET_API BlockAckRecord void blockAckPolicyFrameReceived(const Ptr& header); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); void removeAckStates(SequenceNumberCyclic sequenceNumber); + void initializeCompressedWindow(SequenceNumberCyclic start, int size); + void compressedFrameReceived(const Ptr& header); + void advanceCompressedWindow(SequenceNumberCyclic start); + bool getCompressedAckState(SequenceNumberCyclic sequenceNumber) const; + bool hasCompleteReceipt(SequenceNumberCyclic sequenceNumber) const; + SequenceNumberCyclic getWindowStart() const { return windowStart; } MacAddress getOriginatorAddress() { return originatorAddress; } Tid getTid() { return tid; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 6e9634cae6f..db10d2d489b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -9,6 +9,7 @@ #define __INET_RECIPIENTBLOCKACKAGREEMENT_H #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" namespace inet { namespace ieee80211 { @@ -25,6 +26,7 @@ class INET_API RecipientBlockAckAgreement : public cObject simtime_t expirationTime = -1; SequenceNumberCyclic highestReceivedSequence; int highestReceivedFragment = 0; + BlockAckVariant variant = BlockAckVariant::BASIC; public: RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); @@ -35,6 +37,12 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual BlockAckRecord *getBlockAckRecord() const { return blockAckRecord; } virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } + BlockAckVariant getVariant() const { return variant; } + void setVariant(BlockAckVariant value) { + variant = value; + if (variant == BlockAckVariant::COMPRESSED) + blockAckRecord->initializeCompressedWindow(startingSequenceNumber, bufferSize); + } void recordReceived(const Ptr& header) { auto sequence = header->getSequenceNumber(); if (!highestReceivedSequence.isSpecified() || highestReceivedSequence < sequence) { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..7573552b754 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -65,6 +65,20 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr blockAck->setTidInfo(basicBlockAckReq->getTidInfo()); return blockAck; } + else if (auto compressed = dynamicPtrCast(blockAckReq)) { + ASSERT(agreement != nullptr); + auto blockAck = makeShared(); + auto record = agreement->getBlockAckRecord(); + auto start = record->getWindowStart(); + BitVector bitmap(0, 64); + for (int i = 0; i < 64; i++) + bitmap.setBit(i, record->getCompressedAckState(start + i)); + blockAck->setBlockAckBitmap(bitmap); + blockAck->setReceiverAddress(compressed->getTransmitterAddress()); + blockAck->setStartingSequenceNumber(start); + blockAck->setTidInfo(compressed->getTidInfo()); + return blockAck; + } else throw cRuntimeError("Unsupported Block Ack Request"); } diff --git a/tests/unit/Ieee80211CompressedBlockAckRecord_1.test b/tests/unit/Ieee80211CompressedBlockAckRecord_1.test new file mode 100644 index 00000000000..58daa02e92a --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAckRecord_1.test @@ -0,0 +1,71 @@ +%description: +Check explicit Compressed receipt state independently of the delivery buffer. +Absent positions remain zero after delivery, window movement, and sequence wrap. +The preceding receipt remains available for the individual-MPDU release condition. +These checks do not establish HCF interframe recognition. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" + +using namespace inet; +using namespace inet::ieee80211; + +class ReceiptProcedure : public RecipientBlockAckProcedure { + public: + using RecipientBlockAckProcedure::buildBlockAck; +}; + +%activity: +const MacAddress peer("02:00:00:00:00:02"); +for (int start : {100, 4094}) { + RecipientBlockAckAgreement agreement(peer, 5, SequenceNumberCyclic(start), 4, SIMTIME_ZERO); + agreement.setVariant(BlockAckVariant::COMPRESSED); + auto record = agreement.getBlockAckRecord(); + auto bar = makeShared(); + bar->setTransmitterAddress(peer); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + ReceiptProcedure procedure; + auto response = dynamicPtrCast(procedure.buildBlockAck(bar, &agreement)); + ASSERT(response != nullptr && response->getChunkLength() == B(28)); + ASSERT(response->getBlockAckBitmap().getSize() == 64); + for (int bit = 0; bit < 64; bit++) + ASSERT(!response->getBlockAckBitmap().getBit(bit)); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setTransmitterAddress(peer); + data->setTid(5); + data->setAckPolicy(NORMAL_ACK); + data->setSequenceNumber(SequenceNumberCyclic(start)); + record->compressedFrameReceived(data); + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start))); + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start) - 1)); + // Delivery and Basic compatibility cleanup must not erase Compressed receipt truth. + record->removeAckStates(SequenceNumberCyclic(start) + 1); + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start))); + data->setSequenceNumber(SequenceNumberCyclic(start) + 4); + record->compressedFrameReceived(data); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(start) + 1); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(start))); + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start))); + response = dynamicPtrCast(procedure.buildBlockAck(bar, &agreement)); + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(start) + 1); + for (int bit = 0; bit < 64; bit++) + ASSERT(response->getBlockAckBitmap().getBit(bit) == (bit == 3)); + record->advanceCompressedWindow(SequenceNumberCyclic(start) + 5); + ASSERT(record->hasCompleteReceipt(SequenceNumberCyclic(start) + 4)); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(start))); + record->advanceCompressedWindow(SequenceNumberCyclic(start) + 8); + ASSERT(!record->hasCompleteReceipt(SequenceNumberCyclic(start) + 4)); + for (int bit = 0; bit < 64; bit++) + ASSERT(!record->getCompressedAckState(SequenceNumberCyclic(start) + 8 + bit)); + // An old BAR must preserve the current scoreboard start. + record->advanceCompressedWindow(SequenceNumberCyclic(start)); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(start) + 8); +} +EV << "Compressed receipt state checks passed.\n"; + +%contains: stdout +Compressed receipt state checks passed. From 5b87231f412ca4266dc2cf0b9d38b18df7c6dd19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:51:17 +0200 Subject: [PATCH 15/17] ieee80211: add: select legal Compressed control rates Before this commit, QoS rate selection supports only the Basic Block Ack response contract. Compressed exchanges need distinct non-HT BAR selection and response-duration rules. After this commit, the selector uses the applicable local, BSS, and peer rate sets for initial and later BAR. Compressed responses use their non-HT reference rate and 32-byte airtime. Invalid configured overrides fail explicitly. The new pure interface operation requires matching custom implementations. For example, two peers with different operational rates can require different later-BAR rates. The direct rate fixture checks both peers, both response directions, Basic compatibility, and invalid overrides. HCF still refuses Compressed selection. This prerequisite activates no incomplete exchange. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 4 + doc/src/migration-guide/index.rst | 5 + .../mac/contract/IQosRateSelection.h | 3 + .../mac/rateselection/QosRateSelection.cc | 80 +++++++++ .../mac/rateselection/QosRateSelection.h | 2 + .../Ieee80211HcfMultipleProtection_1.test | 3 + .../Ieee80211HcfSingleProtection_1.test | 3 + .../Ieee80211SingleProtectionGroup_1.test | 1 + tests/module/Ieee80211Txnav_1.test | 1 + tests/module/Ieee80211VhtHtSupport_1.test | 3 + .../Ieee80211CompressedBlockAckRate_1.test | 169 ++++++++++++++++++ tests/unit/Ieee80211ResponseTimeout_1.test | 1 + 12 files changed, 275 insertions(+) create mode 100644 tests/unit/Ieee80211CompressedBlockAckRate_1.test diff --git a/WHATSNEW b/WHATSNEW index 0c95bd28073..c8a291bd77e 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -480,6 +480,10 @@ Notable backward incompatible changes are the following: Compressed receipt helpers now retain positive receipt state independently of delivery. HCF still refuses the Compressed profile until complete exchange support exists. +59. Compressed control-rate helpers + + QoS rate selection now supplies non-HT Compressed BAR and response modes. HCF still refuses the Compressed profile until complete exchange support exists. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 5a2ce312bea..536ee36a263 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -43,6 +43,11 @@ 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. +IEEE 802.11 Compressed control rates +------------------------------------ + +Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. This operation prepares legal non-HT BAR rate selection from the supplied TXOP position and applicable BSS or peer rates. HCF retains its Compressed-profile rejection in this prerequisite. Runtime activation will supply the request's actual TXOP position. Existing Basic methods retain their contracts. + IEEE 802.11 Basic Block Ack extension points ------------------------------------------ diff --git a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h index 34a38ac1d9a..10c35f613a0 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IQosRateSelection.h @@ -32,6 +32,9 @@ class INET_API IQosRateSelection virtual const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame, ResponseQueryRole role) = 0; virtual const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& dataOrMgmtHeader, ResponseQueryRole role) = 0; virtual const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq, ResponseQueryRole role) = 0; + // HCF derives startsTxop from the current frame sequence before this query. + virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) = 0; virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 82db4bbd06c..5c65ba9d116 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -144,10 +144,90 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet auto mode = responseBlockAckFrameMode ? responseBlockAckFrameMode : getMode(packet, blockAckReq); return getPeerCompatibleResponseMode(blockAckReq, mode, role); } + else if (dynamicPtrCast(blockAckReq)) { + auto received = getMode(packet, blockAckReq); + auto referenceRate = received->getNonHtReferenceRate(); + auto receivedClass = received->getModulationClass(); + bool highThroughput = receivedClass == IIeee80211Mode::ModulationClass::HT || receivedClass == IIeee80211Mode::ModulationClass::VHT; + if (!std::isfinite(referenceRate.get())) + throw cRuntimeError("Compressed Block Ack has no non-HT reference rate for the BAR mode"); + const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; + auto matchesClass = [received, receivedClass, highThroughput](const IIeee80211Mode *candidate) { + auto modulation = candidate->getModulationClass(); + return highThroughput ? modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM : + modulation == receivedClass && candidate->getLegacyPreambleType() == received->getLegacyPreambleType(); + }; + const IIeee80211Mode *primary = nullptr; + // IEEE Std 802.11-2024, 10.6.6.5.2: prefer a qualifying BSS basic rate, then a mandatory rate. + for (bool useBasic : {true, false}) { + if (useBasic && (!basic || !basic->known)) + continue; // INET fallback for unknown BSS state uses the mandatory rate set. + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto candidate = modeSet->getMode(i); + auto rate = candidate->getDataMode()->getNetBitrate(); + bool eligible = useBasic ? basic->legacyRates.count(rate) != 0 : modeSet->isMandatory(i); + if (eligible && matchesClass(candidate) && rate <= referenceRate && + (!primary || rate > primary->getDataMode()->getNetBitrate())) + primary = candidate; + } + if (primary) + break; + } + if (!primary) + throw cRuntimeError("Compressed Block Ack has no legal non-HT response mode"); + if (!responseBlockAckFrameMode || responseBlockAckFrameMode == primary) + return primary; + auto alternate = responseBlockAckFrameMode; + auto rate = alternate->getDataMode()->getNetBitrate(); + bool basicOrMandatory = (basic && basic->known && basic->legacyRates.count(rate)) || modeSet->getIsMandatory(alternate); + // IEEE Std 802.11-2024, 10.6.6.5.4: an alternate preserves modulation and actual response duration. + if (!matchesClass(alternate) || alternate->getModulationClass() != primary->getModulationClass() || !basicOrMandatory || + alternate->getDuration(LENGTH_COMPRESSED_BLOCKACK) != primary->getDuration(LENGTH_COMPRESSED_BLOCKACK)) + throw cRuntimeError("responseBlockAckFrameBitrate is not a legal alternate Compressed Block Ack response rate"); + return alternate; + } else throw cRuntimeError("Unknown BlockAckReq frame type"); } +const IIeee80211Mode *QosRateSelection::computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) +{ + const Ieee80211RateSet *local = mib ? &mib->getLocalRateSet().operational : nullptr; + const Ieee80211RateSet *basic = mib ? &mib->getBssRateSet().basic : nullptr; + auto peerState = mib ? mib->findPeerRateSet(blockAckReq->getReceiverAddress()) : nullptr; + const Ieee80211RateSet *peer = peerState ? &peerState->operational : nullptr; + auto eligible = [&](const IIeee80211Mode *candidate) { + auto modulation = candidate->getModulationClass(); + bool legacy = modulation == IIeee80211Mode::ModulationClass::DSSS_HRDSSS || + modulation == IIeee80211Mode::ModulationClass::OFDM || modulation == IIeee80211Mode::ModulationClass::ERP_OFDM; + auto rate = candidate->getDataMode()->getNetBitrate(); + if (!legacy || (local && local->known && !local->legacyRates.count(rate))) + return false; + // IEEE Std 802.11-2024, 10.6.6.2 and 10.6.6.4: initial BAR and later BAR use different sets. + if (!startsTxop && peer && peer->known) + return peer->legacyRates.count(rate) != 0; + if (basic && basic->known && !basic->legacyRates.empty()) + return basic->legacyRates.count(rate) != 0; + // INET uses mandatory rates when BSS rate state is unknown. + return modeSet->getIsMandatory(candidate); + }; + if (controlFrameMode) { + if (!eligible(controlFrameMode)) + throw cRuntimeError("controlFrameBitrate is not a legal non-HT Compressed BAR rate for this TXOP position"); + return controlFrameMode; + } + const IIeee80211Mode *selected = nullptr; + for (int i = 0; i < modeSet->getNumModes(); i++) { + auto candidate = modeSet->getMode(i); + if (eligible(candidate) && (!selected || candidate->getDataMode()->getNetBitrate() > selected->getDataMode()->getNetBitrate())) + selected = candidate; + } + if (!selected) + throw cRuntimeError("Compressed BAR has no legal local non-HT mode"); + return selected; +} + const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 5bc4647a89e..c0ecc61f8a1 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -84,6 +84,8 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame, ResponseQueryRole role) override; virtual const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& dataOrMgmtHeader, ResponseQueryRole role) override; virtual const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq, ResponseQueryRole role) override; + virtual const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, + const Ptr& blockAckReq, bool startsTxop) override; virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override; diff --git a/tests/module/Ieee80211HcfMultipleProtection_1.test b/tests/module/Ieee80211HcfMultipleProtection_1.test index ea7991de3ee..852333eb985 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_1.test +++ b/tests/module/Ieee80211HcfMultipleProtection_1.test @@ -109,6 +109,9 @@ class ProtectionRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } }; class ProtectionHcf : public Hcf { diff --git a/tests/module/Ieee80211HcfSingleProtection_1.test b/tests/module/Ieee80211HcfSingleProtection_1.test index bcf69e606b9..1ef31e67f3d 100644 --- a/tests/module/Ieee80211HcfSingleProtection_1.test +++ b/tests/module/Ieee80211HcfSingleProtection_1.test @@ -91,6 +91,9 @@ class SingleRateSelection : public IQosRateSelection { const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } }; class SingleRateControl : public IRateControl { public: diff --git a/tests/module/Ieee80211SingleProtectionGroup_1.test b/tests/module/Ieee80211SingleProtectionGroup_1.test index c5ff47f33c0..0bb495f62cf 100644 --- a/tests/module/Ieee80211SingleProtectionGroup_1.test +++ b/tests/module/Ieee80211SingleProtectionGroup_1.test @@ -28,6 +28,7 @@ class GroupRateSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txop) override { return delegate->computeMode(packet, header, txop); } const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } }; class GroupMechanism : public SingleProtectionMechanism { public: diff --git a/tests/module/Ieee80211Txnav_1.test b/tests/module/Ieee80211Txnav_1.test index 452f1bc2091..0e23937594f 100644 --- a/tests/module/Ieee80211Txnav_1.test +++ b/tests/module/Ieee80211Txnav_1.test @@ -32,6 +32,7 @@ class TxnavSelection : public IQosRateSelection { const physicallayer::IIeee80211Mode *computeResponseCtsFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseCtsFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseAckFrameMode(packet, header, role); } const physicallayer::IIeee80211Mode *computeResponseBlockAckFrameMode(Packet *packet, const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); } }; class TxnavRx : public Rx { public: diff --git a/tests/module/Ieee80211VhtHtSupport_1.test b/tests/module/Ieee80211VhtHtSupport_1.test index 72514a2b82b..f9b9501aada 100644 --- a/tests/module/Ieee80211VhtHtSupport_1.test +++ b/tests/module/Ieee80211VhtHtSupport_1.test @@ -68,6 +68,9 @@ class VhtHtSelection : public IQosRateSelection { const Ptr& header, ResponseQueryRole role) override { return delegate->computeResponseBlockAckFrameMode(packet, header, role); } + const IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *packet, const Ptr& header, bool startsTxop) override { + return delegate->computeCompressedBlockAckReqMode(packet, header, startsTxop); + } }; class VhtHtRx : public Rx { public: diff --git a/tests/unit/Ieee80211CompressedBlockAckRate_1.test b/tests/unit/Ieee80211CompressedBlockAckRate_1.test new file mode 100644 index 00000000000..1aa02ea3a16 --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAckRate_1.test @@ -0,0 +1,169 @@ +%description: +Check Compressed response rates through the production QoS rate selector. +Use independent MIB rate sets for two peers and both response directions. +Retain the Basic response contract and reject an invalid Compressed override. + +%includes: +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211CompressedBlockAckRate_1 { +class CompressedRateSelection : public QosRateSelection { + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LOCAL) + mib.reference(this, "mibModule", true); + } + public: + void configure(const Ieee80211ModeSet *catalog, const IIeee80211Mode *response) { + modeSet = catalog; + responseBlockAckFrameMode = response; + } + void controlOverride(const IIeee80211Mode *mode) { controlFrameMode = mode; } +}; +Define_Module(CompressedRateSelection); + +static Ieee80211RateSetState rates(std::initializer_list basic) { + Ieee80211RateSetState state; + state.supported.known = state.operational.known = state.basic.known = true; + for (int rate : {6, 9, 12, 18, 24, 36, 48, 54}) + state.supported.legacyRates.insert(Mbps(rate)); + state.operational = state.supported; + for (int rate : basic) + state.basic.legacyRates.insert(Mbps(rate)); + return state; +} +} + +%file: TestNetwork.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mib.Ieee80211Mib; +simple CompressedRateSelection extends SimpleModule { + parameters: + @class(CompressedRateSelection); + string mibModule; +} +network TestNetwork { + submodules: + test: Test; + mib: Ieee80211Mib; + selector: CompressedRateSelection { parameters: mibModule = "^.mib"; } +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +record-vector-results = false +record-scalar-results = false + +%activity: +using namespace Ieee80211CompressedBlockAckRate_1; +auto mib = check_and_cast(getModuleByPath("mib")); +auto selector = check_and_cast(getModuleByPath("selector")); +const MacAddress a("02:00:00:00:00:01"), b("02:00:00:00:00:02"); +for (const char *name : {"n(mixed-2.4Ghz)", "ac"}) { + auto catalog = Ieee80211ModeSet::getModeSet(name); + auto mode = [&](int rate) { return catalog->getMode(Mbps(rate)); }; + auto inputCatalog = Ieee80211ModeSet::getModeSet(name[0] == 'a' ? "a" : "g(mixed)"); + auto fastInput = inputCatalog->getMode(Mbps(54)); + mib->setLocalRateSet(rates({6, 12, 24})); + mib->setBssRateSet(rates({6, 12, 24})); + selector->configure(catalog, nullptr); + auto bar = makeShared(); + bar->setReceiverAddress(b); + bar->setTransmitterAddress(a); + auto check = [&](const IIeee80211Mode *input, int expected) { + Packet predict("predict"), respond("respond"); + predict.addTag()->setMode(input); + respond.addTag()->setMode(input); + auto predicted = selector->computeResponseBlockAckFrameMode(&predict, bar, ResponseQueryRole::PREDICT); + auto response = selector->computeResponseBlockAckFrameMode(&respond, bar, ResponseQueryRole::RESPOND); + ASSERT(predicted == mode(expected)); + ASSERT(response == predicted); + ASSERT(response->getHtMcsIndex() < 0); + ASSERT(response->getDuration(B(32)) < response->getDuration(LENGTH_BASIC_BLOCKACK)); + }; + check(fastInput, 24); + check(mode(6), 6); + mib->setBssRateSet(rates({48})); + check(mode(24), 24); + mib->clearBssRateSet(); + check(fastInput, 24); + mib->setBssRateSet(rates({})); + check(fastInput, 24); + mib->setBssRateSet(rates({6, 12, 24})); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto ht = catalog->getMode(i); + auto family = ht->getModulationClass(); + if (family != IIeee80211Mode::ModulationClass::HT && family != IIeee80211Mode::ModulationClass::VHT) + continue; + auto reference = ht->getNonHtReferenceRate(); + int expected = reference >= Mbps(24) ? 24 : reference >= Mbps(12) ? 12 : 6; + check(ht, expected); + } + mib->setBssRateSet(rates({48})); + selector->configure(catalog, mode(6)); + bool refused = false; + try { check(fastInput, 6); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + selector->configure(catalog, nullptr); + auto basic = makeShared(); + Packet packet("basic"); + packet.addTag()->setMode(mode(24)); + ASSERT(selector->computeResponseBlockAckFrameMode(&packet, basic, ResponseQueryRole::RESPOND) == mode(24)); + auto bss = rates({6, 12, 24}); + auto peer = rates({6}); + peer.operational.legacyRates = {Mbps(6), Mbps(12)}; + mib->installBssAndPeerRateSets(bss, b, peer); + Packet originated("originated"); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, false) == mode(12)); + mib->removePeerRateSet(b); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, false) == mode(24)); + mib->setBssRateSet(rates({})); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + mib->clearBssRateSet(); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(24)); + selector->controlOverride(mode(6)); + ASSERT(selector->computeCompressedBlockAckReqMode(&originated, bar, true) == mode(6)); + mib->setBssRateSet(rates({6})); + selector->controlOverride(mode(24)); + refused = false; + try { selector->computeCompressedBlockAckReqMode(&originated, bar, true); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto ht = catalog->getMode(i); + if (ht->getModulationClass() != IIeee80211Mode::ModulationClass::HT) + continue; + selector->controlOverride(ht); + refused = false; + try { selector->computeCompressedBlockAckReqMode(&originated, bar, true); } + catch (const cRuntimeError&) { refused = true; } + ASSERT(refused); + break; + } + selector->controlOverride(nullptr); +} +// The full OFDM catalog supplies optional rates for the alternate-rate contract. +auto ofdm = Ieee80211ModeSet::getModeSet("a"); +auto primary = ofdm->getMode(Mbps(48)); +auto alternate = ofdm->getMode(Mbps(36)); +ASSERT(primary->getDuration(B(32)) == alternate->getDuration(B(32))); +mib->setBssRateSet(rates({36, 48})); +selector->configure(ofdm, alternate); +Packet alternatePacket("alternate"); +alternatePacket.addTag()->setMode(ofdm->getMode(Mbps(54))); +auto alternateBar = makeShared(); +ASSERT(selector->computeResponseBlockAckFrameMode(&alternatePacket, alternateBar, ResponseQueryRole::RESPOND) == alternate); +EV << "Compressed response rate checks passed.\n"; + +%contains: stdout +Compressed response rate checks passed. diff --git a/tests/unit/Ieee80211ResponseTimeout_1.test b/tests/unit/Ieee80211ResponseTimeout_1.test index dcafbffa653..c55f286bb21 100644 --- a/tests/unit/Ieee80211ResponseTimeout_1.test +++ b/tests/unit/Ieee80211ResponseTimeout_1.test @@ -49,6 +49,7 @@ class TestRateSelection : public SimpleModule, public IRateSelection, public IQo const Ptr& header) override { return responseMode; } virtual const physicallayer::IIeee80211Mode *computeMode(Packet *packet, const Ptr& header, TxopProcedure *txopProcedure) override { return responseMode; } + const physicallayer::IIeee80211Mode *computeCompressedBlockAckReqMode(Packet *, const Ptr&, bool) override { return responseMode; } }; Define_Module(TestRateSelection); From 126c63f561d20fcfbb672668bc8c4d41c317f642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:51:57 +0200 Subject: [PATCH 16/17] ieee80211: add: complete explicit immediate Compressed Block Ack Before this commit, HCF still rejects the Compressed profile. Wire support, explicit receipt state, and rate helpers exist, but production exchanges do not use them. After this commit, explicit Compressed profiles complete immediate exchanges on eligible HT links. Receipt bits survive delivery, absent receipts remain zero, and shifted responses apply only within the active window. Actual physical endpoints establish SIFS or permitted RIFS relations. Active Compressed agreements suppress fragmentation. HCF uses the prepared non-HT BAR rates and 32-byte response airtime. Both endpoints must select the same profile because ADDBA does not negotiate it. Conflicting addressed BAR stops the simulation before agreement updates. Delayed Compressed policy fails initialization. Basic remains the default. Active Compressed DATA checks complete exchange cost before destructive aggregation. Refusal preserves queue frames, deadlines, and sequence state. An oversized aggregate can fall back to its first ordinary MSDU. One selected mode survives RTS/CTS. Failed RTS updates category recovery without a provisional sequence counter. Original lifetimes bound repeated attempts. For example, an aggregate that exceeds the exchange limit leaves its sources intact. HCF can instead admit its first ordinary MSDU with the same selected mode. Commitment records one queue departure and retains its queueing time through retransmission. Direct exchange and admission fixtures check refusal, fallback, retry, and response behavior. Agreement activation, physical interval use, DATA admission, and frame sequences change together to expose a complete supported exchange. Shared queue, timing, permission, wire, preparation, receipt, and rate prerequisites precede activation. Single protection reserves only the current exchange. The following commit changes the default separately. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.add | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 30 +- doc/src/migration-guide/index.rst | 28 +- doc/src/users-guide/ch-80211.rst | 43 +- showcases/wireless/blockack/doc/index.rst | 2 +- .../OriginatorBlockAckAgreementHandler.cc | 7 + .../OriginatorBlockAckAgreementPolicy.cc | 2 + .../RecipientBlockAckAgreementPolicy.cc | 2 + .../blockack/RecipientBlockAckProcedure.cc | 8 + .../blockackreordering/BlockAckReordering.cc | 23 +- .../blockackreordering/BlockAckReordering.h | 2 +- .../mac/contract/IRecipientQosAckPolicy.h | 2 + .../contract/IRecipientQosMacDataService.h | 3 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 532 +++++++++++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 38 +- .../mac/coordinationfunction/Hcf.ned | 2 +- .../mac/framesequence/FrameSequenceContext.cc | 20 + .../mac/framesequence/FrameSequenceContext.h | 37 +- .../ieee80211/mac/framesequence/HcfFs.cc | 10 +- .../framesequence/PrimitiveFrameSequences.cc | 43 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 4 +- .../originator/OriginatorQosMacDataService.cc | 2 +- .../originator/OriginatorQosMacDataService.h | 2 + .../ieee80211/mac/originator/QosAckHandler.cc | 27 + .../ieee80211/mac/originator/QosAckHandler.h | 2 + .../SingleProtectionMechanism.cc | 5 +- .../ieee80211/mac/queue/InProgressFrames.cc | 10 + .../ieee80211/mac/queue/InProgressFrames.h | 2 + .../Ieee80211PeerModeSelection.cc | 11 + .../Ieee80211PeerModeSelection.h | 4 + .../mac/recipient/RecipientQosAckPolicy.cc | 9 +- .../mac/recipient/RecipientQosAckPolicy.h | 1 + .../recipient/RecipientQosMacDataService.cc | 20 +- .../recipient/RecipientQosMacDataService.h | 2 +- .../module/Ieee80211BlockAckReordering_1.test | 37 +- .../module/Ieee80211BlockAckScheduling_1.test | 176 ++++- tests/module/Ieee80211BlockAckVariant_1.test | 24 +- ...eee80211CompressedBlockAckAdmission_1.test | 665 ++++++++++++++++++ ...Ieee80211CompressedBlockAckExchange_1.test | 428 +++++++++++ .../Ieee80211CompressedBlockAckInput_1.test | 325 +++++++++ ...ee80211CompressedBlockAckInterframe_1.test | 542 ++++++++++++++ ...ee80211CompressedBlockAckOriginator_1.test | 175 +++++ .../Ieee80211HcfMultipleProtection_2.test | 121 +++- tests/module/Ieee80211RecipientNav_1.test | 97 ++- tests/protocol/wifi/11ac/Ac_BlockAck.test | 55 +- .../wifi/11ac/Ac_BlockAckFallback.test | 186 +++++ tests/protocol/wifi/11n/N_BlockAck.test | 55 +- tests/protocol/wifi/11n/N_CompressedBa.test | 22 +- tests/unit/Ieee80211BlockAckScheduling_1.test | 42 ++ 48 files changed, 3686 insertions(+), 199 deletions(-) create mode 100644 tests/module/Ieee80211CompressedBlockAckAdmission_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckExchange_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckInput_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckInterframe_1.test create mode 100644 tests/module/Ieee80211CompressedBlockAckOriginator_1.test create mode 100644 tests/protocol/wifi/11ac/Ac_BlockAckFallback.test diff --git a/WHATSNEW b/WHATSNEW index c8a291bd77e..a0646fd959e 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -442,7 +442,7 @@ Notable backward incompatible changes are the following: The recipient delivers complete buffered units in cyclic order across receive gaps before resource release. Association reset instead discards old buffered units in both directions. With Block Ack enabled, reset also discards queued and in-progress QoS DATA for the old peer, including unsent Normal Ack DATA. These changes can alter delivery, disposal, frame exchanges, and fingerprints. - Basic remains the default. Compressed selection still stops initialization. Custom agreement handlers, recipient services, and management implementations require the changes in the migration guide. 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. + 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 @@ -454,6 +454,24 @@ Notable backward incompatible changes are the following: 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 supports explicit immediate Compressed Block Ack. Basic remains the default. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. + + Both endpoints must select the same blockAckVariant because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Compressed rejects delayedAckPolicySupported = true in either agreement policy at initialization. + + Compressed receipt state retains positive bits after delivery and reports zero for absent receipts. HCF recognizes the required SIFS or RIFS relation from actual physical endpoints. It can release individual MPDUs under that relation despite a receive gap. Compressed agreements suppress fragmentation for their peer and TID; Basic retains its fragment bitmap. + + Compressed BAR uses a legal non-HT rate. Initial and later BAR consume their applicable BSS and peer rate sets. A configured controlFrameBitrate must satisfy the applicable rule. Response duration, protection, and timeout calculations use the 32-byte response length. Basic retains its separate 152-byte response. + + Active Compressed agreements check complete DATA exchange cost before destructive aggregation. If an aggregate cannot qualify, HCF evaluates its first ordinary MSDU with the same selected mode. Refusal preserves original queue frames, deadlines, and sequence state. Single protection reserves only the current exchange; a later BAR can require fresh contention. These changes can alter airtime, contention, delivery times, and simulation results. Basic and traffic outside active Compressed agreements retain their existing admission limits. + + Before DATA commitment, failed RTS updates access-category retry state without a provisional sequence counter. Original DATA lifetimes bound repeated attempts. This changes the scoped path's former discard after a fixed number of RTS failures. + + Existing demonstrations retain explicit Basic settings, configurations, and result rows. IEEE Std 802.11e-2005 introduced Basic; IEEE Std 802.11-2024 reserves its Type 0 value. Retention does not establish complete historical conformance. A-MPDU requirements for n and ac remain deferred. Multi-TID Block Ack and the existing delayed Basic requirement also remain deferred. Compressed support does not establish complete VHT transfer support or MAC stop/start recovery. + + Custom QoS rate selectors, recipient ACK policies, recipient data services, and frame sequences require the interface changes in the migration guide. Custom radio media, communication caches, and IEEE 802.11 modes also require the new timing queries. Basic classes and methods remain available. + 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. @@ -476,14 +494,6 @@ Notable backward incompatible changes are the following: 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. Compressed receipt helpers - - Compressed receipt helpers now retain positive receipt state independently of delivery. HCF still refuses the Compressed profile until complete exchange support exists. - -59. Compressed control-rate helpers - - QoS rate selection now supplies non-HT Compressed BAR and response modes. HCF still refuses the Compressed profile until complete exchange support exists. - Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics @@ -560,7 +570,7 @@ 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 diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 536ee36a263..49984769ef5 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -41,12 +41,30 @@ Compressed producers must supply Fragment Number zero and a 64-bit bitmap. A Com IEEE 802.11 reversible DATA preparation ------------------------------------- -Custom QoS data services must implement the reversible preparation operations in ``IOriginatorQosMacDataService``. ``prepareFrame()`` creates an owned preview and retains original MSDUs in their queue. ``isPreparationValid()`` checks current source identity, eligibility, lifetime, sequence state, and aggregation permission. ``commitPreparedFrame()`` transfers the same preview pointer only after successful validation. Failure must preserve the preview and all original queue frames. +Custom QoS data services must implement the reversible preparation operations in ``IOriginatorQosMacDataService``. ``prepareFrame()`` creates an owned preview and retains original MSDUs in their queue. ``isPreparationValid()`` checks current source identity, eligibility, lifetime, sequence state, and aggregation permission. ``commitPreparedFrame()`` transfers the same preview pointer only after successful validation. Failure must preserve the preview and all original queue frames. ``InProgressFrames::adoptPreparedFrame()`` accepts a committed frame without another extraction. -IEEE 802.11 Compressed control rates ------------------------------------- +IEEE 802.11 Compressed Block Ack extension points +------------------------------------------------ -Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. This operation prepares legal non-HT BAR rate selection from the supplied TXOP position and applicable BSS or peer rates. HCF retains its Compressed-profile rejection in this prerequisite. Runtime activation will supply the request's actual TXOP position. Existing Basic methods retain their contracts. +Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. + +Compressed agreements require usable local HT support and directional peer HT state. An HT PHY mode alone does not supply that state. Without it, HCF retains Normal Ack DATA. Explicit ``blockAckVariant = "basic"`` retains historical Basic compatibility without that HT requirement. Compressed agreements suppress fragmentation for their peer and TID. Fragmentation outside those agreements retains its existing threshold. + +Compressed supports immediate Block Ack only. Keep ``delayedAckPolicySupported = false`` in both agreement policies. HCF rejects either true value during initialization when Compressed support is enabled. Basic retains its existing delayed-policy configuration contract. + +Custom QoS rate selectors must implement ``computeCompressedBlockAckReqMode(packet, header, startsTxop)``. HCF supplies the request's actual position in the TXOP. Select a legal non-HT mode from the applicable BSS or peer rates. A configured ``controlFrameBitrate`` must satisfy that rule. The existing Basic rate-selection methods remain available. + +Custom recipient ACK policies must implement ``computeCompressedBlockAckDurationField()``. This operation subtracts SIFS and the actual Compressed response airtime from the request reservation. Compressed Block Ack contains 32 bytes with FCS; Basic contains 152 bytes. Retain the existing Basic operation. + +Custom recipient data services must accept ``bool interframe = false`` in ``dataFrameReceived()``. The value states whether HCF established the required SIFS or RIFS relation. A false value supplies no interval evidence. Preserve that value through the recipient reorder path. Custom callers that omit it retain false. + +Custom frame sequences must supply HCF's resolved profile to ``QoSContext``. Its constructor retains Basic for callers that omit the profile. Custom agreement creation must set the recipient agreement's variant before use. An agreement must retain that variant throughout its lifetime. + +Custom frame sequences must preserve BAR arbitration before DATA preparation. The HCF preparation callback distinguishes the legacy path, accepted preparation, and admission refusal. Refusal must end the sequence without a DATA failure. Keep the selected DATA mode through RTS/CTS. Validate that decision without another adaptive rate query. Admit the complete exchange before source removal. + +The active Compressed path reserves only the current exchange under single protection. A later BAR can require fresh contention. Before DATA commitment, failed RTS updates QSRC and the contention window without a provisional sequence counter. QSRC is the short retry counter for an access category. QSRC rollover does not discard queued DATA. Original lifetimes bound repeated RTS attempts. + +Basic and outside-agreement fallback retain their existing preparation and recovery paths. IEEE 802.11 Basic Block Ack extension points ------------------------------------------ @@ -210,7 +228,7 @@ No configuration change is necessary. Gap release, window movement, and BAR befo IEEE 802.11 Block Ack Agreement Lifecycle ------------------------------------------ -HCF now defers new DATA extraction for a peer and TID during Block Ack setup or recovery. Already numbered DATA resolves before HCF creates ADDBA. The first DATA under an accepted agreement therefore retains the request sequence number. Basic remains the default. Compressed selection still stops initialization. +HCF now defers new DATA extraction for a peer and TID during Block Ack setup or recovery. Already numbered DATA resolves before HCF creates ADDBA. The first DATA under an accepted agreement therefore retains the request sequence number. Both supported profiles use this lifecycle contract. The recipient response now determines the buffer size and inactivity timeout. The built-in recipient selects the smaller of 64 and its configured maximum. A request size of zero supplies no size guidance. The response copies the request dialog token. Both negotiated A-MSDU flags must permit aggregation under an active agreement. diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index b526cdbf248..e6f09b57bd6 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -533,7 +533,7 @@ The first exchange proceeds even when its cost exceeds a positive TXOP limit. Th A zero limit permits the initial exchange and its existing RTS/CTS and immediate response. Each later fragment or unrelated unit needs a fresh TXOP. Prepared fragments and A-MSDUs use the same complete-cost check under positive limits. -``isBlockAckSupported = true`` disables complete-cost continuation admission, even without a Block Ack agreement. Multiple protection also retains its separate admission policy. Both paths use shared TXNAV. Basic Block Ack admission, BAR forecasts, zero-limit companion rules, and optional overrun exceptions remain outside this repair. +With Block Ack support enabled, active Compressed agreements use the admission described in :ref:`ug:sec:80211:blockack`. Basic and traffic outside those agreements retain their existing admission policy. Those retained paths can exceed DATA exchange bounds. All paths use shared TXNAV. Basic Block Ack admission and BAR forecasts remain outside the Compressed correction. All single protection paths classify group frames before the current-frame ACK equation. With Block Ack support disabled, the current one-frame group sequence advertises zero. With support enabled, its legacy forecast can reserve a queued successor that the sequence cannot transmit. This limit applies to group management and non-QoS DATA too. @@ -607,13 +607,9 @@ The standard separately requires AIFSN of at least 1 for APs and at least 2 for example, a non-AP station with AIFSN 1 meets the ownership condition but violates the standard's minimum. -Multiple protection does not check a complete exchange against the remaining budget. -For example, its admission policy can start a 120 us exchange with only -100 us left. The reference scope assumes whole-microsecond positive limits, integral initial PPDU -airtime, and integral legacy RTS airtime. It adds no A-MPDU, Compressed Block Ack, new -fragmentation, CTS-to-self, dual CTS, PIFS recovery, or TXOP sharing. It also adds no reverse -direction, beamforming, S1G, multi-user, or general MAC lifecycle support. It does not repair -general rate selection, recipient response timing, or TXOP overruns. +Outside active Compressed agreements, multiple protection does not check complete exchange cost against the remaining budget. For example, that retained policy can start a 120 us exchange with only 100 us left. Active Compressed agreements use their separate complete-cost admission. The reference scope assumes whole-microsecond positive limits, integral initial PPDU airtime, and integral legacy RTS airtime. + +Multiple protection adds no A-MPDU, new fragmentation, CTS-to-self, dual CTS, PIFS recovery, or TXOP sharing. Reverse direction, beamforming, S1G, multi-user operation, and general MAC lifecycle support remain outside this implementation. General rate selection and recipient response timing retain their separate limits. .. _ug:sec:80211:fragmentation: @@ -708,12 +704,27 @@ BAR can obtain channel access without a queued DATA frame. A later BAR within a After the final exchange, the originator sends DELBA when its agreement has no queued DATA, in-progress DATA, or active BAR exchange. A zero inactivity timeout does not disable this transfer-completion rule. DATA that arrives after this boundary waits for DELBA completion. A later transfer can require another ADDBA exchange. If the last unresolved DATA expires, the originator uses the same teardown path. Ordinary recipient teardown releases complete units behind a receive gap before it releases agreement resources. -The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and -``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size -(:par:`maximumAllowedBufferSize`, default 64), the A-MSDU support -(:par:`aMsduSupported`, default ``true``) and the inactivity timeout -(:par:`blockAckTimeoutValue`, default ``0s``, which means no timeout). Only -the basic Block Ack variant is modeled. +The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size. ``maximumAllowedBufferSize`` defaults to 64. Both negotiated ``aMsduSupported`` flags control A-MSDU use; each defaults to ``true``. ``blockAckTimeoutValue`` defaults to ``0s``, which disables the inactivity timeout. Transfer completion and DATA lifetime expiry retain their separate teardown rules. + +Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. + +Compressed agreements require usable local HT support and directional peer HT state. Infrastructure association supplies that peer state for eligible HT links and the supported ``ac`` path. An HT radio mode alone does not supply peer capabilities. Without usable state, DATA uses Normal Ack. Local non-HT operation produces a warning when Block Ack support is enabled. HT ad hoc links without peer state retain Normal Ack. + +Explicit ``blockAckVariant = "basic"`` selects historical Basic compatibility on both endpoints. Basic retains its 64 fragment bitmap entries and its 152-byte response with FCS. IEEE Std 802.11e-2005 introduced this format. IEEE Std 802.11-2024 reserves its Type 0 value. INET retains Basic for existing studies; this compatibility does not establish complete historical conformance. + +Both endpoints must select the same profile because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Configure matching profiles before a new run. Malformed frames follow input rejection instead of this configuration diagnostic. + +Compressed agreements suppress fragmentation for their peer and TID. The recipient rejects fragmented DATA under those agreements before receipt or reorder updates. Basic retains fragmentation. Outside a Compressed agreement, the configured fragmentation threshold retains its existing effect. + +Keep ``delayedAckPolicySupported = false`` in both agreement policies for Compressed support. HCF rejects true at initialization because the Compressed path supplies immediate responses only. Retained Basic delayed-policy configuration does not establish a complete delayed exchange implementation. + +The Compressed originator sends BAR with a non-HT PHY mode. Initial BAR uses the applicable BSS basic or mandatory rate rule. Later BAR uses the peer operational rate rule. A configured ``controlFrameBitrate`` must satisfy that rule. The response selector uses the actual 32-byte response length when a legal alternate rate is available. These rules retain separate Basic, ACK, and CTS contracts. + +Active Compressed agreements check the complete DATA exchange before source commitment. If an aggregate cannot qualify, HCF evaluates its first ordinary MSDU with the same selected DATA mode. If neither form qualifies, HCF ends the sequence without queue removal, sequence assignment, or deadline replacement. RTS/CTS retains the selected mode. Expiry or capability changes before DATA cancel invalid preparation. + +An initial unaggregated MSDU can use the applicable TXOP overrun exception. A same-size single-MPDU retransmission can also use its exception. That retransmission can carry an A-MSDU. Each exception permits at most one Data or Management frame in that TXOP. A new A-MSDU must fit a positive limit. A zero limit permits one DATA PPDU with one MSDU or A-MSDU, plus its required protection and response. + +Single protection reserves only the current Compressed exchange. A later BAR can require fresh contention. This policy can change airtime and delivery times. Before DATA commitment, failed RTS updates QSRC and the contention window without a provisional sequence counter. QSRC is the short retry counter for an access category. Original MSDU lifetimes bound repeated attempts; QSRC rollover does not discard them. .. code-block:: ini @@ -1030,9 +1041,9 @@ The model does not contain: - PCF and HCCA, the polled channel access methods. The module :ned:`Hcca` exists as a placeholder. -- A-MPDU aggregation, the MSDU lifetime limit, and self-CTS protection. +- A-MPDU aggregation and self-CTS protection. -- Compressed and multi-TID block acknowledgement. +- Multi-TID block acknowledgement and delayed Compressed Block Ack. - Operational HT widths above 20 MHz, and a primary/secondary channel relationship for wider VHT modes. diff --git a/showcases/wireless/blockack/doc/index.rst b/showcases/wireless/blockack/doc/index.rst index 1418ec4a985..2d6aa48f04e 100644 --- a/showcases/wireless/blockack/doc/index.rst +++ b/showcases/wireless/blockack/doc/index.rst @@ -50,7 +50,7 @@ Block Acknowledgment in INET In INET, the block ack mechanism can be enabled by setting the :par:`isBlockAckSupported` parameter of the MAC's :ned:`Hcf` submodule to ``true`` (it is disabled by default). -Select ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default during preparation for Compressed support. This stage rejects ``"compressed"`` because its runtime exchange path does not yet exist. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. +This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default. Other configurations can select Compressed explicitly. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Several submodules of :ned:`Hcf` are involved in the block ack mechanism. These modules have parameters controlling the block ack behavior. The following paragraphs diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index e136518a229..c526b7fe88c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -174,6 +174,13 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { + auto agreement = getAgreement(compressed->getTransmitterAddress(), compressed->getTidInfo()); + if (agreement) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } + } else throw cRuntimeError("Unsupported BlockAck"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 835216df923..1a03dd26ef2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -41,6 +41,8 @@ simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const bool OriginatorBlockAckAgreementPolicy::isProfileAllowed(BlockAckVariant variant, bool localHt, bool peerHt) const { + if (variant == BlockAckVariant::COMPRESSED && (!localHt || !peerHt)) + EV_DEBUG << "Compressed Block Ack setup refused: " << (!localHt ? "local HT state is absent" : "peer HT state is absent") << "; DATA uses Normal Ack.\n"; return variant == BlockAckVariant::BASIC || (localHt && peerHt); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index f2bd97fa697..33360fcfc32 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -39,6 +39,8 @@ bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const PtrtransmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, basicBlockAckReq); } } + else if (auto compressed = dynamicPtrCast(blockAckReq)) { + auto agreement = blockAckAgreementHandler->getAgreement(compressed->getTidInfo(), compressed->getTransmitterAddress()); + if (ackPolicy->isBlockAckNeeded(compressed, agreement)) { + auto blockAck = buildBlockAck(compressed, agreement); + blockAck->setDurationField(ackPolicy->computeCompressedBlockAckDurationField(blockAckPacketReq, compressed)); + callback->transmitControlResponseFrame(new Packet("CompressedBlockAck", blockAck), blockAck, blockAckPacketReq, compressed); + } + } else throw cRuntimeError("Unsupported BlockAckReq"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 02a811e4732..78f06a2d725 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -50,11 +50,12 @@ void BlockAckReordering::clearPeer(MacAddress peer) } } -BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe) { ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); auto sequenceNumber = dataHeader->getSequenceNumber(); auto windowStart = receiveBuffer->getNextExpectedSequenceNumber(); + bool insideWindow = windowStart <= sequenceNumber && sequenceNumber < windowStart + receiveBuffer->getBufferSize(); ReleasedFrames releasedFrames; // IEEE Std 802.11-2024, 10.25.6.6.2.1(b,c): advance for newer DATA beyond the window; discard older DATA. // The shared reorder service also retains Basic fragment reassembly as an INET compatibility choice. @@ -69,8 +70,15 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedQoSFrame(R // This allows the recipient to delete the Block Ack if the originator does not switch // back to using Block Ack. if (receiveBuffer->insertFrame(dataPacket, dataHeader)) { - if (dataHeader->getAckPolicy() == BLOCK_ACK) + if (dataHeader->getAckPolicy() == BLOCK_ACK && agreement->getVariant() == BlockAckVariant::BASIC) agreement->blockAckPolicyFrameReceived(dataHeader); + auto record = agreement->getBlockAckRecord(); + // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2)(v) and 10.25.6.6.1: release the complete prefix in order. + if (agreement->getVariant() == BlockAckVariant::COMPRESSED && insideWindow && windowStart < sequenceNumber && + interframe && record->hasCompleteReceipt(record->getWindowStart() - 1)) { + auto preceding = advanceReceiveBuffer(agreement, receiveBuffer, sequenceNumber); + releasedFrames.insert(releasedFrames.end(), preceding.begin(), preceding.end()); + } // IEEE Std 802.11-2024, 10.25.6.6.2.1(a)(2): release every consecutive complete unit at the window start. auto consecutiveFrames = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); releaseReceiveBuffer(agreement, receiveBuffer, consecutiveFrames); @@ -92,6 +100,14 @@ BlockAckReordering::ReleasedFrames BlockAckReordering::processReceivedBlockAckRe return ReleasedFrames(); startingSequenceNumber = basicReq->getStartingSequenceNumber(); } + else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { + if (compressedReq->getFragmentNumber() != 0 || compressedReq->getMultiTid() || !compressedReq->getCompressedBitmap() || + compressedReq->getTidInfo() < 0 || compressedReq->getTidInfo() > 15 || !compressedReq->getStartingSequenceNumber().isSpecified() || + compressedReq->getChunkLength() != LENGTH_BLOCKACKREQ - B(4)) + return ReleasedFrames(); + startingSequenceNumber = compressedReq->getStartingSequenceNumber(); + agreement->getBlockAckRecord()->advanceCompressedWindow(startingSequenceNumber); + } else { throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); } @@ -209,7 +225,8 @@ void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, Receive receiveBuffer->setNextExpectedSequenceNumber(sequenceNumber + 1); receiveBuffer->dropFramesUntil(sequenceNumber); receiveBuffer->removeFrame(sequenceNumber); - agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); + if (agreement->getVariant() == BlockAckVariant::BASIC) + agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); } std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 05a203ed44b..fdfbd29e226 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -54,7 +54,7 @@ class INET_API BlockAckReordering void clearPeer(MacAddress peer); void processReceivedDelba(const Ptr& delba); - ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); + ReleasedFrames processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader, bool interframe = false); ReleasedFrames processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h index ccf47cc5610..42aaecc9db8 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h @@ -26,6 +26,8 @@ class INET_API IRecipientQosAckPolicy virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const = 0; virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const = 0; + // The request selects the response variant and its actual airtime. + virtual simtime_t computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 7e97329e94d..7aded713794 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -26,7 +26,8 @@ class INET_API IRecipientQosMacDataService virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) = 0; virtual void clearPeer(MacAddress peer) = 0; - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; + // HCF supplies the observed SIFS or permitted RIFS relation for this frame. + virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index e968aabbee7..601eb39739e 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -18,12 +18,17 @@ #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/queueing/contract/ISelectivePacketCollection.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" namespace inet { namespace ieee80211 { @@ -47,8 +52,6 @@ void Hcf::initialize(int stage) blockAckVariant = BlockAckVariant::COMPRESSED; else throw cRuntimeError("Unknown blockAckVariant \"%s\": expected \"basic\" or \"compressed\"", configuredVariant); - if (blockAckVariant == BlockAckVariant::COMPRESSED) - throw cRuntimeError("Compressed Block Ack exchanges are not implemented"); mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); mib = check_and_cast(mac->getModuleByPath(mac->par("mibModule"))); startRxTimer = new cMessage("startRxTimeout"); @@ -65,6 +68,7 @@ void Hcf::initialize(int stage) rateSelection = check_and_cast(getSubmodule("rateSelection")); frameSequenceHandler = new FrameSequenceHandler(); WATCH_EXPR("frameSequenceInfo", getFrameSequenceInfo()); + WATCH_EXPR("numPendingInterframeReceptions", pendingReceptions.size()); originatorDataService = check_and_cast(getSubmodule(("originatorMacDataService"))); recipientDataService = check_and_cast(getSubmodule("recipientMacDataService")); originatorAckPolicy = check_and_cast(getSubmodule("originatorAckPolicy")); @@ -83,6 +87,23 @@ void Hcf::initialize(int stage) } } else if (stage == INITSTAGE_LAST) { + if (blockAckVariant == BlockAckVariant::COMPRESSED && par("isBlockAckSupported").boolValue()) { + interframeRadio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto radioModule = check_and_cast(interframeRadio.get()); + for (auto signal : {receptionStartedSignal, receptionEndedSignal, transmissionStartedSignal, transmissionEndedSignal, IRadio::receptionStateChangedSignal}) + radioModule->subscribe(signal, this); + } + if (par("isBlockAckSupported").boolValue()) { + EV_INFO << "Block Ack profile: " << (blockAckVariant == BlockAckVariant::BASIC ? "basic" : "compressed") << endl; + if (blockAckVariant == BlockAckVariant::COMPRESSED) { + if (originatorBlockAckAgreementPolicy && originatorBlockAckAgreementPolicy->isDelayedAckPolicySupported()) + throw cRuntimeError("Compressed Block Ack does not support delayedAckPolicySupported in %s.originatorBlockAckAgreementPolicy", getFullPath().c_str()); + if (recipientBlockAckAgreementPolicy && recipientBlockAckAgreementPolicy->delayedBlockAckPolicySupported()) + throw cRuntimeError("Compressed Block Ack does not support delayedAckPolicySupported in %s.recipientBlockAckAgreementPolicy", getFullPath().c_str()); + if (!mib->isHtOperationSupported()) + EV_WARN << "Compressed Block Ack has no usable local HT support; DATA uses Normal Ack. Set blockAckVariant = basic for Basic compatibility.\n"; + } + } // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. auto lifetimeHandler = edca->getLifetimeHandler(); @@ -92,6 +113,13 @@ void Hcf::initialize(int stage) check_and_cast(originatorDataService)->setEligibility( [this](Packet *packet) { return isFrameEligible(packet, false); }, [this](Packet *packet) { return isAMsduPermitted(packet); }); + check_and_cast(originatorDataService)->setFragmentationPermission([this](Packet *packet) { + auto data = dynamicPtrCast(packet->peekAtFront()); + if (blockAckVariant != BlockAckVariant::COMPRESSED || !data || data->getType() != ST_DATA_WITH_QOS || !originatorBlockAckAgreementHandler) + return true; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + return !agreement || !agreement->getIsAddbaResponseReceived(); + }); mib->subscribe(Ieee80211Mib::rateStateChangedSignal, this); for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { edca->getEdcaf(static_cast(ac))->setFrameEligibility( @@ -310,7 +338,59 @@ void Hcf::processUpperFrame(Packet *packet, const Ptr(interframeRadio.get()) && signalID == receptionStartedSignal) { + auto reception = check_and_cast(obj); + auto transmission = reception->getTransmission(); + std::set active; + for (auto ongoing : interframeRadio->getReceptionsInProgress()) + active.insert(ongoing->getId()); + for (auto it = pendingReceptions.begin(); it != pendingReceptions.end();) { + if (!it->second.completed && !active.count(it->first)) + it = pendingReceptions.erase(it); + else + ++it; + } + PendingReception pending; + auto medium = interframeRadio->getMedium(); + pending.timing = medium->getTransmissionTimingSnapshot(transmission->getId()); + auto wifi = dynamic_cast(transmission); + pending.ht = wifi && wifi->getMode()->getModulationClass() == IIeee80211Mode::ModulationClass::HT; + if (wifi) + pending.extension = wifi->getSignalExtensionDuration(); + auto gap = medium->computePrecedingSignalGap(transmission->getId(), interframeHistory.timing); + if (gap && (interframeHistory.mpdu || interframeHistory.ack)) { + // IEEE Std 802.11-2024, 10.3.2.3.3: this model uses coverage class zero. + pending.sifs = std::abs((*gap - modeSet->getSifsTime()).raw()) <= modeSet->getSlotTime().raw() / 10; + if (interframeHistory.mpdu && interframeHistory.ht && pending.ht && interframeHistory.noSifsResponse && + interframeHistory.timing.transmitterRadioId == pending.timing.transmitterRadioId && interframeHistory.extension) { + auto rifs = wifi->getMode()->getRifsTime(); + // IEEE Std 802.11-2024, 10.3.2.3.2 and 10.3.8: RIFS excludes the actual preceding extension. + pending.rifs = std::abs((*gap + *interframeHistory.extension - rifs).raw()) <= rifs.raw() / 10; + pending.precedingReceiver = interframeHistory.receiver; + } + } + interframeHistory = {}; + pendingReceptions.insert_or_assign(transmission->getId(), std::move(pending)); + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == receptionEndedSignal) { + auto reception = check_and_cast(obj); + auto pending = pendingReceptions.find(reception->getTransmission()->getId()); + if (pending != pendingReceptions.end()) + pending->second.completed = true; + } + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == transmissionStartedSignal) + clearInterframeHistory(); + else if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == transmissionEndedSignal) { + clearInterframeHistory(); + auto transmission = check_and_cast(obj); + if (completedTransmissionWasAck) { + interframeHistory.timing = interframeRadio->getMedium()->getTransmissionTimingSnapshot(transmission->getId()); + interframeHistory.ack = true; + } + completedTransmissionWasAck = false; + } + else if (signalID == InProgressFrames::packetDequeuedSignal) { Enter_Method("packetDequeued"); edca->getLifetimeHandler()->forgetFrame(check_and_cast(obj)->getId()); } @@ -329,6 +409,56 @@ void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, ModeSetListener::receiveSignal(source, signalID, obj, details); } +void Hcf::receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) +{ + Enter_Method_Silent(); + if (interframeRadio && source == dynamic_cast(interframeRadio.get()) && signalID == IRadio::receptionStateChangedSignal && + value != IRadio::RECEPTION_STATE_RECEIVING) { + bool incomplete = false; + for (auto it = pendingReceptions.begin(); it != pendingReceptions.end();) { + if (!it->second.completed) { + incomplete = true; + it = pendingReceptions.erase(it); + } + else + ++it; + } + if (incomplete || value == IRadio::RECEPTION_STATE_BUSY || value == IRadio::RECEPTION_STATE_UNDEFINED) + interframeHistory = {}; + } +} + +void Hcf::clearInterframeHistory() +{ + interframeHistory = {}; + pendingReceptions.clear(); +} + +bool Hcf::receiveInterframe(Packet *packet, const Ptr& header) +{ + auto time = packet->findTag(); + if (!time) + return false; + auto entry = pendingReceptions.find(time->getTransmissionId()); + if (entry == pendingReceptions.end() || !entry->second.completed) + return false; + auto pending = std::move(entry->second); + pendingReceptions.erase(entry); + // IEEE Std 802.11-2024, 10.27.3.1, 10.27.3.3, and 11.38.1: the BSS controls RIFS permission. + bool rifsPermitted = pending.rifs && modeSet->getPhyType() != Ieee80211ModeSet::PhyType::VHT && + (mib->mode == Ieee80211Mib::INDEPENDENT || mib->mode == Ieee80211Mib::MESH || mib->getHtOperation().rifsMode); + bool related = pending.sifs || (pending.rifs && rifsPermitted && pending.precedingReceiver == header->getReceiverAddress()); + interframeHistory.timing = std::move(pending.timing); + interframeHistory.mpdu = true; + interframeHistory.ht = pending.ht; + interframeHistory.receiver = header->getReceiverAddress(); + interframeHistory.extension = pending.extension; + auto dataOrMgmt = dynamicPtrCast(header); + interframeHistory.noSifsResponse = dataOrMgmt ? !recipientAckPolicy->isAckNeeded(dataOrMgmt) : + !dynamicPtrCast(header) && !dynamicPtrCast(header); + return related; +} + void Hcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -571,6 +701,14 @@ void Hcf::completeTransfers() void Hcf::discardAgreementData(MacAddress peer, Tid tid, bool all, int64_t highestTransmittedDataIndex) { + if (frameSequenceHandler->isSequenceRunning()) { + auto prepared = frameSequenceHandler->getContext()->getQoSContext()->preparedData.get(); + if (prepared) { + auto header = prepared->frame->peekAtFront(); + if (header->getReceiverAddress() == peer && header->getTid() == tid) + prepared->cancelled = true; + } + } for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { auto edcaf = edca->getEdcaf(static_cast(ac)); auto frames = edcaf->getInProgressFrames(); @@ -693,6 +831,8 @@ void Hcf::invalidateActions(MacAddress peer, Tid tid, bool originator) void Hcf::retireAgreement(MacAddress peer, Tid tid, bool originator, bool associationReset) { + if (!originator) + clearInterframeHistory(); if (originator) { auto agreement = originatorBlockAckAgreementHandler->detachAgreement(peer, tid); invalidateActions(peer, tid, originator); @@ -787,6 +927,7 @@ void Hcf::actionTerminated(Packet *packet, bool acknowledged) void Hcf::resetPeer(MacAddress peer) { + clearInterframeHistory(); Enter_Method("resetPeer"); if (!originatorBlockAckAgreementHandler) return; @@ -842,6 +983,16 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& { Enter_Method("processLowerFrame(%s)", packet->getName()); take(packet); + if (auto bar = dynamicPtrCast(header)) { + if (!validateBlockAckRequest(packet, bar)) { + interframeHistory = {}; + if (auto time = packet->findTag()) + pendingReceptions.erase(time->getTransmissionId()); + delete packet; + return; + } + } + bool interframe = interframeRadio && receiveInterframe(packet, header); EV_INFO << "Processing lower frame: " << packet->getName() << endl; auto edcaf = edca->getChannelOwner(); auto action = dynamicPtrCast(header); @@ -887,7 +1038,7 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& else if (hcca->isOwning()) throw cRuntimeError("Hcca is unimplemented!"); else if (isForUs(header)) - recipientProcessReceivedFrame(packet, header); + recipientProcessReceivedFrame(packet, header, interframe); else { EV_INFO << "This frame is not for us" << std::endl; PacketDropDetails details; @@ -897,6 +1048,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"); @@ -929,7 +1107,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); } @@ -954,7 +1133,30 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) edcaf->requestChannel(this); continue; } - Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); + auto frames = edcaf->getInProgressFrames(); + Packet *internallyCollidedFrame = nullptr; + bool scopedQueuedData = false; + if (blockAckVariant == BlockAckVariant::COMPRESSED && originatorBlockAckAgreementHandler) { + auto candidate = frames->peekFrameToTransmit(); + auto data = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + auto agreement = data ? originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()) : nullptr; + if (data && data->getType() == ST_DATA_WITH_QOS && !data->getReceiverAddress().isMulticast() && + agreement && agreement->getIsAddbaResponseReceived() && !frames->containsFrame(candidate)) { + scopedQueuedData = true; + // Collision recovery needs a numbered frame, but it must not form an unadmitted aggregate. + auto service = check_and_cast(originatorDataService); + auto prepared = service->prepareFrame(edcaf->getPendingQueue(), false); + if (prepared) { + auto selected = prepared->packet->peekAtFront(); + if (selected->getReceiverAddress() == data->getReceiverAddress() && selected->getTid() == data->getTid()) + internallyCollidedFrame = service->commitPreparedFrame(*prepared); + } + if (internallyCollidedFrame) + frames->adoptPreparedFrame(internallyCollidedFrame); + } + } + if (!internallyCollidedFrame && !scopedQueuedData) + internallyCollidedFrame = frames->getFrameToTransmit(); if (internallyCollidedFrame == nullptr) continue; auto internallyCollidedHeader = internallyCollidedFrame->peekAtFront(); @@ -1037,7 +1239,7 @@ void Hcf::frameSequenceFinished() finishingSequence = false; } -void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) +void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe) { EV_INFO << "Processing received frame " << packet->getName() << " as recipient.\n"; emit(packetReceivedFromPeerSignal, packet); @@ -1045,6 +1247,17 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { + auto agreement = data->getType() == ST_DATA_WITH_QOS ? + recipientBlockAckAgreementHandler->getAgreement(data->getTid(), data->getTransmitterAddress()) : nullptr; + if (agreement && (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified())) { + delete packet; + return; + } + if (agreement && blockAckVariant == BlockAckVariant::COMPRESSED && + (data->getFragmentNumber() != 0 || data->getMoreFragments())) { + delete packet; + return; + } if (data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == BLOCK_ACK && (data->getTid() < 0 || data->getTid() > 15 || !data->getSequenceNumber().isSpecified() || data->getTransmitterAddress().isUnspecified() || data->getTransmitterAddress().isMulticast())) { @@ -1061,16 +1274,17 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { - if (bar->getFragmentNumber() != 0 || bar->getMultiTid() || bar->getCompressedBitmap() || - bar->getTidInfo() < 0 || bar->getTidInfo() > 15 || !bar->getStartingSequenceNumber().isSpecified() || - bar->getTransmitterAddress().isUnspecified() || bar->getTransmitterAddress().isMulticast()) { + else if (auto bar = dynamicPtrCast(header)) { + if (!validateBlockAckRequest(packet, bar)) { delete packet; return; } - auto agreement = recipientBlockAckAgreementHandler->getAgreement(bar->getTidInfo(), bar->getTransmitterAddress()); + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = recipientBlockAckAgreementHandler->getAgreement(tid, bar->getTransmitterAddress()); if (!agreement) { - beginTeardown(bar->getTransmitterAddress(), bar->getTidInfo(), false, 38); + beginTeardown(bar->getTransmitterAddress(), tid, false, 38); delete packet; return; } @@ -1083,7 +1297,7 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { if (dataHeader->getType() == ST_DATA_WITH_QOS && recipientBlockAckAgreementHandler) recipientBlockAckAgreementHandler->qosFrameReceived(dataHeader, this); - sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); + sendUp(recipientDataService->dataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler, interframe)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { auto accepted = recipientDataService->managementFrameReceived(packet, mgmtHeader); @@ -1111,7 +1325,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); } @@ -1144,6 +1358,10 @@ bool Hcf::dispatchAgreementAction(const Ptr& header) } recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + if (!previous && agreement) + agreement->setVariant(blockAckVariant); + if (!previous && agreement) + clearInterframeHistory(); if (!previous && agreement) emit(blockAckAgreementAddedSignal, agreement); scheduleInactivityTimer(); @@ -1197,6 +1415,7 @@ bool Hcf::dispatchAgreementAction(const Ptr& header) void Hcf::transmissionComplete(Packet *packet, const Ptr& header) { Enter_Method("transmissionComplete"); + completedTransmissionWasAck = header->getType() == ST_ACK; auto edcaf = edca->getChannelOwner(); if (edcaf) { auto step = check_and_cast(frameSequenceHandler->getContext()->getLastStep()); @@ -1217,6 +1436,15 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) Enter_Method("originatorProcessRtsProtectionFailed"); auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + if (prepared && prepared->frame == packet && prepared->preparation) { + // IEEE Std 802.11-2024, 10.23.2.2 and 10.23.2.12.1: BA RTS failure updates QSRC/CW. + // The uncommitted sources retain their lifetimes. No provisional sequence owns an SRC entry. + if (validatePreparedData(*prepared, simTime())) + edcaf->getRecoveryProcedure()->controlFrameTransmissionFailed(); + return; + } if (obsoleteActions.count(packet->getId()) || !edcaf->getInProgressFrames()->containsFrame(packet)) return; if (edcaf) { @@ -1345,9 +1573,12 @@ void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& controlHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + auto basic = dynamicPtrCast(blockAckReq); + auto compressed = dynamicPtrCast(blockAckReq); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); auto agreement = originatorBlockAckAgreementHandler ? - originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), blockAckReq->getTidInfo()) : nullptr; + originatorBlockAckAgreementHandler->getAgreement(blockAckReq->getReceiverAddress(), tid) : nullptr; if (agreement) edcaf->getAckHandler()->processTransmittedBlockAckReq(blockAckReq, agreement); } @@ -1495,13 +1726,18 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { - if (!originatorBlockAckAgreementHandler || - !originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo())) + else if (auto blockAck = dynamicPtrCast(header)) { + auto basic = dynamicPtrCast(blockAck); + auto compressed = dynamicPtrCast(blockAck); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(blockAck->getTransmitterAddress(), tid) : nullptr; + if (!agreement) return; - EV_INFO << "BasicBlockAck has arrived" << std::endl; + EV_INFO << "BlockAck has arrived" << std::endl; edcaf->getRecoveryProcedure()->blockAckFrameReceived(); - auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); + auto ackedSeqAndFragNums = compressed ? edcaf->getAckHandler()->processReceivedCompressedBlockAck(compressed, agreement) : + edcaf->getAckHandler()->processReceivedBlockAck(blockAck); if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); EV_TRACE << "It has acknowledged the following frames:" << std::endl; @@ -1528,7 +1764,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"); @@ -1629,8 +1865,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); @@ -1645,6 +1882,180 @@ simtime_t Hcf::computeImmediateResponseDuration(Packet *packet, const PtrcomputeResponseAckFrameMode(packet, header, ResponseQueryRole::PREDICT)->getDuration(LENGTH_ACK) : SIMTIME_ZERO; } +bool Hcf::isInitialDataException(const FrameSequenceContext *context, const PreparedDataTransmission& prepared) const +{ + auto header = prepared.frame->peekAtFront(); + // IEEE Std 802.11-2024, 10.23.2.9: the same-size single-MPDU retry exception also permits an A-MSDU. + // Committed Compressed MPDUs retain their payload and length throughout retry; this path forms no A-MPDU. + bool committedRetry = !prepared.preparation && header->getRetry(); + if (!committedRetry && (header->getAMsduPresent() || header->getRetry())) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto step = dynamic_cast(context->getStep(i)); + if (step && step->getPpduEnd() >= SIMTIME_ZERO && + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront())) + return false; + } + return true; +} + +simtime_t Hcf::computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const +{ + auto sifs = modeSet->getSifsTime(); + auto cost = prepared.dataMode->getDuration(prepared.frame->getDataLength()); + if (prepared.ackMode) + cost += sifs + prepared.ackMode->getDuration(LENGTH_ACK); + if (protect) + cost += prepared.rtsMode->getDuration(prepared.rtsPrediction->getDataLength()) + + sifs + prepared.ctsMode->getDuration(LENGTH_CTS) + sifs; + return cost; +} + +bool Hcf::canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, + simtime_t ifs, bool protect) const +{ + auto txop = context->getQoSContext()->txopProcedure; + auto cost = computePreparedDataCost(prepared, protect); + int previousData = 0; + for (int i = 0; i < context->getNumSteps(); i++) { + auto step = dynamic_cast(context->getStep(i)); + if (step && step->getPpduEnd() >= SIMTIME_ZERO && + dynamicPtrCast(step->getFrameToTransmit()->peekAtFront())) + previousData++; + } + // IEEE Std 802.11-2024, 10.23.2.9: zero permits one SU DATA PPDU, including an A-MSDU. + if (txop->getLimit() == 0) { + if (previousData != 0) + return false; + } + else if (ifs + cost > txop->getRemaining() && !isInitialDataException(context, prepared)) + return false; + bool later = context->getNumSteps() > 0; + // The current TransmitStep is already present at the final submission check. + if (later) { + auto last = dynamic_cast(context->getLastStep()); + later = !last || last->getPpduEnd() >= SIMTIME_ZERO || context->getNumSteps() > 1; + } + // 10.23.2.8 samples TXNAV at exchange completion, before the leading IFS. + if (later && txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()) + return false; + if (later && protect && txop->getLimit() > 0 && txop->getProtectionMechanism() == TxopProcedure::MULTIPLE_PROTECTION && + (simTime() + ifs - txop->getStart() - txop->getLimit()).raw() % SimTime(1, SIMTIME_US).raw() != 0) + return false; + return true; +} + +bool Hcf::validatePreparedData(const PreparedDataTransmission& prepared, simtime_t start) const +{ + if (prepared.cancelled || prepared.modeSet != modeSet) + return false; + auto header = prepared.frame->peekAtFront(); + auto agreement = originatorBlockAckAgreementHandler ? + originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()) : nullptr; + if (agreement != prepared.agreement || !agreement || !agreement->getIsAddbaResponseReceived() || + !isProfileAllowed(header->getReceiverAddress(), true)) + return false; + auto service = check_and_cast(originatorDataService); + if (prepared.preparation) { + if (!service->isPreparationValid(*prepared.preparation, start)) + return false; + } + else if (!edca->getChannelOwner()->getInProgressFrames()->containsFrame(prepared.frame) || + !isFrameEligible(prepared.frame, true) || edca->getLifetimeHandler()->isExpired(prepared.frame, start)) + return false; + auto peer = mib->findPeerHtState(header->getReceiverAddress()); + if (!isPeerCompatibleMode(modeSet, peer, prepared.dataMode) || + (prepared.rtsMode && !isPeerCompatibleMode(modeSet, peer, prepared.rtsMode))) + return false; + if (originatorAckPolicy->computeAckPolicy(prepared.frame, header, agreement) != header->getAckPolicy()) + return false; + if (prepared.ackMode && (!modeSet->containsMode(prepared.ackMode) || + rateSelection->computeResponseAckFrameMode(prepared.frame, header, ResponseQueryRole::PREDICT) != prepared.ackMode)) + return false; + if (prepared.ctsMode && (!modeSet->containsMode(prepared.ctsMode) || + rateSelection->computeResponseCtsFrameMode(prepared.rtsPrediction.get(), + prepared.rtsPrediction->peekAtFront(), ResponseQueryRole::PREDICT) != prepared.ctsMode)) + return false; + return true; +} + +DataPreparationResult Hcf::prepareDataTransmission(FrameSequenceContext *context) +{ + Enter_Method("prepareDataTransmission"); + if (blockAckVariant != BlockAckVariant::COMPRESSED || !originatorBlockAckAgreementHandler) + return DataPreparationResult::LEGACY; + auto frames = context->getInProgressFrames(); + auto candidate = frames->peekFrameToTransmit(); + auto header = candidate ? dynamicPtrCast(candidate->peekAtFront()) : nullptr; + if (!header || header->getType() != ST_DATA_WITH_QOS || header->getReceiverAddress().isMulticast()) + return DataPreparationResult::LEGACY; + auto agreement = originatorBlockAckAgreementHandler->getAgreement(header->getReceiverAddress(), header->getTid()); + if (!agreement || !agreement->getIsAddbaResponseReceived()) + return DataPreparationResult::LEGACY; + auto prepared = std::make_unique(); + prepared->modeSet = modeSet; + prepared->agreement = agreement; + auto service = check_and_cast(originatorDataService); + if (frames->containsFrame(candidate)) + prepared->frame = candidate; + else { + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), true); + if (!prepared->preparation) + return DataPreparationResult::DEFERRED; + prepared->frame = prepared->preparation->packet.get(); + } + auto selected = prepared->frame->peekAtFront(); + if (selected->getReceiverAddress() != header->getReceiverAddress() || selected->getTid() != header->getTid()) + return DataPreparationResult::DEFERRED; + auto txop = context->getQoSContext()->txopProcedure; + prepared->dataMode = rateSelection->computeMode(prepared->frame, + prepared->frame->peekAtFront(), txop); + const IIeee80211Mode *selectedRtsMode = nullptr; + for (;;) { + auto packet = prepared->frame; + auto policy = originatorAckPolicy->computeAckPolicy(packet, packet->peekAtFront(), agreement); + auto updated = packet->removeAtFront(); + bool retry = false; + if (!prepared->preparation) { + auto status = edca->getChannelOwner()->getAckHandler()->getQoSDataAckStatus(updated); + retry = status == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED || + status == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED || status == QosAckHandler::Status::BLOCK_ACK_ARRIVED_UNACKED; + } + // Forwarded MSDUs can carry the preceding hop's Retry bit. Local ACK state owns this hop's retry decision. + updated->setRetry(retry); + updated->setAckPolicy(policy); + packet->insertAtFront(updated); + setFrameMode(packet, updated, prepared->dataMode); + prepared->ackMode = recipientAckPolicy->isAckNeeded(updated) ? + rateSelection->computeResponseAckFrameMode(packet, updated, ResponseQueryRole::PREDICT) : nullptr; + bool protect = rtsPolicy->isRtsNeeded(packet, updated); + prepared->setRtsPrediction(nullptr); + prepared->rtsMode = nullptr; + prepared->ctsMode = nullptr; + if (protect) { + prepared->setRtsPrediction(new Packet("prepared-RTS", rtsProcedure->buildRtsFrame(updated))); + prepared->rtsPrediction->insertAtBack(makeShared()); + auto rts = prepared->rtsPrediction->peekAtFront(); + if (!selectedRtsMode) + selectedRtsMode = rateSelection->computeMode(prepared->rtsPrediction.get(), rts, txop); + prepared->rtsMode = selectedRtsMode; + setFrameMode(prepared->rtsPrediction.get(), rts, selectedRtsMode); + prepared->ctsMode = rateSelection->computeResponseCtsFrameMode(prepared->rtsPrediction.get(), rts, ResponseQueryRole::PREDICT); + } + if (validatePreparedData(*prepared, simTime() + context->getIfs()) && + canAdmitPreparedData(context, *prepared, context->getIfs(), protect)) { + context->getQoSContext()->preparedData = std::move(prepared); + return DataPreparationResult::READY; + } + if (!prepared->preparation || !prepared->preparation->aggregate) + return DataPreparationResult::DEFERRED; + prepared->preparation = service->prepareFrame(edca->getChannelOwner()->getPendingQueue(), false); + if (!prepared->preparation) + return DataPreparationResult::DEFERRED; + prepared->frame = prepared->preparation->packet.get(); + } +} + simtime_t Hcf::forecastSingleProtectionSuccessor(Packet *packet, const Ptr& header, TxopProcedure *txop, InProgressFrames *frames, simtime_t completionTime) { @@ -1692,7 +2103,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) auto channelOwner = edca->getChannelOwner(); if (channelOwner) { auto header = packet->peekAtFront(); + auto context = frameSequenceHandler->getContext(); + auto prepared = context->getQoSContext()->preparedData.get(); + auto rtsStep = dynamic_cast(context->getLastStep()); + bool preparedStep = prepared && (prepared->frame == packet || + (rtsStep && rtsStep->getProtectedFrame() == prepared->frame)); + if (preparedStep && (!validatePreparedData(*prepared, simTime() + ifs) || + !canAdmitPreparedData(context, *prepared, ifs, rtsStep != nullptr))) + return false; if (dynamicPtrCast(header) && + !(preparedStep && prepared->preparation) && !channelOwner->getInProgressFrames()->containsFrame(packet)) return false; if (!isFrameEligible(packet, true)) @@ -1716,7 +2136,6 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) if (lifetimeHandler->isExpired(data, simTime() + ifs)) return false; auto txop = channelOwner->getTxopProcedure(); - auto context = frameSequenceHandler->getContext(); bool scopedSingle = txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && !par("isBlockAckSupported").boolValue(); Packet *retainedPacket = packet; auto retainedModeTag = scopedSingle ? retainedPacket->findTag() : nullptr; @@ -1729,14 +2148,21 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && receiveStep->getReceivedFrame()->peekAtFront()->getType() == ST_CTS; } - auto mode = dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); + auto compressedBar = dynamicPtrCast(header); + auto mode = preparedStep ? (rtsStep ? prepared->rtsMode : prepared->dataMode) : + compressedBar ? rateSelection->computeCompressedBlockAckReqMode(packet, compressedBar, context->getNumSteps() == 1) : + dataAfterCts ? packet->getTag()->getMode() : rateSelection->computeMode(packet, header, txop); setFrameMode(packet, header, mode); - if (auto bar = dynamicPtrCast(header)) { + if (auto bar = dynamicPtrCast(header)) { + auto basic = dynamicPtrCast(bar); + auto compressed = dynamicPtrCast(bar); + auto tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); auto agreement = originatorBlockAckAgreementHandler ? - originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), bar->getTidInfo()) : nullptr; + originatorBlockAckAgreementHandler->getAgreement(bar->getReceiverAddress(), tid) : nullptr; if (!agreement || !agreement->getIsAddbaResponseReceived()) return false; - auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + auto response = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT)->getDuration( + compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + response; // IEEE Std 802.11-2024, 10.23.2.8 and 10.23.2.9: a later BAR must fit both bounds. if (context->getNumSteps() > 1 && @@ -1744,7 +2170,33 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION && cost >= rx->getTxnavRemaining()))) return false; } - if (scopedSingle) { + if (preparedStep) { + auto response = rtsStep ? prepared->ctsMode->getDuration(LENGTH_CTS) : + prepared->ackMode ? prepared->ackMode->getDuration(LENGTH_ACK) : SIMTIME_ZERO; + simtime_t duration; + if (txop->getProtectionMechanism() == TxopProcedure::SINGLE_PROTECTION) { + // Reserve this exchange only. A later BAR obtains contention through the existing arbitration path. + duration = encodeDuration(singleProtectionMechanism->computeDurationField(header, response, + rtsStep ? computePreparedDataCost(*prepared, false) : SIMTIME_ZERO, true), true); + } + else { + auto ppduDuration = mode->getDuration(packet->getDataLength()); + auto projectedStart = simTime() + ifs; + auto reservation = std::max(SIMTIME_ZERO, txop->getReservationEnd() - projectedStart); + auto remaining = std::max(SIMTIME_ZERO, txop->getStart() + txop->getLimit() - projectedStart); + auto cost = computePreparedDataCost(*prepared, rtsStep != nullptr); + bool singleDataOverrun = txop->getLimit() > 0 && ifs + cost > txop->getRemaining() && + isInitialDataException(context, *prepared); + // An allowed single-DATA overrun protects its actual exchange beyond the nominal limit. + duration = singleDataOverrun ? computeMultipleProtectionDuration(0, 0, 0, ppduDuration, cost, false) : + computeMultipleProtectionDuration(txop->getLimit(), remaining, reservation, ppduDuration, + txop->getLimit() == 0 ? cost : SIMTIME_ZERO, txop->getReservationEnd() != 0); + } + auto updated = packet->removeAtFront(); + updated->setDurationField(duration); + packet->insertAtFront(updated); + } + else if (scopedSingle) { auto sifs = modeSet->getSifsTime(); auto cost = mode->getDuration(packet->getDataLength()); simtime_t responseDuration = SIMTIME_ZERO; @@ -1829,6 +2281,16 @@ bool Hcf::transmitFrame(Packet *packet, simtime_t ifs) } else throw cRuntimeError("Undefined protection mechanism"); + if (preparedStep && packet == prepared->frame && prepared->preparation) { + auto service = check_and_cast(originatorDataService); + auto committed = service->commitPreparedFrame(*prepared->preparation); + if (!committed) + return false; + ASSERT(committed == packet); + prepared->preparation.reset(); + channelOwner->getInProgressFrames()->adoptPreparedFrame(committed); + header = committed->peekAtFront(); + } RateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; tx->transmitFrame(packet, packet->peekAtFront(), ifs, false, this); @@ -1858,7 +2320,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); @@ -1957,6 +2419,7 @@ bool Hcf::isSentByUs(const Ptr& header) const void Hcf::corruptedFrameReceived() { Enter_Method("corruptedFrameReceived"); + clearInterframeHistory(); if (frameSequenceHandler->isSequenceRunning() && !startRxTimer->isScheduled()) { frameSequenceHandler->handleStartRxTimeout(); } @@ -1966,6 +2429,11 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + if (interframeRadio) { + auto radioModule = check_and_cast(interframeRadio.get()); + for (auto signal : {receptionStartedSignal, receptionEndedSignal, transmissionStartedSignal, transmissionEndedSignal, IRadio::receptionStateChangedSignal}) + radioModule->unsubscribe(signal, this); + } cancelAndDelete(addbaResponseTimer); cancelAndDelete(dataLifetimeTimer); cancelAndDelete(startRxTimer); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 03d7bb93a45..a6594025b0f 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,6 +8,8 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" @@ -37,6 +39,8 @@ #include "inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/CtsProcedure.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/TransmissionTimingSnapshot.h" namespace inet { namespace ieee80211 { @@ -51,6 +55,31 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: { private: BlockAckVariant blockAckVariant = BlockAckVariant::BASIC; + bool validateBlockAckRequest(Packet *packet, const Ptr& bar) const; + struct InterframeHistory { + physicallayer::TransmissionTimingSnapshot timing; + bool mpdu = false; + bool ack = false; + bool ht = false; + bool noSifsResponse = false; + MacAddress receiver; + std::optional extension; + }; + struct PendingReception { + bool completed = false; + bool sifs = false; + bool rifs = false; + MacAddress precedingReceiver; + physicallayer::TransmissionTimingSnapshot timing; + bool ht = false; + std::optional extension; + }; + opp_component_ptr interframeRadio; + InterframeHistory interframeHistory; + std::map pendingReceptions; + bool completedTransmissionWasAck = false; + void clearInterframeHistory(); + bool receiveInterframe(Packet *packet, const Ptr& header); public: static simsignal_t edcaCollisionDetectedSignal; @@ -130,6 +159,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void handleMessage(cMessage *msg) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, bool value, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -140,6 +170,12 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: FrameSequenceContext *buildContext(AccessCategory ac); static simtime_t encodeDuration(simtime_t duration, bool holder); simtime_t computeImmediateResponseDuration(Packet *packet, const Ptr& header); + DataPreparationResult prepareDataTransmission(FrameSequenceContext *context); + bool validatePreparedData(const PreparedDataTransmission& prepared, simtime_t start) const; + simtime_t computePreparedDataCost(const PreparedDataTransmission& prepared, bool protect) const; + bool canAdmitPreparedData(const FrameSequenceContext *context, const PreparedDataTransmission& prepared, + simtime_t ifs, bool protect) const; + bool isInitialDataException(const FrameSequenceContext *context, const PreparedDataTransmission& prepared) const; simtime_t forecastSingleProtectionSuccessor(Packet *packet, const Ptr& header, TxopProcedure *txop, InProgressFrames *frames, simtime_t completionTime); static simtime_t computeMultipleProtectionDuration(simtime_t limit, simtime_t remaining, @@ -173,7 +209,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: bool isProfileAllowed(MacAddress peer, bool originator) const; // Recipient - virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); + virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header, bool interframe = false); virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); virtual void recipientProcessReceivedManagementFrame(const Ptr& header); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 2036bbbe680..e38a6636622 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -29,7 +29,7 @@ module Hcf extends Module like IHcf { parameters: bool isBlockAckSupported = default(false); - string blockAckVariant = default("basic"); // "basic" selects historical compatibility. "compressed" is reserved until its exchange path exists. + string blockAckVariant = default("basic"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. string rxModule; string txModule; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc index d5a8ac4d2c3..f392d6fc3aa 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.cc @@ -12,6 +12,21 @@ namespace ieee80211 { using namespace inet::physicallayer; +PreparedDataTransmission::~PreparedDataTransmission() +{ + if (rtsPrediction) + drop(rtsPrediction.get()); +} + +void PreparedDataTransmission::setRtsPrediction(Packet *packet) +{ + if (rtsPrediction) + drop(rtsPrediction.get()); + rtsPrediction.reset(packet); + if (rtsPrediction) + take(rtsPrediction.get()); +} + FrameSequenceContext::FrameSequenceContext(MacAddress address, Ieee80211ModeSet *modeSet, InProgressFrames *inProgressFrames, IRtsProcedure *rtsProcedure, IRtsPolicy *rtsPolicy, NonQoSContext *nonQoSContext, QoSContext *qosContext) : address(address), modeSet(modeSet), @@ -31,6 +46,11 @@ void FrameSequenceContext::removeLastStep() delete step; } +Packet *FrameSequenceContext::getDataFrameToTransmit() const +{ + return qosContext && qosContext->preparedData ? qosContext->preparedData->frame : inProgressFrames->getFrameToTransmit(); +} + simtime_t FrameSequenceContext::getIfs() const { return getNumSteps() == 0 ? 0 : modeSet->getSifsTime(); // TODO pifs diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h index ba9a3695257..85fa551c5f4 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h @@ -8,13 +8,18 @@ #ifndef __INET_FRAMESEQUENCECONTEXT_H #define __INET_FRAMESEQUENCECONTEXT_H +#include +#include + #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/contract/BlockAckVariant.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" #include "inet/linklayer/ieee80211/mac/contract/IFrameSequence.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorAckPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/contract/IRtsPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/RtsProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" @@ -24,20 +29,46 @@ namespace inet { namespace ieee80211 { +class FrameSequenceContext; + +enum class DataPreparationResult { LEGACY, READY, DEFERRED }; + +/** Retains one actual mode decision until DATA submission or cancellation. */ +struct INET_API PreparedDataTransmission : public cObject +{ + std::unique_ptr preparation; + Packet *frame = nullptr; // Borrowed from preparation, or from InProgressFrames after commitment. + const physicallayer::Ieee80211ModeSet *modeSet = nullptr; + const physicallayer::IIeee80211Mode *dataMode = nullptr; + const physicallayer::IIeee80211Mode *rtsMode = nullptr; + const physicallayer::IIeee80211Mode *ackMode = nullptr; + const physicallayer::IIeee80211Mode *ctsMode = nullptr; + std::unique_ptr rtsPrediction; + OriginatorBlockAckAgreement *agreement = nullptr; // Identity only; validation obtains current state from its owner. + bool cancelled = false; + + ~PreparedDataTransmission() override; + void setRtsPrediction(Packet *packet); +}; + class INET_API QoSContext { public: - QoSContext(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler, TxopProcedure *txopProcedure) : + QoSContext(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler, TxopProcedure *txopProcedure, BlockAckVariant variant = BlockAckVariant::BASIC) : ackPolicy(ackPolicy), blockAckProcedure(blockAckProcedure), blockAckAgreementHandler(blockAckAgreementHandler), - txopProcedure(txopProcedure) + txopProcedure(txopProcedure), + blockAckVariant(variant) {} IOriginatorQoSAckPolicy *ackPolicy = nullptr; IOriginatorBlockAckProcedure *blockAckProcedure = nullptr; IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler = nullptr; TxopProcedure *txopProcedure = nullptr; + const BlockAckVariant blockAckVariant; + std::function prepareData; + std::unique_ptr preparedData; }; class INET_API NonQoSContext @@ -79,6 +110,8 @@ class INET_API FrameSequenceContext : public cObject virtual IFrameSequenceStep *getStepBeforeLast() const { return steps.size() > 1 ? steps[steps.size() - 2] : nullptr; } virtual InProgressFrames *getInProgressFrames() const { return inProgressFrames; } + // A reversible preview replaces destructive extraction only for the scoped QoS path. + Packet *getDataFrameToTransmit() const; virtual IRtsProcedure *getRtsProcedure() const { return rtsProcedure; } virtual IRtsPolicy *getRtsPolicy() const { return rtsPolicy; } diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index 7a49e0ece7c..7bcd7d3be5c 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -64,6 +64,14 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte auto frames = context->getInProgressFrames(); auto qos = context->getQoSContext(); bool bar = qos->ackPolicy->isBlockAckReqNeeded(frames, qos->txopProcedure, qos->blockAckAgreementHandler); + qos->preparedData.reset(); + if (!bar && qos->prepareData) { + auto preparation = qos->prepareData(context); + if (preparation == DataPreparationResult::READY) + return true; + if (preparation == DataPreparationResult::DEFERRED) + return false; + } bool hasFrameToTransmit = qos->blockAckAgreementHandler ? bar || frames->peekFrameToTransmit() != nullptr : frames->hasInProgressFrames(); if (hasFrameToTransmit) { @@ -101,7 +109,7 @@ bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *conte bool HcfFs::hasMoreTxOpsAndMulticast(RepeatingFs *frameSequence, FrameSequenceContext *context) { - return hasMoreTxOps(frameSequence, context) && context->getInProgressFrames()->getFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); + return hasMoreTxOps(frameSequence, context) && context->getDataFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index f9a3de4a6e5..66da226a753 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -40,7 +40,7 @@ IFrameSequenceStep *RtsFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto dataOrMgmtPacket = context->getInProgressFrames()->getFrameToTransmit(); + auto dataOrMgmtPacket = context->getDataFrameToTransmit(); auto rtsFrame = context->getRtsProcedure()->buildRtsFrame(dataOrMgmtPacket->peekAtFront()); auto rtsPacket = new Packet("RTS"); rtsPacket->insertAtBack(rtsFrame); @@ -119,7 +119,7 @@ IFrameSequenceStep *DataFs::prepareStep(FrameSequenceContext *context) // RTS/CTS retains its selected DATA even if its lifetime expires during protection. // HCF rejects the next submission before Tx accepts that expired DATA. if (packet == nullptr) - packet = context->getInProgressFrames()->getFrameToTransmit(); + packet = context->getDataFrameToTransmit(); return new TransmitStep(packet, context->getIfs()); } case 1: @@ -275,7 +275,7 @@ IFrameSequenceStep *RtsCtsFs::prepareStep(FrameSequenceContext *context) { switch (step) { case 0: { - auto packet = context->getInProgressFrames()->getFrameToTransmit(); + auto packet = context->getDataFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); auto rtsFrame = context->getRtsProcedure()->buildRtsFrame(dataOrMgmtHeader); auto rtsPacket = new Packet("RTS"); @@ -417,8 +417,10 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); - auto blockAckReq = context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); - auto blockAckPacket = new Packet("BasicBlockAckReq", blockAckReq); + bool compressed = qos->blockAckVariant == BlockAckVariant::COMPRESSED; + auto blockAckReq = compressed ? qos->blockAckProcedure->buildCompressedBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber) : + qos->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); + auto blockAckPacket = new Packet(compressed ? "CompressedBlockAckReq" : "BasicBlockAckReq", blockAckReq); blockAckPacket->insertAtBack(makeShared()); return new TransmitStep(blockAckPacket, context->getIfs(), true); } @@ -446,12 +448,33 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - auto response = dynamicPtrCast(receivedHeader); auto requestStep = check_and_cast(context->getStep(firstStep)); - auto request = requestStep->getFrameToTransmit()->peekAtFront(); - return context->isForUs(receivedHeader) && response && - response->getTransmitterAddress() == request->getReceiverAddress() && - response->getTidInfo() == request->getTidInfo(); + auto request = requestStep->getFrameToTransmit()->peekAtFront(); + if (auto basic = dynamicPtrCast(request)) { + auto response = dynamicPtrCast(receivedHeader); + bool valid = response && !response->getMultiTid() && !response->getCompressedBitmap() && + response->getFragmentNumber() == 0 && response->getStartingSequenceNumber().isSpecified() && + (response->getReserved() & 3) == 0 && response->getChunkLength() == LENGTH_BASIC_BLOCKACK - B(4) && + receivedPacket->getDataLength() == LENGTH_BASIC_BLOCKACK && response->getBlockAckBitmapArraySize() == 64; + if (valid) + for (int i = 0; i < 64; i++) + valid &= response->getBlockAckBitmap(i).getSize() == 16; + return valid && context->isForUs(receivedHeader) && + response->getReceiverAddress() == basic->getTransmitterAddress() && + response->getTransmitterAddress() == basic->getReceiverAddress() && + response->getTidInfo() == basic->getTidInfo(); + } + auto compressed = dynamicPtrCast(request); + auto response = dynamicPtrCast(receivedHeader); + // IEEE Std 802.11-2024, 9.3.1.8.2: this profile supports the 64-bit bitmap without dynamic fragmentation. + return context->isForUs(receivedHeader) && compressed && response && + response->getReceiverAddress() == compressed->getTransmitterAddress() && + response->getTransmitterAddress() == compressed->getReceiverAddress() && + response->getTidInfo() == compressed->getTidInfo() && + !response->getMultiTid() && response->getCompressedBitmap() && response->getFragmentNumber() == 0 && + response->getStartingSequenceNumber().isSpecified() && response->getBlockAckBitmap().getSize() == 64 && + response->getChunkLength() == LENGTH_COMPRESSED_BLOCKACK - B(4) && + receivedPacket->getDataLength() == LENGTH_COMPRESSED_BLOCKACK; } default: throw cRuntimeError("Unknown step"); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc index e0daefabaa7..d2986e6dffe 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -47,7 +47,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte auto qos = context->getQoSContext(); if (qos->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), qos->txopProcedure, qos->blockAckAgreementHandler)) return 2; - auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + auto frameToTransmit = context->getDataFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); if (dynamicPtrCast(macHeader)) return 3; @@ -71,7 +71,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte bool TxOpFs::isRtsCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *context) { - auto protectedFrame = context->getInProgressFrames()->getFrameToTransmit(); + auto protectedFrame = context->getDataFrameToTransmit(); return context->getRtsPolicy()->isRtsNeeded(protectedFrame, protectedFrame->peekAtFront()); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index 7986ca4f13c..c3612e1c969 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -309,7 +309,7 @@ std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queu // if (msduIntegrityAndProtection) // frame = protectMsduIfNeeded(frame); std::vector *fragments = nullptr; - if (fragmentationPolicy) + if (fragmentationPolicy && (!fragmentationPermitted || fragmentationPermitted(packet))) fragments = fragmentIfNeeded(packet); if (!fragments) fragments = new std::vector({ packet }); diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 73bda528989..cddf0af45be 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -43,6 +43,7 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService std::function expireFrames; std::function extractionEligible; std::function aMsduPermitted; + std::function fragmentationPermitted; int64_t nextDataAllocationIndex = 0; protected: @@ -54,6 +55,7 @@ class INET_API OriginatorQosMacDataService : public IOriginatorQosMacDataService virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); public: + void setFragmentationPermission(std::function permission) { fragmentationPermitted = permission; } void setEligibility(std::function eligible, std::function permission) { extractionEligible = eligible; aMsduPermitted = permission; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index c79877d4a72..e213b5665bb 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -109,6 +109,33 @@ void QosAckHandler::dropFrames(std::set>> QosAckHandler::processReceivedCompressedBlockAck( + const Ptr& blockAck, const OriginatorBlockAckAgreement *agreement) +{ + std::set acked; + if (!agreement) + return acked; + auto start = blockAck->getStartingSequenceNumber(); + const auto& bitmap = blockAck->getBlockAckBitmap(); + // IEEE Std 802.11-2024, 10.25.6.8: positive bits cover transmitted frames throughout the active window. + for (auto& entry : ackStatuses) { + if (entry.first.first != blockAck->getTransmitterAddress() || entry.first.second.first != blockAck->getTidInfo()) + continue; + auto sequence = SequenceNumberCyclic(entry.first.second.second.getSequenceNumber()); + auto& status = entry.second; + bool transmitted = status != Status::FRAME_NOT_YET_TRANSMITTED && status != Status::NO_ACK_REQUIRED; + int offset = OriginatorBlockAckAgreement::computeSequenceDistance(start, sequence); + if (transmitted && agreement->containsSequence(sequence) && entry.first.second.second.getFragmentNumber() == 0 && + offset < 64 && bitmap.getBit(offset)) { + status = Status::BLOCK_ACK_ARRIVED_ACKED; + acked.insert(entry.first); + } + else if (status == Status::WAITING_FOR_BLOCK_ACK) + status = Status::BLOCK_ACK_ARRIVED_UNACKED; + } + return acked; +} + std::set>> QosAckHandler::processReceivedBlockAck(const Ptr& blockAck) { MacAddress receiverAddr = blockAck->getTransmitterAddress(); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 96f1026d85f..69371236796 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -57,6 +57,8 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void processReceivedAck(const Ptr& ack, const Ptr& ackedHeader); virtual std::set>> processReceivedBlockAck(const Ptr& blockAck); + std::set>> processReceivedCompressedBlockAck( + const Ptr& blockAck, const OriginatorBlockAckAgreement *agreement); virtual void processFailedBlockAckReq(const Ptr& blockAckReq); virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc index fbd4606601a..e2f2b63c464 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc @@ -67,8 +67,9 @@ simtime_t SingleProtectionMechanism::computeCtsDurationField(const Ptr& blockAckReq) { // TODO ACK or BlockAck frame, as applicable - if (dynamicPtrCast(blockAckReq)) { - simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getDuration(LENGTH_BASIC_BLOCKACK); + if (dynamicPtrCast(blockAckReq) || dynamicPtrCast(blockAckReq)) { + simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::PREDICT)->getDuration( + dynamicPtrCast(blockAckReq) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); simtime_t blockAckReqDurationPerId = blockAckFrameDuration + modeSet->getSifsTime(); return blockAckReqDurationPerId; } diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index 416e9709ad3..190afe1d5c6 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -87,6 +87,16 @@ void InProgressFrames::ensureHasFrameToTransmit() } } +void InProgressFrames::adoptPreparedFrame(Packet *packet) +{ + Enter_Method("adoptPreparedFrame"); + take(packet); + ackHandler->frameGotInProgress(packet->peekAtFront()); + inProgressFrames.push_back(packet); + packet->setArrivalTime(simTime()); + emit(packetEnqueuedSignal, packet); +} + Packet *InProgressFrames::getFrameToTransmit() { ensureHasFrameToTransmit(); diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 65b13b67aa7..4804e562628 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -52,6 +52,8 @@ class INET_API InProgressFrames : public SimpleModule virtual int getLength() const { return inProgressFrames.size(); } virtual Packet *getFrames(int i) const { return inProgressFrames[i]; } bool containsFrame(const Packet *packet) const; + // Takes ownership after the data service commits a reversible preparation. + void adoptPreparedFrame(Packet *packet); virtual Packet *getFrameToTransmit(); virtual Packet *peekFrameToTransmit(); SequenceNumberCyclic getCandidateSequenceNumber(Packet *packet) const; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 6076a48172d..bd93bd3c52d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -131,6 +131,17 @@ static const IIeee80211Mode *selectCompatibleMode(const Ieee80211ModeSet *modeSe } // namespace +bool isPeerCompatibleMode(const Ieee80211ModeSet *modeSet, + const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode) +{ + if (!modeSet || !mode || !modeSet->containsMode(mode)) + return false; + if (mode->getHtMcsIndex() < 0) + return true; + return peerHtState && peerHtState->valid && + isCompatibleHtMode(mode, peerHtState->negotiatedCapabilities.localTxPeerRx, peerHtState->negotiatedCapabilities.operation); +} + const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, const Ieee80211Mib::PeerHtState *peerHtState, const IIeee80211Mode *mode, const MacAddress& peerAddress) { diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index 5197aa38891..e512fbbdb28 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -15,6 +15,10 @@ namespace inet { namespace ieee80211 { +/** Checks the retained transmit mode without a new rate-control decision. */ +INET_API bool isPeerCompatibleMode(const physicallayer::Ieee80211ModeSet *modeSet, + const Ieee80211Mib::PeerHtState *peerHtState, const physicallayer::IIeee80211Mode *mode); + /** * Selects a mode compatible with local transmission to the peer. * Non-HT modes remain unchanged. Absent peer state selects a legacy mode diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index cb9b112bd42..4712d009d0c 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -61,7 +61,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { - if (dynamicPtrCast(blockAckReq)) { + if (dynamicPtrCast(blockAckReq) || dynamicPtrCast(blockAckReq)) { return agreement != nullptr; // TODO The Basic BlockAckReq frame shall be discarded if all MSDUs referenced by this // frame have been discarded from the transmit buffer due to expiry of their lifetime limit. @@ -94,6 +94,13 @@ simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packe return basicBlockAckReq->getDurationField() - modeSet->getSifsTime() - computeBasicBlockAckDuration(packet, basicBlockAckReq); } +simtime_t RecipientQosAckPolicy::computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const +{ + // IEEE Std 802.11-2024, 9.2.5.6: subtract the actual response airtime and SIFS. + auto airtime = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq, ResponseQueryRole::RESPOND)->getDuration(LENGTH_COMPRESSED_BLOCKACK); + return std::max(SIMTIME_ZERO, blockAckReq->getDurationField() - modeSet->getSifsTime() - airtime); +} + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 9f4ce456784..0012ef77b55 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -34,6 +34,7 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const override; virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const override; + virtual simtime_t computeCompressedBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index e73a15720c1..e3f9ea2151d 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -53,14 +53,17 @@ Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) return nullptr; } -std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) +std::vector RecipientQosMacDataService::dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe) { Enter_Method("dataFrameReceived"); take(dataPacket); if (blockAckAgreementHandler) { auto agreement = blockAckAgreementHandler->getAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); - if (agreement) + if (agreement) { agreement->recordReceived(dataHeader); + if (agreement->getVariant() == BlockAckVariant::COMPRESSED) + agreement->getBlockAckRecord()->compressedFrameReceived(dataHeader); + } } // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { @@ -77,7 +80,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader, interframe); } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -137,14 +140,19 @@ std::vector RecipientQosMacDataService::managementFrameReceived(Packet std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReleasedFrames frames; if (blockAckReordering) { - Tid tid = blockAckReq->getTidInfo(); + auto basic = dynamicPtrCast(blockAckReq); + auto compressed = dynamicPtrCast(blockAckReq); + if (!basic && !compressed) + return {}; + Tid tid = basic ? basic->getTidInfo() : compressed->getTidInfo(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) + if (agreement) { frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); + } else return std::vector(); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 10b26ca843f..066359162b1 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -48,7 +48,7 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, public: virtual std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool associationReset) override; virtual void clearPeer(MacAddress peer) override; - virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; + virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, bool interframe = false) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; }; diff --git a/tests/module/Ieee80211BlockAckReordering_1.test b/tests/module/Ieee80211BlockAckReordering_1.test index 449a6041fde..3731b956b54 100644 --- a/tests/module/Ieee80211BlockAckReordering_1.test +++ b/tests/module/Ieee80211BlockAckReordering_1.test @@ -3,9 +3,10 @@ Check S1 recipient delivery through RecipientQosMacDataService. Supply accepted Basic agreements with known buffer sizes. Check fragment capacity, complete gap release, cyclic delivery, and BAR before DATA. Check window movement, incomplete-unit disposal, peer/TID isolation, and A-MSDU order. -The fixture supplies agreements; it does not test ADDBA negotiation or Compressed exchanges. +The fixture supplies agreements; it does not test ADDBA negotiation. +Compressed cases check gap release, cyclic delivery, peer/TID isolation, and receipt retention through the production recipient service. -%extraargs: -r 0..7 +%extraargs: -r 0..10 %file: RecipientReordering.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -40,8 +41,9 @@ Define_Module(ReorderingRecipient); class ReorderingAgreements : public RecipientBlockAckAgreementHandler { public: - void accept(MacAddress peer, Tid tid, int start, int size) { + void accept(MacAddress peer, Tid tid, int start, int size, BlockAckVariant variant = BlockAckVariant::BASIC) { auto agreement = new RecipientBlockAckAgreement(peer, tid, SequenceNumberCyclic(start), size, SIMTIME_ZERO); + agreement->setVariant(variant); agreement->addbaResposneSent(); blockAckAgreements[{peer, tid}] = agreement; } @@ -224,6 +226,33 @@ class RecipientReorderingTest : public cSimpleModule { checkDelivery(receive(makeData(101, "fragmented-frag1", 1)), {"fragmented", "third"}); checkWindow(103, 0, peer); break; + case 8: + case 9: { + int start = scenario == 8 ? 100 : 4095; + agreements.accept(peer, 5, start, 4, BlockAckVariant::COMPRESSED); + checkDelivery(receive(makeData(start, "first")), {"first"}); + checkDelivery(receive(makeData((start + 2) % 4096, "third")), {}); + checkDelivery(receive(makeData((start + 1) % 4096, "second")), {"second", "third"}); + checkWindow((start + 3) % 4096, 0, peer); + auto record = agreements.getAgreement(5, peer)->getBlockAckRecord(); + for (int offset = 0; offset < 4; offset++) + ASSERT(record->getCompressedAckState(SequenceNumberCyclic(start) + offset) == (offset < 3)); + break; + } + case 10: + agreements.accept(peer, 5, 100, 4, BlockAckVariant::COMPRESSED); + agreements.accept(otherPeer, 5, 100, 4, BlockAckVariant::COMPRESSED); + agreements.accept(peer, 6, 100, 4, BlockAckVariant::COMPRESSED); + checkDelivery(receive(makeData(101, "peer-one")), {}); + checkDelivery(receive(makeData(101, "peer-two", 0, false, otherPeer)), {}); + checkDelivery(receive(makeData(101, "tid-six", 0, false, peer, 6)), {}); + checkDelivery(receive(makeData(100, "first")), {"first", "peer-one"}); + checkWindow(102, 0, peer); + checkWindow(100, 1, otherPeer); + checkWindow(100, 1, peer, 6); + ASSERT(!agreements.getAgreement(5, otherPeer)->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + ASSERT(!agreements.getAgreement(6, peer)->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + break; default: throw cRuntimeError("Unknown recipient scenario"); } @@ -267,7 +296,7 @@ seed-set = 0 cmdenv-express-mode = true record-vector-results = false record-scalar-results = false -*.test.scenario = ${scenario=0..7} +*.test.scenario = ${scenario=0..10} %contains: stdout Recipient reordering checks passed. diff --git a/tests/module/Ieee80211BlockAckScheduling_1.test b/tests/module/Ieee80211BlockAckScheduling_1.test index 3fe472f6f68..ec072ea0dd2 100644 --- a/tests/module/Ieee80211BlockAckScheduling_1.test +++ b/tests/module/Ieee80211BlockAckScheduling_1.test @@ -1,11 +1,12 @@ %description: -Exercise Basic BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. +Exercise Basic and Compressed BAR selection and final DELBA through HCF, frame sequences, EDCA, and Tx. The fixture supplies accepted agreements and injects responses at the HCF receive boundary. +Compressed cases supply peer HT capabilities; the fixture does not test capability negotiation. Response events include SIFS and the selected response airtime. Cases cover short bursts, windows smaller than the threshold, two peers, BAR timeout, negative bitmap bits, Normal Ack window progress, and late BAR admission. -%extraargs: -r 0..29 +%extraargs: -r 0..75 %file: Scheduling.cc #include @@ -17,13 +18,16 @@ negative bitmap bits, Normal Ack window progress, and late BAR admission. #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class SchedulingRecipientAgreements : public RecipientBlockAckAgreementHandler { public: - void accept(MacAddress peer) { - auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + void accept(MacAddress peer, BlockAckVariant variant, int start = 0) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, SIMTIME_ZERO); + agreement->setVariant(variant); agreement->addbaResposneSent(); agreement->calculateExpirationTime(); blockAckAgreements[{peer, 5}] = agreement; @@ -33,19 +37,69 @@ class SchedulingRecipient : public Hcf { public: std::vector delivered; bool checkedBitmap = false; + bool orderedReceipt = false; + void publishPrimaryChannel() { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + } + void checkOrderedReceipt() { + Enter_Method("checkOrderedReceipt"); + delete recipientBlockAckAgreementHandler; + auto agreements = new SchedulingRecipientAgreements(); + recipientBlockAckAgreementHandler = agreements; + orderedReceipt = true; + publishPrimaryChannel(); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:01"), mib->localHtCapabilities, mib->getHtOperation()); + agreements->accept(MacAddress("02:00:00:00:00:01"), BlockAckVariant::COMPRESSED, 100); + for (int sequence : {100, 102, 101}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(data->getTransmitterAddress()); + data->setTid(5); + data->setAckPolicy(BLOCK_ACK); + data->setSequenceNumber(SequenceNumberCyclic(sequence)); + auto packet = new Packet("ordered", data); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getSlowestMode()); + processLowerFrame(packet, data); + ASSERT((delivered == (sequence == 101 ? std::vector{100, 101, 102} : std::vector{100}))); + } + auto bar = makeShared(); + bar->setTransmitterAddress(MacAddress("02:00:00:00:00:01")); + bar->setReceiverAddress(mac->getAddress()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto packet = new Packet("ordered-bar", bar); + packet->insertAtBack(makeShared()); + packet->addTag()->setMode(modeSet->getSlowestMode()); + processLowerFrame(packet, bar); + ASSERT(checkedBitmap); + } void prepare() { Enter_Method("prepare"); delete recipientBlockAckAgreementHandler; auto agreements = new SchedulingRecipientAgreements(); recipientBlockAckAgreementHandler = agreements; - agreements->accept(MacAddress("02:00:00:00:00:01")); + if (std::string(par("blockAckVariant").stringValue()) == "compressed") { + publishPrimaryChannel(); + mib->setPeerHtCapabilities(MacAddress("02:00:00:00:00:01"), mib->localHtCapabilities, mib->getHtOperation()); + } + agreements->accept(MacAddress("02:00:00:00:00:01"), std::string(par("blockAckVariant").stringValue()) == "compressed" ? BlockAckVariant::COMPRESSED : BlockAckVariant::BASIC); subscribe(blockAckAgreementDeletedSignal, this); } void receive(Packet *packet) { Enter_Method("receive"); + take(packet); auto header = packet->peekAtFront(); if (auto data = dynamicPtrCast(header)) { - if (data->getSequenceNumber() == SequenceNumberCyclic(0) && data->getFragmentNumber() != 0) { + if (data->getSequenceNumber() == SequenceNumberCyclic(0) && + (std::string(par("blockAckVariant").stringValue()) == "compressed" || data->getFragmentNumber() != 0)) { delete packet; return; } @@ -80,25 +134,42 @@ class SchedulingRecipient : public Hcf { ASSERT(delivered.empty()); } } + if (auto response = dynamicPtrCast(header)) { + if (orderedReceipt) { + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(100)); + for (int bit = 0; bit < 64; bit++) + ASSERT(response->getBlockAckBitmap().getBit(bit) == (bit < 3)); + ASSERT((delivered == std::vector{100, 101, 102})); + checkedBitmap = true; + delete packet; + return; + } + ASSERT(response->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + if (response->getBlockAckBitmap().getBit(1) && response->getBlockAckBitmap().getBit(2)) { + checkedBitmap = true; + ASSERT(delivered.empty()); + } + } delete packet; } }; Define_Module(SchedulingRecipient); class SchedulingAgreements : public OriginatorBlockAckAgreementHandler { public: - void accept(MacAddress peer, int size) { + void accept(MacAddress peer, int size, Tid tid = 5) { auto request = makeShared(); request->setReceiverAddress(peer); - request->setTid(5); + request->setTid(tid); request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setBufferSize(size); request->setBlockAckPolicy(true); createAgreement(request); - auto agreement = getAgreement(peer, 5); + auto agreement = getAgreement(peer, tid); agreement->setIsAddbaResponseReceived(true); agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); agreement->calculateExpirationTime(); - finishProcedure(peer, 5); + finishProcedure(peer, tid); } }; class SchedulingHcf : public Hcf { @@ -111,15 +182,26 @@ class SchedulingHcf : public Hcf { int retries = 0; int dataInTxop = 0; bool responsePending = false; - Ptr pendingBar; + Ptr pendingBar; const physicallayer::IIeee80211Mode *pendingResponseMode = nullptr; bool firstBarWasStandalone = false; int expired = 0; + bool expiredDuringExchange = false; int collisions = 0; SchedulingRecipient *recipient = nullptr; simtime_t responseEnd; simtime_t txopDeadline; int scenario() const { return par("scenario"); } + bool compressed() const { return std::string(par("blockAckVariant").stringValue()) == "compressed"; } + b responseLength() const { return compressed() ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK; } + Tid barTid(const Ptr& bar) const { + return compressed() ? check_and_cast(bar.get())->getTidInfo() : + check_and_cast(bar.get())->getTidInfo(); + } + SequenceNumberCyclic barStart(const Ptr& bar) const { + return compressed() ? check_and_cast(bar.get())->getStartingSequenceNumber() : + check_and_cast(bar.get())->getStartingSequenceNumber(); + } int burst() const { switch (scenario()) { case 0: return 1; @@ -146,7 +228,17 @@ class SchedulingHcf : public Hcf { delete originatorBlockAckAgreementHandler; auto agreements = new SchedulingAgreements(); originatorBlockAckAgreementHandler = agreements; + if (compressed()) { + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + mib->setPeerHtCapabilities(other, mib->localHtCapabilities, mib->getHtOperation()); + } agreements->accept(peer, window()); + if (compressed() && scenario() == 0) + check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf"))->checkOrderedReceipt(); if (scenario() == 9) { recipient = check_and_cast(getModuleByPath("^.^.^.^.recipient.wlan[0].mac.hcf")); recipient->prepare(); @@ -154,6 +246,8 @@ class SchedulingHcf : public Hcf { } if (scenario() == 4) agreements->accept(other, 64); + if (scenario() == 18) + agreements->accept(peer, 64, 4); rx->setContentionBlocked(true); for (int i = 0; i < burst(); i++) queueData(peer, scenario() == 9 ? (i == 0 ? 200 : 10) : scenario() == 7 && i == 0 ? 1100 : 100); @@ -173,6 +267,10 @@ class SchedulingHcf : public Hcf { for (int i = 0; i < 8; i++) queueData(other, 100); } + if (scenario() == 18) { + for (int i = 0; i < 8; i++) + queueData(peer, 100, 4); + } rx->setContentionBlocked(false); scheduleAfter(SimTime(40, SIMTIME_MS), new cMessage("verify")); } @@ -180,8 +278,11 @@ class SchedulingHcf : public Hcf { void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { if (signal == packetDroppedSignal) { auto drop = dynamic_cast(details); - if (drop && drop->getReason() == LIFETIME_EXPIRED) + if (drop && drop->getReason() == LIFETIME_EXPIRED) { expired++; + expiredDuringExchange |= frameSequenceHandler->isSequenceRunning() && + originatorBlockAckAgreementHandler->getAgreement(peer, 5) != nullptr; + } if (source == this) return; } @@ -226,7 +327,7 @@ class SchedulingHcf : public Hcf { } bool transmitFrame(Packet *packet, simtime_t ifs) override { auto header = packet->peekAtFront(); - auto bar = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); if (bar) { auto frames = edca->getChannelOwner()->getInProgressFrames(); auto candidate = frames->peekFrameToTransmit(); @@ -256,6 +357,8 @@ class SchedulingHcf : public Hcf { } if (scenario() == 4 && data->getReceiverAddress() == other) ASSERT(bars >= 1); + if (scenario() == 18 && data->getTid() == 4) + ASSERT(bars >= 1); auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); if ((scenario() == 12 || scenario() == 14) && data->getTid() == 6) ASSERT(!data->getRetry()); @@ -273,13 +376,13 @@ class SchedulingHcf : public Hcf { if (bars == 1) { firstBarWasStandalone = dataInTxop == 0; ASSERT(bar->getReceiverAddress() == peer); - ASSERT(bar->getTidInfo() == 5); + ASSERT(barTid(bar) == 5); if (scenario() != 9) ASSERT(dataCount == (scenario() == 2 ? 1 : scenario() == 3 ? 4 : scenario() == 7 || scenario() == 12 || scenario() == 14 ? 2 : scenario() == 11 ? 5 : burst())); } auto mode = packet->getTag()->getMode(); auto responseMode = rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT); - auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(LENGTH_BASIC_BLOCKACK); + auto cost = mode->getDuration(packet->getDataLength()) + modeSet->getSifsTime() + responseMode->getDuration(responseLength()); auto txop = edca->getChannelOwner()->getTxopProcedure(); auto end = simTime() + ifs + cost; if (dataInTxop > 1 && txop->getLimit() > 0) @@ -287,7 +390,7 @@ class SchedulingHcf : public Hcf { std::cout << "BAR timing: scenario=" << scenario() << " multiple=" << par("multiple").boolValue() << " start=" << simTime() << " ifs=" << ifs << " bytes=" << packet->getByteLength() << " rate=" << mode->getDataMode()->getNetBitrate() << " responseRate=" << responseMode->getDataMode()->getNetBitrate() - << " response=" << responseMode->getDuration(LENGTH_BASIC_BLOCKACK) << " end=" << end + << " response=" << responseMode->getDuration(responseLength()) << " end=" << end << " deadline=" << txop->getStart() + txop->getLimit() << " data=" << dataInTxop << "\n"; } if (auto delba = dynamicPtrCast(header)) { @@ -300,7 +403,7 @@ class SchedulingHcf : public Hcf { } void transmissionComplete(Packet *packet, const Ptr& header) override { Enter_Method("transmissionComplete"); - auto bar = dynamicPtrCast(header); + auto bar = dynamicPtrCast(header); auto data = dynamicPtrCast(header); if (recipient && (data || bar || dynamicPtrCast(header))) recipient->receive(packet->dup()); @@ -312,7 +415,7 @@ class SchedulingHcf : public Hcf { pendingResponseMode = bar ? rateSelection->computeResponseBlockAckFrameMode(packet, bar, ResponseQueryRole::PREDICT) : rateSelection->computeResponseAckFrameMode(packet, dynamicPtrCast(header), ResponseQueryRole::PREDICT); - auto duration = pendingResponseMode->getDuration(bar ? LENGTH_BASIC_BLOCKACK : LENGTH_ACK); + auto duration = pendingResponseMode->getDuration(bar ? responseLength() : LENGTH_ACK); responseEnd = simTime() + modeSet->getSifsTime() + duration; txopDeadline = edca->getChannelOwner()->getTxopProcedure()->getStart() + edca->getChannelOwner()->getTxopProcedure()->getLimit(); scheduleAt(responseEnd, new cMessage("response")); @@ -327,10 +430,25 @@ class SchedulingHcf : public Hcf { responsePending = false; Ptr header; if (pendingBar) { + if (compressed()) { + auto response = makeShared(); + response->setTransmitterAddress(pendingBar->getReceiverAddress()); + response->setTidInfo(barTid(pendingBar)); + auto responseStart = barStart(pendingBar); + if (bars == 1 && scenario() >= 15 && scenario() <= 17) + responseStart = responseStart + (scenario() == 15 ? -2 : scenario() == 16 ? 1 : 100); + response->setStartingSequenceNumber(responseStart); + BitVector bits; + for (int i = 0; i < 64; i++) + bits.appendBit(scenario() == 9 ? i != 0 : !(bars == 1 && (scenario() == 6 || scenario() == 17))); + response->setBlockAckBitmap(bits); + header = response; + } + else { auto response = makeShared(); response->setTransmitterAddress(pendingBar->getReceiverAddress()); - response->setTidInfo(pendingBar->getTidInfo()); - response->setStartingSequenceNumber(pendingBar->getStartingSequenceNumber()); + response->setTidInfo(barTid(pendingBar)); + response->setStartingSequenceNumber(barStart(pendingBar)); for (int i = 0; i < 64; i++) { BitVector bits; for (int j = 0; j < 16; j++) @@ -338,6 +456,7 @@ class SchedulingHcf : public Hcf { response->setBlockAckBitmap(i, bits); } header = response; + } } else header = makeShared(); @@ -352,14 +471,16 @@ class SchedulingHcf : public Hcf { delete message; std::cout << "Scheduling totals: data=" << dataCount << " bars=" << bars << " delbas=" << delbas << " refused=" << refusedBars << "\n"; ASSERT(bars > 0); - ASSERT(delbas == (scenario() == 4 ? 2 : 1)); + ASSERT(delbas == (scenario() == 4 || scenario() == 18 ? 2 : 1)); if (scenario() != 9) - ASSERT(dataCount == burst() + (scenario() == 4 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 ? 1 : 0)); + ASSERT(dataCount == burst() + (scenario() == 4 || scenario() == 18 ? 8 : scenario() == 5 || scenario() == 6 || scenario() == 12 || scenario() == 14 || (compressed() && scenario() == 16) ? 1 : compressed() && scenario() == 17 ? 4 : 0)); else { ASSERT(recipient->checkedBitmap && recipient->retired()); ASSERT((recipient->delivered == std::vector{1, 2})); ASSERT(dataCount > 3); ASSERT(expired > 0); + if (compressed()) + ASSERT(expiredDuringExchange); } ASSERT(originatorBlockAckAgreementHandler->getAgreements().empty()); ASSERT(originatorBlockAckAgreementHandler->getProcedures().empty()); @@ -367,6 +488,8 @@ class SchedulingHcf : public Hcf { ASSERT(!frameSequenceHandler->isSequenceRunning()); if (scenario() == 5 || scenario() == 6) ASSERT(retries == 1 && bars == 2); + if (compressed() && scenario() >= 15 && scenario() <= 17) + ASSERT(retries == (scenario() == 15 ? 0 : scenario() == 16 ? 1 : 4) && bars == (scenario() == 15 ? 1 : 2)); if (scenario() == 8 || scenario() == 10 || scenario() == 13) ASSERT(firstBarWasStandalone); if (scenario() == 12 || scenario() == 14) @@ -416,22 +539,23 @@ record-vector-results = false record-scalar-results = false **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${variant} == "basic" ? "a" : "n(mixed-2.4Ghz)" **.wlan[*].bitrate = 6Mbps +**.wlan[*].radio.channelNumber = 0 *.host.wlan[*].address = "02:00:00:00:00:01" *.recipient.wlan[*].address = "02:00:00:00:00:02" **.wlan[*].mac.qosStation = true *.recipient.wlan[*].mac.hcf.typename = "SchedulingRecipient" *.host.wlan[*].mac.hcf.typename = "SchedulingHcf" -**.wlan[*].mac.hcf.scenario = ${scenario=0..14} +**.wlan[*].mac.hcf.scenario = ${scenario=0..18} **.wlan[*].mac.hcf.multiple = ${multiple=false,true} **.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${variant="basic","compressed"} **.wlan[*].mac.hcf.originatorAckPolicy.ackTimeout = 1ms **.wlan[*].mac.hcf.originatorAckPolicy.blockAckTimeout = 1ms **.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 2346B **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" -**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 ? 128B : 2346B +**.wlan[*].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = ${scenario} == 9 && ${variant} == "basic" ? 128B : 2346B **.wlan[*].mac.hcf.edca.edcaf[*].msduLifetime = ${scenario} == 9 ? 10ms : 512ms **.wlan[*].mac.hcf.edca.edcaf[*].cwMin = ${scenario} == 12 || ${scenario} == 14 ? 0 : -1 **.wlan[*].mac.hcf.edca.edcaf[2].cwMax = ${scenario} == 14 ? 3 : ${scenario} == 12 ? 0 : -1 diff --git a/tests/module/Ieee80211BlockAckVariant_1.test b/tests/module/Ieee80211BlockAckVariant_1.test index 61da5b30adc..195c298c73a 100644 --- a/tests/module/Ieee80211BlockAckVariant_1.test +++ b/tests/module/Ieee80211BlockAckVariant_1.test @@ -1,8 +1,8 @@ %description: Check profile validation through production HCF initialization. -Retain the default and explicit Basic profile. +Accept the default, explicit Basic, and explicit Compressed profiles. Reject unknown values with support enabled or disabled. -Reject Compressed until its runtime exchange path exists. +Reject delayed policy for Compressed before any ADDBA transmission. %testprog: python3 check-profile.py @@ -18,20 +18,26 @@ import subprocess root = Path.cwd().parents[3] cases = [ - (None, True, None), - ("basic", True, None), - ("invalid", True, 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), - ("invalid", False, 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), - ("compressed", True, 'Compressed Block Ack exchanges are not implemented'), - ("compressed", False, 'Compressed Block Ack exchanges are not implemented'), + (None, True, (), None), + ("basic", True, (), None), + ("invalid", True, (), 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), + ("invalid", False, (), 'Unknown blockAckVariant "invalid": expected "basic" or "compressed"'), + ("compressed", True, (), None), + ("compressed", False, (), None), ] -for variant, supported, diagnostic in cases: +for policies in [("originator",), ("recipient",), ("originator", "recipient")]: + cases.append(("compressed", True, policies, + "Compressed Block Ack does not support delayedAckPolicySupported")) + cases.append(("basic", True, policies, None)) +for variant, supported, policies, diagnostic in cases: command = [str(root / "bin/inet"), "--debug", "-u", "Cmdenv", "-f", str(Path.cwd() / "profile.ini"), "-c", "General", "-r", "0", "--sim-time-limit=0s", "--record-vector-results=false", "--record-scalar-results=false", f"--result-dir={Path.cwd() / 'results'}", f"--**.wlan[*].mac.hcf.isBlockAckSupported={'true' if supported else 'false'}"] if variant is not None: command.append(f'--**.wlan[*].mac.hcf.blockAckVariant="{variant}"') + for policy in policies: + command.append(f'--**.wlan[*].mac.hcf.{policy}BlockAckAgreementPolicy.delayedAckPolicySupported=true') result = subprocess.run(command, cwd=root / "showcases/wireless/blockack", text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) print(result.stdout) diff --git a/tests/module/Ieee80211CompressedBlockAckAdmission_1.test b/tests/module/Ieee80211CompressedBlockAckAdmission_1.test new file mode 100644 index 00000000000..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..4ccb39e660a --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckExchange_1.test @@ -0,0 +1,428 @@ +%description: +Use real association and management rate publication for two Compressed Block Ack hops. +The STA sends thirty UDP payloads through the AP to the other STA. +Observe accepted ADDBA dialogs, matched Compressed BAR/BA, positive receipt bits, and final agreement retirement. +After setup, remove one BSS basic rate through the MIB API. Require the agreement and HT generation to remain unchanged. +Record actual frame modes, lengths, airtime, IFS, and TXOP bounds for both protection modes at 6 and 24 Mbps. +The medium uses zero propagation time. Compare local receipt completion with the exact TXOP endpoint without a timing allowance. +The fixture enforces DATA and ACK bounds under active Compressed agreements, with the permitted initial MSDU and same-size retry exceptions. +The fixture records overruns outside active agreements separately. StrictAllTiming retains the original aggregate assertion for inherited failures. +Repeat each case with a non-HT link. Require payload delivery without ADDBA or BAR. +Repeat each case with A-MSDU aggregation enabled. Require actual A-MSDUs under accepted agreements at 24 Mbps. +The LowestResponse configuration supplies two extra runs with a 6 Mbps BAR and BA, one per protection mode. + +%extraargs: -r 0..15 + +%file: Exchange.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211DataSequenceTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class ExchangeHcf : public Hcf { + struct DataScope { + bool activeCompressed; + AckPolicy policy; + }; + std::map dataScopes; + struct PendingBar { + SequenceNumberCyclic start; + simtime_t bound; + int dataManagementCount; + bool aggregate; + }; + std::map, int> dialogs; + std::map, PendingBar> pendingBars; + simtime_t observedTxopStart = -1; + int dataManagementCount = 0; + bool aggregate = false; + simtime_t controlReceivedAt = -1; + public: + int requests = 0; + int acceptedResponses = 0; + int bars = 0; + int positiveResponses = 0; + int activeData = 0; + int activeAggregates = 0; + int retired = 0; + int initialOverruns = 0; + int generalDataOverruns = 0; + int generalAckOverruns = 0; + int initialAckOverruns = 0; + int initialBlockAckOverruns = 0; + int outsideAgreementDataOverruns = 0; + int outsideAgreementAckOverruns = 0; + int permittedRetryOverruns = 0; + bool refreshedRates = false; + bool clean() const { + std::cout << "Exchange cleanup: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " originator=" << originatorBlockAckAgreementHandler->getAgreements().size() + << " recipient=" << recipientBlockAckAgreementHandler->getAgreements().size() + << " pendingBars=" << pendingBars.size() + << " inactivityTimer=" << inactivityTimer->isScheduled() << " addbaTimer=" << addbaResponseTimer->isScheduled() << "\n"; + for (const auto& entry : originatorBlockAckAgreementHandler->getProcedures()) { + std::cout << "Exchange procedure: peer=" << entry.first.first << " tid=" << int(entry.first.second) + << " state=" << int(entry.second.state) << " deadline=" << entry.second.responseDeadline << "\n"; + ASSERT(entry.second.state == IOriginatorBlockAckAgreementHandler::ProcedureState::WAITING_FOR_SUCCESSOR); + ASSERT(!entry.second.requestAttempted && entry.second.responseDeadline == SIMTIME_MAX); + } + for (auto category : {AC_BK, AC_BE, AC_VI, AC_VO}) { + std::cout << "Exchange remaining: ac=" << category + << " queued=" << edca->getEdcaf(category)->getPendingQueue()->getNumPackets() + << " committed=" << edca->getEdcaf(category)->getInProgressFrames()->getLength() << "\n"; + ASSERT(edca->getEdcaf(category)->getPendingQueue()->isEmpty()); + ASSERT(edca->getEdcaf(category)->getInProgressFrames()->getLength() == 0); + } + ASSERT(!inactivityTimer->isScheduled() && !addbaResponseTimer->isScheduled()); + return originatorBlockAckAgreementHandler->getAgreements().empty() && + recipientBlockAckAgreementHandler->getAgreements().empty() && pendingBars.empty(); + } + void checkPublishedState(MacAddress peer) { + ASSERT(mib->getLocalRateSet().operational.known); + ASSERT(mib->getBssRateSet().basic.known); + ASSERT(!mib->getBssRateSet().basic.legacyRates.empty()); + auto rates = mib->findPeerRateSet(peer); + ASSERT(rates && rates->operational.known && !rates->operational.legacyRates.empty()); + auto ht = mib->findPeerHtState(peer); + ASSERT(mib->isHtOperationSupported() && ht && ht->valid); + } + protected: + void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, + Packet *receivedPacket, const Ptr& receivedHeader) override { + controlReceivedAt = simTime(); + Hcf::transmitControlResponseFrame(responsePacket, responseHeader, receivedPacket, receivedHeader); + } + void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) override { + auto mode = packet->getTag()->getMode(); + auto airtime = mode->getDuration(packet->getDataLength()); + ASSERT(simTime() - airtime - controlReceivedAt == modeSet->getSifsTime()); + if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + std::cout << "Exchange response: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " start=" << simTime() - airtime << " end=" << simTime() << " type=" << header->getType() + << " bytes=" << packet->getByteLength() << " bitrate=" << mode->getDataMode()->getNetBitrate() + << " airtime=" << airtime << " ifs=" << modeSet->getSifsTime() << "\n"; + Hcf::recipientProcessTransmittedControlResponseFrame(packet, header); + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (auto data = dynamicPtrCast(header)) { + auto index = packet->getTag()->getAllocationIndex(); + ASSERT(dataScopes.count(index)); + const auto& scope = dataScopes.at(index); + std::cout << "Exchange scope: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " at=" << simTime() << " allocation=" << index << " activeCompressed=" << scope.activeCompressed + << " ackPolicy=" << scope.policy << " aggregate=" << data->getAMsduPresent() << " retry=" << data->getRetry() << "\n"; + auto owner = edca->getChannelOwner(); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + auto bound = txop->getStart() + txop->getLimit(); + if (simTime() > bound) { + if (par("strictAllTiming").boolValue()) + ASSERT(!data->getAMsduPresent()); + if (scope.activeCompressed) { + ASSERT(dataManagementCount == 1); + ASSERT(!data->getAMsduPresent() || data->getRetry()); + permittedRetryOverruns += data->getRetry(); + } + else + outsideAgreementDataOverruns++; + if (dataManagementCount == 1) + initialOverruns++; + else + generalDataOverruns++; + } + } + Hcf::transmissionComplete(packet, header); + } + void originatorProcessReceivedFrame(Packet *packet, Packet *transmitted) override { + auto data = dynamicPtrCast(transmitted->peekAtFront()); + if (data) { + auto owner = edca->getChannelOwner(); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + if (simTime() > txop->getStart() + txop->getLimit()) { + auto index = transmitted->getTag()->getAllocationIndex(); + const auto& scope = dataScopes.at(index); + if (par("strictAllTiming").boolValue()) + ASSERT(!data->getAMsduPresent()); + if (scope.activeCompressed) { + ASSERT(dataManagementCount == 1); + ASSERT(!data->getAMsduPresent() || data->getRetry()); + } + else + outsideAgreementAckOverruns++; + if (dataManagementCount > 1) + generalAckOverruns++; + else + initialAckOverruns++; + } + } + Hcf::originatorProcessReceivedFrame(packet, transmitted); + } + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + bool activeCompressed = false; + if (auto data = dynamicPtrCast(header)) { + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + activeCompressed = std::string(par("blockAckVariant").stringValue()) == "compressed" && + agreement && agreement->getIsAddbaResponseReceived(); + } + bool sent = Hcf::transmitFrame(packet, ifs); + if (!sent) + return false; + header = packet->peekAtFront(); + if (auto data = dynamicPtrCast(header)) + dataScopes.insert_or_assign(packet->getTag()->getAllocationIndex(), + DataScope{activeCompressed, data->getAckPolicy()}); + auto owner = edca->getChannelOwner(); + if (owner) { + auto txop = owner->getTxopProcedure(); + if (txop->getStart() != observedTxopStart) { + observedTxopStart = txop->getStart(); + dataManagementCount = 0; + aggregate = false; + } + if (dynamicPtrCast(header)) + dataManagementCount++; + } + if (auto request = dynamicPtrCast(header)) { + checkPublishedState(request->getReceiverAddress()); + dialogs[{request->getReceiverAddress(), request->getTid()}] = request->getDialogToken(); + requests++; + } + if (auto bar = dynamicPtrCast(header)) { + ASSERT(bar->getCompressedBitmap() && !bar->getMultiTid()); + ASSERT(packet->getDataLength() == LENGTH_BLOCKACKREQ); + ASSERT(owner); + auto txop = owner->getTxopProcedure(); + pendingBars.insert_or_assign({bar->getReceiverAddress(), bar->getTidInfo()}, + PendingBar{bar->getStartingSequenceNumber(), txop->getStart() + txop->getLimit(), dataManagementCount, aggregate}); + bars++; + } + ASSERT(!dynamicPtrCast(header)); + if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == LENGTH_COMPRESSED_BLOCKACK); + if (auto data = dynamicPtrCast(header)) { + aggregate |= data->getAMsduPresent(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(data->getReceiverAddress(), data->getTid()); + if (agreement && agreement->getIsAddbaResponseReceived()) { + ASSERT(data->getFragmentNumber() == 0 && !data->getMoreFragments()); + activeData++; + activeAggregates += data->getAMsduPresent(); + } + } + if (dynamicPtrCast(header)) + retired++; + if (header->getType() == ST_DATA_WITH_QOS || header->getType() == ST_RTS || + header->getType() == ST_CTS || header->getType() == ST_BLOCKACK_REQ || header->getType() == ST_BLOCKACK) { + auto mode = packet->getTag()->getMode(); + std::cout << "Exchange frame: node=" << getParentModule()->getParentModule()->getParentModule()->getFullName() + << " at=" << simTime() << " type=" << header->getType() << " bytes=" << packet->getByteLength() + << " bitrate=" << mode->getDataMode()->getNetBitrate() << " ifs=" << ifs + << " airtime=" << mode->getDuration(packet->getDataLength()); + if (owner) { + auto txop = owner->getTxopProcedure(); + std::cout << " txopStart=" << txop->getStart() << " txopLimit=" << txop->getLimit() + << " protection=" << txop->getProtectionMechanism() << " dataManagementCount=" << dataManagementCount; + } + std::cout << "\n"; + } + return true; + } + void processLowerFrame(Packet *packet, const Ptr& header) override { + if (auto response = dynamicPtrCast(header); response && response->getReceiverAddress() == mib->address) { + auto key = std::make_pair(response->getTransmitterAddress(), int(response->getTid())); + ASSERT(dialogs.count(key) && dialogs.at(key) == response->getDialogToken()); + ASSERT(response->getStatusCode() == 0 && response->getBlockAckPolicy()); + acceptedResponses++; + } + if (auto response = dynamicPtrCast(header); response && response->getReceiverAddress() == mib->address) { + auto key = std::make_pair(response->getTransmitterAddress(), int(response->getTidInfo())); + ASSERT(pendingBars.count(key)); + auto pending = pendingBars.at(key); + ASSERT(response->getStartingSequenceNumber() == pending.start); + ASSERT(response->getCompressedBitmap() && !response->getMultiTid()); + ASSERT(response->getBlockAckBitmap().getSize() == 64); + bool positive = false; + for (unsigned int i = 0; i < response->getBlockAckBitmap().getSize(); i++) + positive |= response->getBlockAckBitmap().getBit(i); + ASSERT(positive); + std::cout << "Exchange completion: at=" << simTime() << " bound=" << pending.bound + << " dataManagementCount=" << pending.dataManagementCount << "\n"; + // One initial unaggregated MSDU can overrun. A later exchange must finish within the TXOP. + if (pending.dataManagementCount != 1 || pending.aggregate) + ASSERT(simTime() <= pending.bound); + else if (simTime() > pending.bound) + initialBlockAckOverruns++; + positiveResponses++; + pendingBars.erase(key); + } + ASSERT(!dynamicPtrCast(header)); + Hcf::processLowerFrame(packet, header); + if (auto response = dynamicPtrCast(header); + response && response->getReceiverAddress() == mib->address && !refreshedRates) { + auto peer = response->getTransmitterAddress(); + auto tid = response->getTid(); + auto agreement = originatorBlockAckAgreementHandler->getAgreement(peer, tid); + ASSERT(agreement && agreement->getIsAddbaResponseReceived()); + auto generation = mib->findPeerHtState(peer)->generation; + auto rates = mib->getBssRateSet(); + ASSERT(rates.basic.legacyRates.size() > 1); + rates.basic.legacyRates.erase(std::prev(rates.basic.legacyRates.end())); + // Association supplies the initial rates. This rate-only update exercises the production subscriber. + mib->setBssRateSet(rates); + ASSERT(originatorBlockAckAgreementHandler->getAgreement(peer, tid) == agreement); + ASSERT(mib->findPeerHtState(peer)->generation == generation); + refreshedRates = true; + } + } +}; +Define_Module(ExchangeHcf); + +class ExchangeTest : public cSimpleModule, public cListener { + int received = 0; + int64_t bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + auto packet = check_and_cast(value); + received++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.sta2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(1200, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(received == 30 && bytes == 21000); + for (const char *path : {"^.sta1.wlan[0].mac.hcf", "^.ap.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + std::cout << "Exchange summary: node=" << path << " accepted=" << hcf->acceptedResponses + << " bars=" << hcf->bars << " positiveResponses=" << hcf->positiveResponses + << " activeData=" << hcf->activeData << " retired=" << hcf->retired + << " activeAggregates=" << hcf->activeAggregates << " initialOverruns=" << hcf->initialOverruns + << " generalDataOverruns=" << hcf->generalDataOverruns << " generalAckOverruns=" << hcf->generalAckOverruns + << " outsideAgreementDataOverruns=" << hcf->outsideAgreementDataOverruns + << " outsideAgreementAckOverruns=" << hcf->outsideAgreementAckOverruns + << " permittedRetryOverruns=" << hcf->permittedRetryOverruns + << " initialAckOverruns=" << hcf->initialAckOverruns << " initialBlockAckOverruns=" << hcf->initialBlockAckOverruns << "\n"; + if (par("ht").boolValue()) { + ASSERT(hcf->requests > 0 && hcf->acceptedResponses > 0 && hcf->bars > 0); + ASSERT(hcf->positiveResponses > 0 && hcf->positiveResponses <= hcf->bars && hcf->activeData > 0 && hcf->retired > 0); + ASSERT(hcf->refreshedRates); + if (par("aggregate").boolValue() && par("rate").intValue() == 24) + ASSERT(hcf->activeAggregates > 0); + } + else + ASSERT(hcf->requests == 0 && hcf->acceptedResponses == 0 && hcf->bars == 0 && hcf->activeData == 0); + ASSERT(hcf->clean()); + } + ASSERT(check_and_cast(getModuleByPath("^.sta2.wlan[0].mac.hcf"))->clean()); + std::cout << "Compressed infrastructure exchange assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(ExchangeTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module ExchangeHcf extends Hcf { parameters: bool strictAllTiming = default(false); @class(::inet::ieee80211::ExchangeHcf); } +simple ExchangeTest { parameters: bool ht; bool aggregate; int rate; @class(::inet::ieee80211::ExchangeTest); } +network ExchangeNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta1: WirelessHost; + sta2: WirelessHost; + test: ExchangeTest; +} + +%inifile: omnetpp.ini +[General] +**.hcf.blockAckVariant = "compressed" +network = ExchangeNetwork +ned-path = .;../../../../src +sim-time-limit = 1.3s +seed-set = 0 +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = 0s +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.sta1.numApps = 1 +*.sta1.app[0].typename = "UdpBasicApp" +*.sta1.app[0].destAddresses = "sta2" +*.sta1.app[0].destPort = 1000 +*.sta1.app[0].messageLength = 700B +*.sta1.app[0].startTime = 1s +*.sta1.app[0].stopTime = 1.00295s +*.sta1.app[0].sendInterval = 100us +*.sta2.numApps = 1 +*.sta2.app[0].typename = "UdpSink" +*.sta2.app[0].localPort = 1000 +*.sta*.ipv4.arp.typename = "GlobalArp" +*.test.ht = ${ht=true,false} +*.test.aggregate = ${aggregationPolicy="","BasicMsduAggregationPolicy"} != "" +*.test.rate = ${rate=6,24} +**.wlan[*].opMode = ${ht} ? "n(mixed-2.4Ghz)" : "g(mixed)" +**.wlan[*].bitrate = ${rate}Mbps +**.bandName = "2.4 GHz" +**.wlan[*].radio.antenna.numAntennas = 2 +**.wlan[*].classifier.typename = "QosClassifier" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "ExchangeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.originatorAckPolicy.blockAckReqThreshold = 5 +**.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +**.originatorMacDataService.msduAggregationPolicy.typename = ${aggregationPolicy} +**.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 +**.originatorMacDataService.msduAggregationPolicy.maxAMsduSize = 1600 +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +**.rtsPolicy.rtsThreshold = 500B +**.txopProcedure.protectionMechanism = ${protection="single","multiple"} +**.txopProcedure.txopLimit = 1ms +**.wlan[*].radio.receiver.energyDetection = -105dBm +**.wlan[*].radio.receiver.sensitivity = -105dBm +**.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.sta1.mobility.initialX = 0m +*.ap.mobility.initialX = 5m +*.sta2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +[Config StrictAllTiming] +**.wlan[*].mac.hcf.strictAllTiming = true + +[Config LowestResponse] +constraint = $ht == true && $aggregationPolicy == "" && $rate == 6 +*.test.ht = true +*.test.aggregate = false +*.test.rate = 6 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.wlan[*].mac.hcf.rateSelection.controlFrameBitrate = 6Mbps + +%contains: stdout +Compressed infrastructure exchange assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckInput_1.test b/tests/module/Ieee80211CompressedBlockAckInput_1.test new file mode 100644 index 00000000000..0775fc9b4a7 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckInput_1.test @@ -0,0 +1,325 @@ +%description: +Check malformed input and profile conflicts through initialized HCF. +Both local profiles receive valid addressed BAR from the opposite profile. +HCF must stop before receipt, reorder, inactivity, recovery, or response updates. +Malformed BAR and unaddressed BAR must not cause a profile conflict. +An active Compressed agreement rejects fragmented DATA before ACK and data-service callbacks. +A valid Compressed BAR without an agreement requests DELBA. A malformed BAR does not. +The fixture supplies an accepted agreement. It does not test ADDBA negotiation or wireless reception. +The separate Compressed exchange fixture tests negotiation and frame exchange. + +%extraargs: -r 0..1 + +%file: CompressedInput.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet::ieee80211 { +class InputAgreement : public RecipientBlockAckAgreement { + public: + int refreshes = 0; + using RecipientBlockAckAgreement::RecipientBlockAckAgreement; + void calculateExpirationTime() override { + refreshes++; + RecipientBlockAckAgreement::calculateExpirationTime(); + } +}; +class InputAgreements : public RecipientBlockAckAgreementHandler { + public: + int dataUpdates = 0; + InputAgreement *accept(MacAddress peer, BlockAckVariant variant) { + auto agreement = new InputAgreement(peer, 5, SequenceNumberCyclic(100), 64, SimTime(5, SIMTIME_MS)); + agreement->setVariant(variant); + agreement->addbaResposneSent(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } + void qosFrameReceived(const Ptr& header, IBlockAckAgreementHandlerCallback *callback) override { + dataUpdates++; + RecipientBlockAckAgreementHandler::qosFrameReceived(header, callback); + } +}; +class InputAckSpy : public IRecipientAckProcedure { + public: + IRecipientAckProcedure *delegate; + int calls = 0; + explicit InputAckSpy(IRecipientAckProcedure *delegate) : delegate(delegate) {} + ~InputAckSpy() override { delete delegate; } + void processReceivedFrame(Packet *packet, const Ptr& header, + IRecipientAckPolicy *policy, IProcedureCallback *callback) override { + calls++; + delegate->processReceivedFrame(packet, header, policy, callback); + } + void processTransmittedAck(const Ptr& header) override { + delegate->processTransmittedAck(header); + } +}; +class InputDataSpy : public IRecipientQosMacDataService { + public: + IRecipientQosMacDataService *delegate = nullptr; + int dataCalls = 0; + int controlCalls = 0; + int retireCalls = 0; + std::vector retireAgreement(RecipientBlockAckAgreement *agreement, bool reset) override { + retireCalls++; + return delegate->retireAgreement(agreement, reset); + } + void clearPeer(MacAddress peer) override { delegate->clearPeer(peer); } + std::vector dataFrameReceived(Packet *packet, const Ptr& header, + IRecipientBlockAckAgreementHandler *agreements, bool interframe = false) override { + dataCalls++; + return delegate->dataFrameReceived(packet, header, agreements, interframe); + } + std::vector controlFrameReceived(Packet *packet, const Ptr& header, + IRecipientBlockAckAgreementHandler *agreements) override { + controlCalls++; + return delegate->controlFrameReceived(packet, header, agreements); + } + std::vector managementFrameReceived(Packet *packet, const Ptr& header) override { + return delegate->managementFrameReceived(packet, header); + } +}; +class CompressedInputHcf : public Hcf { + protected: + InputDataSpy dataSpy; + InputAckSpy *ackSpy = nullptr; + InputAgreements *agreements = nullptr; + InputAgreement *accepted = nullptr; + BlockAckVariant localVariant = BlockAckVariant::BASIC; + int responses = 0; + int deliveries = 0; + int delbas = 0; + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + const MacAddress absent = MacAddress("02:00:00:00:00:04"); + std::vector receiptSnapshot; + simtime_t expirationSnapshot; + + std::vector receipts() { + std::vector result; + auto record = accepted->getBlockAckRecord(); + for (int sequence = 100; sequence < 164; sequence++) { + if (localVariant == BlockAckVariant::COMPRESSED) + result.push_back(record->hasCompleteReceipt(SequenceNumberCyclic(sequence))); + else + for (int fragment = 0; fragment < 16; fragment++) + result.push_back(record->getAckState(SequenceNumberCyclic(sequence), fragment)); + } + return result; + } + void unchanged() { + ASSERT(agreements->getAgreement(5, peer) == accepted); + ASSERT(!agreements->isTeardownPending(peer, 5)); + ASSERT(accepted->refreshes == 0); + ASSERT(accepted->getExpirationTime() == expirationSnapshot); + ASSERT(accepted->getHighestReceivedSequence() == SequenceNumberCyclic(100)); + ASSERT(receipts() == receiptSnapshot); + if (localVariant == BlockAckVariant::COMPRESSED) + ASSERT(accepted->getBlockAckRecord()->getWindowStart() == SequenceNumberCyclic(100)); + ASSERT(inactivityTimer->isScheduled()); + ASSERT(inactivityTimer->getArrivalTime() == expirationSnapshot); + ASSERT(agreements->dataUpdates == 0); + ASSERT(dataSpy.dataCalls == 0 && dataSpy.controlCalls == 0 && dataSpy.retireCalls == 0); + ASSERT(ackSpy->calls == 0); + ASSERT(responses == 0 && deliveries == 0 && delbas == 0); + } + Ptr makeBar(bool compressed, MacAddress transmitter, bool addressed, + int fragment = 0, bool unsupported = false) { + Ptr bar; + if (unsupported) + bar = makeShared(); + else if (compressed) { + auto concrete = makeShared(); + concrete->setTidInfo(5); + concrete->setFragmentNumber(fragment); + concrete->setStartingSequenceNumber(SequenceNumberCyclic(101)); + bar = concrete; + } + else { + auto concrete = makeShared(); + concrete->setTidInfo(5); + concrete->setFragmentNumber(fragment); + concrete->setStartingSequenceNumber(SequenceNumberCyclic(101)); + bar = concrete; + } + bar->setTransmitterAddress(transmitter); + bar->setReceiverAddress(addressed ? mac->getAddress() : MacAddress("02:00:00:00:00:09")); + return bar; + } + void injectBar(bool compressed, MacAddress transmitter, bool addressed, bool conflict, + int fragment = 0, int extraBytes = 0, bool unsupported = false) { + auto bar = makeBar(compressed, transmitter, addressed, fragment, unsupported); + auto packet = new Packet("bar", bar); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + if (extraBytes) + packet->insertAtBack(makeShared(B(extraBytes))); + packet->insertAtBack(makeShared()); + bool caught = false; + try { + processLowerFrame(packet, bar); + } + catch (const cRuntimeError& error) { + // processLowerFrame takes the Packet before the exception. This wrapper owns its disposal. + take(packet); + delete packet; + std::string message = error.what(); + ASSERT(message.find("Block Ack profile conflict at ") == 0); + ASSERT(message.find("peer=" + peer.str() + " TID=5") != std::string::npos); + ASSERT(message.find(compressed ? "local=basic received=compressed" : "local=compressed received=basic") != std::string::npos); + caught = true; + } + ASSERT(caught == conflict); + } + void injectFragment(int fragment, bool more, AckPolicy policy) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->addChunkLength(QOSCONTROL_PART_LENGTH); + data->setTransmitterAddress(peer); + data->setReceiverAddress(mac->getAddress()); + data->setAddress3(peer); + data->setTid(5); + data->setAckPolicy(policy); + data->setSequenceNumber(SequenceNumberCyclic(101)); + data->setFragmentNumber(fragment); + data->setMoreFragments(more); + auto packet = new Packet("fragment", data); + packet->addTag()->setMode(modeSet->getMode(bps(6000000))); + packet->insertAtBack(makeShared(B(10))); + packet->insertAtBack(makeShared()); + processLowerFrame(packet, data); + } + void sendUp(const std::vector& frames) override { + deliveries += frames.size(); + for (auto frame : frames) { + take(frame); + delete frame; + } + } + void transmitControlResponseFrame(Packet *packet, const Ptr&, + Packet *, const Ptr&) override { + responses++; + delete packet; + } + void processMgmtFrame(Packet *packet, const Ptr& header) override { + auto delba = dynamicPtrCast(header); + ASSERT(delba && delba->getReceiverAddress() == absent && delba->getTid() == 5); + ASSERT(!delba->getInitiator() && delba->getReasonCode() == 38); + delbas++; + delete packet; + } + public: + void runChecks() { + Enter_Method("runChecks"); + localVariant = std::string(par("blockAckVariant").stringValue()) == "compressed" ? BlockAckVariant::COMPRESSED : BlockAckVariant::BASIC; + delete recipientBlockAckAgreementHandler; + agreements = new InputAgreements(); + recipientBlockAckAgreementHandler = agreements; + accepted = agreements->accept(peer, localVariant); + ackSpy = new InputAckSpy(recipientAckProcedure); + recipientAckProcedure = ackSpy; + dataSpy.delegate = recipientDataService; + recipientDataService = &dataSpy; + auto seed = makeShared(); + seed->setAckPolicy(BLOCK_ACK); + seed->setSequenceNumber(SequenceNumberCyclic(100)); + accepted->recordReceived(seed); + if (localVariant == BlockAckVariant::COMPRESSED) + accepted->getBlockAckRecord()->compressedFrameReceived(seed); + else + accepted->blockAckPolicyFrameReceived(seed); + receiptSnapshot = receipts(); + expirationSnapshot = accepted->getExpirationTime(); + scheduleInactivityTimer(); + unchanged(); + + bool oppositeCompressed = localVariant == BlockAckVariant::BASIC; + injectBar(oppositeCompressed, peer, true, true); + unchanged(); + injectBar(oppositeCompressed, peer, false, false); + unchanged(); + for (bool compressed : {false, true}) { + injectBar(compressed, peer, true, false, 1); + unchanged(); + injectBar(compressed, peer, true, false, 0, 1); + unchanged(); + injectBar(compressed, peer, true, false, 0, 0, true); + unchanged(); + injectBar(compressed, absent, true, false, 1); + unchanged(); + injectBar(compressed, absent, true, false, 0, 1); + unchanged(); + } + if (localVariant == BlockAckVariant::COMPRESSED) { + for (AckPolicy policy : {NORMAL_ACK, BLOCK_ACK}) { + injectFragment(0, true, policy); + unchanged(); + injectFragment(1, false, policy); + unchanged(); + } + } + injectBar(true, absent, true, false); + ASSERT(delbas == 1 && responses == 0 && deliveries == 0); + ASSERT(agreements->getAgreement(5, absent) == nullptr); + ASSERT(agreements->isTeardownPending(absent, 5)); + ASSERT(agreements->getAgreement(5, peer) == accepted); + ASSERT(receipts() == receiptSnapshot && accepted->refreshes == 0); + std::cout << "Compressed Block Ack input assertions passed.\n"; + } +}; +Define_Module(CompressedInputHcf); +class CompressedInputTest : public cSimpleModule { + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("test")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->runChecks(); + endSimulation(); + } +}; +Define_Module(CompressedInputTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module CompressedInputHcf extends Hcf { parameters: @class(::inet::ieee80211::CompressedInputHcf); } +simple CompressedInputTest { parameters: @class(::inet::ieee80211::CompressedInputTest); } +network CompressedInputNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: CompressedInputTest; +} + +%inifile: omnetpp.ini +[General] +network = CompressedInputNetwork +ned-path = .;../../../../src +sim-time-limit = 2ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "CompressedInputHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = ${"basic", "compressed"} +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m + +%contains: stdout +Compressed Block Ack input assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckInterframe_1.test b/tests/module/Ieee80211CompressedBlockAckInterframe_1.test new file mode 100644 index 00000000000..6d9a76be69d --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckInterframe_1.test @@ -0,0 +1,542 @@ +%description: +Check Compressed interframe behavior through physical reception and HCF ACK completion. +The fixture supplies an accepted recipient agreement with four positions. It does not test negotiation. +A raw radio driver transmits HT DATA and receives real HCF responses. +The driver supplies no interframe relation flag to HCF or the data service. +Check SIFS, the exact tolerance boundary, a missing prior receipt, wrap, and unrelated traffic. +Check RIFS with an external HT waveform that omits the six-microsecond signal extension. +The external transmitter changes physical and analog duration together. It retains the actual HT mode and PHY length. +Check a default-extension RIFS control and a gap outside the RIFS tolerance. +Repeat each case with zero and five-microsecond propagation time. +After cache expiry, require the saved physical snapshot to retain the exact interval for a later transmission. + +%extraargs: -r 0..27 + +%file: Interframe.cc +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" +#include "inet/mobility/contract/IMobility.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmitterAnalogModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +using namespace physicallayer; + +class InterframeTransmitter : public Ieee80211Transmitter { + public: + const ITransmission *createTransmission(const IRadio *radio, const Packet *packet, simtime_t start) const override { + if (!packet->isName("first-no-extension")) + return Ieee80211Transmitter::createTransmission(radio, packet, start); + auto selected = computeTransmissionMode(packet); + auto phy = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + ASSERT(selected->getHtMcsIndex() == 0); + ASSERT(selected->getSignalExtensionTime() == SimTime(6, SIMTIME_US)); + auto duration = selected->getDuration(B(phy->getLengthField())) - SimTime(6, SIMTIME_US); + auto preamble = selected->getPreambleMode()->getDuration(); + auto header = selected->getHeaderMode()->getDuration(); + auto data = duration - preamble - header; + auto mobility = radio->getAntenna()->getMobility(); + auto position = mobility->getCurrentPosition(); + auto orientation = mobility->getCurrentAngularPosition(); + auto analog = getAnalogModel()->createAnalogModel(preamble, header, data, centerFrequency, + selected->getDataMode()->getBandwidth(), computeTransmissionPower(packet)); + return new Ieee80211Transmission(radio, packet, start, start + duration, preamble, header, data, + position, position, orientation, orientation, nullptr, nullptr, nullptr, nullptr, + analog, selected, computeTransmissionChannel(packet)); + } +}; +Define_Module(InterframeTransmitter); + +class InterframeReordering : public BlockAckReordering { + public: + ReceiveBuffer *find(MacAddress peer) const { + auto it = receiveBuffers.find({5, peer}); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; +class InterframeRecipient : public RecipientQosMacDataService { + protected: + void initialize() override { + RecipientQosMacDataService::initialize(); + delete blockAckReordering; + blockAckReordering = new InterframeReordering(); + } + public: + ReceiveBuffer *buffer(MacAddress peer) const { + return static_cast(blockAckReordering)->find(peer); + } +}; +Define_Module(InterframeRecipient); + +class InterframeJammer : public cSimpleModule { + public: + void start() { + Enter_Method("start"); + scheduleAfter(SimTime(1, SIMTIME_PS), new cMessage("busy")); + } + protected: + void handleMessage(cMessage *message) override { + delete message; + auto radio = check_and_cast(getParentModule()->getSubmodule("radio")); + radio->setRadioMode(IRadio::RADIO_MODE_TRANSMITTER); + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(MacAddress("02:00:00:00:00:08")); + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet("busy-only", header); + auto fcs = makeShared(); + fcs->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(fcs); + auto catalog = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const IIeee80211Mode *mode = nullptr; + for (int i = 0; i < catalog->getNumModes(); i++) { + auto candidate = catalog->getMode(i); + if (candidate->getHtMcsIndex() == 8 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + mode = candidate; + } + ASSERT(mode && mode->getDataMode()->getNumberOfSpatialStreams() == 2); + packet->addTag()->setMode(mode); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + send(packet, "out"); + } +}; +Define_Module(InterframeJammer); + +class InterframeAgreements : public RecipientBlockAckAgreementHandler { + public: + RecipientBlockAckAgreement *accept(MacAddress peer, int start) { + auto agreement = new RecipientBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 4, SIMTIME_ZERO); + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } +}; +class InterframeHcf : public Hcf { + public: + const MacAddress peer = MacAddress("02:00:00:00:00:02"); + InterframeRecipient *recipient = nullptr; + InterframeAgreements *agreements = nullptr; + RecipientBlockAckAgreement *accepted = nullptr; + std::vector delivered; + int ackCompletions = 0; + int ctsCompletions = 0; + int baCompletions = 0; + int physicalStarts = 0; + int busyStates = 0; + MacAddress address() const { return mac->getAddress(); } + void prepare(int start) { + Enter_Method("prepare"); + delete recipientBlockAckAgreementHandler; + agreements = new InterframeAgreements(); + recipientBlockAckAgreementHandler = agreements; + accepted = agreements->accept(peer, start); + recipient = check_and_cast(getSimulation()->getSystemModule()->getSubmodule("recipientProbe")); + recipientDataService = recipient; + } + void retireAndAcceptAgain(int start) { + Enter_Method("retireAndAcceptAgain"); + retireAgreement(peer, 5, false, true); + accepted = agreements->accept(peer, start); + // Supply prior receipt state independently of the previous agreement's physical ACK context. + auto seed = makeShared(); + seed->setSequenceNumber(SequenceNumberCyclic(start)); + accepted->recordReceived(seed); + accepted->getBlockAckRecord()->compressedFrameReceived(seed); + } + void check(int start, int scenario, bool report = true) { + Enter_Method("check"); + int first = (start + (scenario == 2 || scenario == 12 ? 1 : 0)) % 4096; + int last = (start + 4) % 4096; + int recordStart = (start + 1) % 4096; + bool related = scenario == 0 || scenario == 3 || scenario == 4 || scenario == 6 || scenario == 7; + auto record = accepted->getBlockAckRecord(); + ASSERT(record->getWindowStart() == SequenceNumberCyclic(recordStart)); + unsigned bitmap = 0; + for (int i = 0; i < 64; i++) + if (record->getCompressedAckState(SequenceNumberCyclic((recordStart + i) % 4096))) + bitmap |= 1u << i; + ASSERT(bitmap == (scenario == 2 || scenario == 12 ? 9u : 8u)); + auto buffer = recipient->buffer(peer); + ASSERT(buffer); + int expectedB = related ? (last + 1) % 4096 : scenario == 2 || scenario == 12 ? (start + 2) % 4096 : recordStart; + ASSERT(buffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(expectedB)); + ASSERT(buffer->getLength() == (related ? 0 : 1)); + std::vector expected = related ? std::vector{first, last} : std::vector{first}; + ASSERT(delivered == expected); + ASSERT(ackCompletions == (scenario >= 7 && scenario <= 9 ? 1 : 2)); + ASSERT(ctsCompletions == (scenario == 5 ? 1 : 0)); + ASSERT(baCompletions == (scenario == 12 ? 1 : 0)); + if (scenario == 13 && report) { + ASSERT(physicalStarts == 2); + ASSERT(busyStates > 0); + } + if (report) + std::cout << "Interframe scenario=" << scenario << " R=" << recordStart << " bitmap=" << bitmap + << " B=" << expectedB << " ACK=" << ackCompletions << " CTS=" << ctsCompletions << "\n"; + } + void checkNoPending() { + Enter_Method("checkNoPending"); + auto watch = dynamic_cast(findObject("numPendingInterframeReceptions", false)); + ASSERT(watch && watch->str() == "0"); + } + protected: + void sendUp(const std::vector& frames) override { + for (auto frame : frames) { + delivered.push_back(frame->peekAtFront()->getSequenceNumber().get()); + take(frame); + delete frame; + } + } + void transmissionComplete(Packet *packet, const Ptr& header) override { + if (header->getType() == ST_ACK) + ackCompletions++; + if (header->getType() == ST_CTS) + ctsCompletions++; + if (header->getType() == ST_BLOCKACK) + baCompletions++; + Hcf::transmissionComplete(packet, header); + if (header->getType() == ST_ACK && ackCompletions == 1 && par("busyProbe").boolValue()) + check_and_cast(getSimulation()->getSystemModule()->getModuleByPath("jammerNode.jammer"))->start(); + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (source == getParentModule()->getParentModule()->getSubmodule("radio") && signal == receptionStartedSignal) + physicalStarts++; + Hcf::receiveSignal(source, signal, object, details); + } + void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override { + if (source == getParentModule()->getParentModule()->getSubmodule("radio") && + signal == IRadio::receptionStateChangedSignal && value == IRadio::RECEPTION_STATE_BUSY) + busyStates++; + Hcf::receiveSignal(source, signal, value, details); + } +}; +Define_Module(InterframeHcf); + +class InterframeDriver : public cSimpleModule, public cListener { + protected: + InterframeHcf *receiver = nullptr; + Ieee80211Radio *radio = nullptr; + const IRadioMedium *medium = nullptr; + const IIeee80211Mode *ht = nullptr; + const Ieee80211ModeSet *catalog = nullptr; + int scenario = -1; + int start = 100; + int receivedAcks = 0; + bool finalSent = false; + TransmissionTimingSnapshot saved; + simtime_t initialAckArrival = -1; + bool snapshotVerified = false; + bool completed = false; + int malformedIndex = 0; + + void initialize() override { + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("start")); + } + void finish() override { ASSERT(completed); } + void scheduleFinal(simtime_t gap) { + scheduleAfter(gap, new cMessage("final")); + } + void sendFrame(const Ptr& header, const char *name, bool payload = false) { + header->setDurationField(SIMTIME_ZERO); + auto packet = new Packet(name, header); + if (payload) + packet->insertAtBack(makeShared(B(10))); + auto fcs = makeShared(); + fcs->setFcsMode(FCS_DECLARED_CORRECT); + packet->insertAtBack(fcs); + packet->addTag()->setProtocol(&Protocol::ieee80211Mac); + packet->addTag()->setMode(header->getType() == ST_RTS ? catalog->getMode(bps(6000000)) : ht); + send(packet, "out"); + } + void data(int sequence, const char *name, bool addressed = true, AckPolicy policy = NORMAL_ACK) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(addressed ? receiver->address() : MacAddress("02:00:00:00:00:09")); + header->setTransmitterAddress(receiver->peer); + header->setAddress3(receiver->peer); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setAckPolicy(policy); + sendFrame(header, name, true); + } + void sendMalformed() { + Ptr bar; + int kind = malformedIndex % 3; + if (kind == 2) + bar = makeShared(); + else { + auto compressed = makeShared(); + compressed->setStartingSequenceNumber(SequenceNumberCyclic((start + 2) % 4096)); + compressed->setTidInfo(5); + compressed->setFragmentNumber(kind == 0 ? 1 : 0); + bar = compressed; + } + bar->setTransmitterAddress(receiver->peer); + bar->setReceiverAddress(receiver->address()); + sendFrame(bar, "malformed-bar", kind == 1); + } + void receiveSignal(cComponent *, simsignal_t signal, cObject *object, cObject *) override { + Enter_Method_Silent(); + auto transmission = check_and_cast(object); + auto name = std::string(transmission->getPacket()->getName()); + if (signal == transmissionStartedSignal && name == "snapshot-probe") { + ASSERT(saved.transmissionId >= 0); + ASSERT(medium->getTransmission(saved.transmissionId) == nullptr); + ASSERT(medium->getTransmissionTimingSnapshot(saved.transmissionId).transmissionId == -1); + auto gap = medium->computePrecedingSignalGap(transmission->getId(), saved); + ASSERT(gap.has_value()); + ASSERT(*gap == transmission->getStartTime() - saved.arrivalEndTimes.at(radio->getId())); + snapshotVerified = true; + } + if (signal == transmissionEndedSignal && name == "unrelated") + scheduleFinal(catalog->getSifsTime() + (scenario == 11 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10 + 1) : SIMTIME_ZERO)); + if (signal == transmissionEndedSignal && (name == "first-no-extension" || name == "first-rifs-default")) { + auto extension = transmission->getSignalExtensionDuration(); + ASSERT(extension.has_value()); + ASSERT(*extension == (scenario == 8 ? SimTime(6, SIMTIME_US) : SIMTIME_ZERO)); + scheduleFinal(SimTime(2, SIMTIME_US) + (scenario == 9 ? SimTime(200001, SIMTIME_PS) : SIMTIME_ZERO)); + } + if (signal == transmissionEndedSignal && name == "snapshot-probe") + scheduleAfter(SimTime(20, SIMTIME_US), new cMessage("finish")); + if (signal == transmissionEndedSignal && name == "malformed-bar") { + auto propagation = getSimulation()->getSystemModule()->getSubmodule("radioMedium")->getSubmodule("propagation"); + scheduleAfter(SimTime(propagation->par("propagationTime").doubleValue()) + SimTime(1, SIMTIME_PS), + new cMessage("malformed-check")); + } + } + void handleMessage(cMessage *message) override { + if (!message->isSelfMessage()) { + auto packet = check_and_cast(message); + ASSERT(!packet->hasBitError()); + if (packet->isName("busy-only")) { + delete packet; + return; + } + auto header = packet->peekAtFront(); + if (header->getType() == ST_BLOCKACK) { + ASSERT(scenario == 12 && receivedAcks == 0); + scheduleAfter(catalog->getSifsTime(), new cMessage("first")); + } + else if (header->getType() == ST_CTS) { + ASSERT(scenario == 5 && receivedAcks == 1); + scheduleFinal(catalog->getSifsTime()); + } + else { + ASSERT(header->getType() == ST_ACK); + receivedAcks++; + if (receivedAcks == 1 && (scenario < 7 || scenario >= 10)) { + auto timing = packet->getTag(); + saved = medium->getTransmissionTimingSnapshot(timing->getTransmissionId()); + initialAckArrival = timing->getEndTime(); + ASSERT(initialAckArrival == simTime()); + if (scenario == 10) + receiver->retireAndAcceptAgain(start); + if (scenario == 4 || scenario == 11) + scheduleAfter(catalog->getSifsTime(), new cMessage("unrelated")); + else if (scenario == 5) + scheduleAfter(catalog->getSifsTime(), new cMessage("rts")); + else { + auto offset = scenario == 1 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10 + 1) : + scenario == 6 ? SimTime::fromRaw(catalog->getSlotTime().raw() / 10) : SIMTIME_ZERO; + scheduleFinal(catalog->getSifsTime() + offset); + } + } + else { + ASSERT(finalSent); + if (saved.transmissionId < 0) + saved = medium->getTransmissionTimingSnapshot(packet->getTag()->getTransmissionId()); + scheduleAfter(SimTime(2, SIMTIME_MS), new cMessage("snapshot")); + } + } + delete packet; + return; + } + std::string name = message->getName(); + delete message; + if (name == "start") { + receiver = check_and_cast(getSimulation()->getSystemModule()->getModuleByPath("receiver.wlan[0].mac.hcf")); + radio = check_and_cast(getParentModule()->getSubmodule("radio")); + medium = radio->getMedium(); + ASSERT(medium->getTransmission(0) == nullptr); + ASSERT(medium->getTransmissionTimingSnapshot(0).transmissionId == -1); + ASSERT(!medium->computePrecedingSignalGap(0, TransmissionTimingSnapshot()).has_value()); + radio->setRadioMode(IRadio::RADIO_MODE_TRANSCEIVER); + radio->subscribe(transmissionStartedSignal, this); + radio->subscribe(transmissionEndedSignal, this); + scenario = par("scenario"); + start = scenario == 3 ? 4094 : 100; + catalog = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + for (int i = 0; i < catalog->getNumModes(); i++) { + auto candidate = catalog->getMode(i); + if (candidate->getHtMcsIndex() == 0 && !candidate->isHtShortGuardInterval() && + candidate->getDataMode()->getBandwidth() == MHz(20)) + ht = candidate; + } + ASSERT(ht); + receiver->prepare(start); + bool rifs = scenario >= 7 && scenario <= 9; + if (scenario == 12) { + auto bar = makeShared(); + bar->setTransmitterAddress(receiver->peer); + bar->setReceiverAddress(receiver->address()); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(101)); + sendFrame(bar, "initial-bar"); + } + else + data((start + (scenario == 2 ? 1 : 0)) % 4096, + rifs ? scenario == 8 ? "first-rifs-default" : "first-no-extension" : "first", + true, rifs ? BLOCK_ACK : NORMAL_ACK); + } + else if (name == "first") + data(101, "first"); + else if (name == "final") { + finalSent = true; + data((start + 4) % 4096, "final"); + } + else if (name == "unrelated") + data(200, "unrelated", false); + else if (name == "rts") { + auto header = makeShared(); + header->setTransmitterAddress(receiver->peer); + header->setReceiverAddress(receiver->address()); + sendFrame(header, "rts"); + } + else if (name == "snapshot") { + receiver->check(start, scenario); + data(300, "snapshot-probe", false); + } + else if (name == "finish") { + ASSERT(snapshotVerified); + ASSERT(receivedAcks == (scenario >= 7 && scenario <= 9 ? 1 : 2)); + sendMalformed(); + } + else if (name == "malformed-check") { + receiver->checkNoPending(); + receiver->check(start, scenario, false); + malformedIndex++; + if (malformedIndex < 6) { + sendMalformed(); + return; + } + std::cout << "Compressed Block Ack interframe assertions passed.\n"; + completed = true; + endSimulation(); + } + } +}; +Define_Module(InterframeDriver); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +import inet.mobility.static.StationaryMobility; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Transmitter; +module InterframeTransmitter extends Ieee80211Transmitter { parameters: @class(::inet::ieee80211::InterframeTransmitter); } +module InterframeRecipient extends RecipientQosMacDataService { parameters: @class(::inet::ieee80211::InterframeRecipient); } +module InterframeHcf extends Hcf { parameters: bool busyProbe; @class(::inet::ieee80211::InterframeHcf); } +simple InterframeJammer { + parameters: @class(::inet::ieee80211::InterframeJammer); + gates: output out; +} +simple InterframeDriver { + parameters: int scenario; @class(::inet::ieee80211::InterframeDriver); + gates: input in; output out; +} +module DriverNode { + parameters: @networkNode; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + driver: InterframeDriver; + connections allowunconnected: + driver.out --> radio.upperLayerIn; + radio.upperLayerOut --> driver.in; +} +module JammerNode { + parameters: @networkNode; + submodules: + mobility: StationaryMobility; + radio: Ieee80211ScalarRadio; + jammer: InterframeJammer; + connections allowunconnected: + jammer.out --> radio.upperLayerIn; +} +network InterframeNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + receiver: WirelessHost; + driverNode: DriverNode; + jammerNode: JammerNode; + recipientProbe: InterframeRecipient; +} + +%inifile: omnetpp.ini +[General] +network = InterframeNetwork +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.driverNode.driver.scenario = ${scenario=0..13} +*.radioMedium.propagation.typename = "ConstantTimePropagation" +*.radioMedium.propagation.propagationTime = (${scenario} == 13 ? 100us : 0s) + ${propagation=0s,5us} +*.radioMedium.mediumLimitCache.maxTransmissionDuration = 1ms +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "InterframeHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +*.receiver.wlan[0].mac.hcf.busyProbe = ${scenario} == 13 +*.receiver.wlan[0].radio.bandwidth = 20MHz +*.receiver.wlan[0].radio.antenna.numAntennas = 1 +*.receiver.wlan[0].radio.receiver.sensitivity = -70dBm +*.receiver.wlan[0].radio.receiver.energyDetection = -90dBm +*.jammerNode.radio.opMode = "n(mixed-2.4Ghz)" +*.jammerNode.radio.bandwidth = 20MHz +*.jammerNode.radio.transmitter.bitrate = 6.5Mbps +*.jammerNode.radio.transmitter.power = -40dBm +*.jammerNode.radio.antenna.numAntennas = 2 +*.jammerNode.radio.antenna.mobilityModule = "^.^.mobility" +*.driverNode.radio.opMode = "n(mixed-2.4Ghz)" +*.driverNode.radio.bandwidth = 20MHz +*.driverNode.radio.transmitter.typename = "InterframeTransmitter" +*.driverNode.radio.transmitter.bitrate = 6.5Mbps +*.driverNode.radio.antenna.mobilityModule = "^.^.mobility" +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 10m +**.mobility.constraintAreaMaxY = 10m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +*.receiver.mobility.initialX = 0m +*.driverNode.mobility.initialX = 1m +*.jammerNode.mobility.initialX = 1m + +%contains: stdout +Compressed Block Ack interframe assertions passed. diff --git a/tests/module/Ieee80211CompressedBlockAckOriginator_1.test b/tests/module/Ieee80211CompressedBlockAckOriginator_1.test new file mode 100644 index 00000000000..10bac06324e --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckOriginator_1.test @@ -0,0 +1,175 @@ +%description: +Check Compressed responses through the HCF control callback and real EDCAF state. +The fixture supplies accepted agreements and numbered frames with known ACK states. +The fixture supplies peer HT capabilities; it does not test capability negotiation. +An earlier response acknowledges a sent frame outside the current BAR wait. +Later and disjoint responses release every current BAR wait for retry. +Unsent frames, frames outside the active window, other peers, and other TIDs retain their state. +Cases use ordinary sequence numbers and wrap across 4095. +Scheduling_1 separately checks shifted responses through an active frame sequence. + +%extraargs: -r 0..5 + +%file: Originator.cc +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/Rx.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet::ieee80211 { +class OriginatorAgreements : public OriginatorBlockAckAgreementHandler { + public: + OriginatorBlockAckAgreement *accept(MacAddress peer, int start) { + auto agreement = new OriginatorBlockAckAgreement(peer, 5, SequenceNumberCyclic(start), 64, false, false); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(SIMTIME_ZERO); + agreement->calculateExpirationTime(); + blockAckAgreements[{peer, 5}] = agreement; + return agreement; + } +}; +class OriginatorHcf : public Hcf { + public: + void check() { + Enter_Method("check"); + rx->setContentionBlocked(true); + const MacAddress peer("02:00:00:00:00:02"); + const MacAddress other("02:00:00:00:00:03"); + auto radio = check_and_cast(mac->gate("lowerLayerOut")->getNextGate()->getOwnerModule()); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + int start = par("start"); + int offset = par("offset"); + delete originatorBlockAckAgreementHandler; + auto agreements = new OriginatorAgreements(); + originatorBlockAckAgreementHandler = agreements; + auto agreement = agreements->accept(peer, start); + auto edcaf = edca->getEdcaf(AC_VI); + auto frames = edcaf->getInProgressFrames(); + auto ack = edcaf->getAckHandler(); + auto add = [&](MacAddress receiver, Tid tid, int position, bool sent) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setReceiverAddress(receiver); + header->setTransmitterAddress(mac->getAddress()); + header->setTid(tid); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(start) + position); + auto packet = new Packet("supplied-data", header); + packet->insertAtBack(makeShared()); + frames->adoptPreparedFrame(packet); + if (sent) + ack->processTransmittedDataOrMgmtFrame(header); + return packet; + }; + auto earlier = add(peer, 5, 2, true); + auto earlierHeader = earlier->peekAtFront(); + auto requested = add(peer, 5, 4, true); + auto requestedHeader = requested->peekAtFront(); + auto unsent = add(peer, 5, 5, false); + auto outside = add(peer, 5, 64, true); + auto otherTid = add(peer, 6, 2, true); + auto otherPeer = add(other, 5, 2, true); + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + ack->processTransmittedBlockAckReq(request, agreement); + ASSERT(ack->getQoSDataAckStatus(earlierHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(ack->getQoSDataAckStatus(requestedHeader) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setReceiverAddress(mac->getAddress()); + response->setTidInfo(5); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + offset); + BitVector bits; + for (int bit = 0; bit < 64; bit++) + bits.appendBit((offset == 0 && (bit == 2 || bit == 5)) || (offset == 6 && bit == 58) || offset == 100); + response->setBlockAckBitmap(bits); + auto packet = new Packet("supplied-response", response); + packet->insertAtBack(makeShared()); + originatorProcessReceivedControlFrame(packet, response, nullptr, request, AC_VI); + delete packet; + ASSERT(frames->containsFrame(earlier) == (offset != 0)); + if (offset != 0) + ASSERT(ack->getQoSDataAckStatus(earlierHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(frames->containsFrame(requested)); + ASSERT(ack->isEligibleToTransmit(requestedHeader)); + ASSERT(frames->containsFrame(unsent)); + ASSERT(ack->getQoSDataAckStatus(unsent->peekAtFront()) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + for (auto untouched : {outside, otherTid, otherPeer}) { + ASSERT(frames->containsFrame(untouched)); + ASSERT(ack->getQoSDataAckStatus(untouched->peekAtFront()) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + } + ASSERT(frames->getLength() == (offset == 0 ? 5 : 6)); + std::cout << "Compressed HCF originator assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(OriginatorHcf); +class OriginatorTest : public cSimpleModule { + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("check")); } + void handleMessage(cMessage *message) override { + delete message; + check_and_cast(getModuleByPath("^.host.wlan[0].mac.hcf"))->check(); + } +}; +Define_Module(OriginatorTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module OriginatorHcf extends Hcf { + parameters: + int start; + int offset; + @class(::inet::ieee80211::OriginatorHcf); +} +simple OriginatorTest { parameters: @class(::inet::ieee80211::OriginatorTest); } +network OriginatorNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: WirelessHost; + test: OriginatorTest; +} + +%inifile: omnetpp.ini +[General] +network = OriginatorNetwork +ned-path = .;../../../../src +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.wlan[*].agent.typename = "" +**.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 6Mbps +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].mac.qosStation = true +**.wlan[*].mac.hcf.typename = "OriginatorHcf" +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.start = ${start=100,4094} +**.wlan[*].mac.hcf.offset = ${offset=0,6,100} +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +Compressed HCF originator assertions passed. diff --git a/tests/module/Ieee80211HcfMultipleProtection_2.test b/tests/module/Ieee80211HcfMultipleProtection_2.test index 1d278478756..bd2ef75b3d2 100644 --- a/tests/module/Ieee80211HcfMultipleProtection_2.test +++ b/tests/module/Ieee80211HcfMultipleProtection_2.test @@ -2,8 +2,11 @@ Exercise the existing immediate Basic Block Ack exchange through HCF. Check zero-limit RTS estimates with an absent agreement, a Block Ack agreement, AARF query effects, and a deterministic rate decrease. Check response conversion at Tx. +Retain all seven existing scenarios. Add Compressed exchanges with positive and zero TXOP limits. +Repeat negative and fractional Duration/ID conversion with the 32-byte Compressed response. +The Compressed cases supply valid HT peer state and accepted agreements where the exchange requires them. -%extraargs: -r 0..6 +%extraargs: -r 0..9 %file: ProtectionRates.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -16,6 +19,8 @@ AARF query effects, and a deterministic rate decrease. Check response conversion #include "inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class RateProtectionRx : public Rx { @@ -41,14 +46,17 @@ class SeededOriginator : public OriginatorBlockAckAgreementHandler { }; class SeededRecipient : public RecipientBlockAckAgreementHandler { public: - void seed(const MacAddress& peer) { + void seed(const MacAddress& peer, bool compressed = false) { auto request = makeShared(); request->setTransmitterAddress(peer); request->setTid(5); request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setBufferSize(64); request->setBlockAckTimeoutValue(0); - addAgreement(request)->addbaResposneSent(); + auto agreement = addAgreement(request); + if (compressed) + agreement->setVariant(BlockAckVariant::COMPRESSED); + agreement->addbaResposneSent(); } }; @@ -88,6 +96,8 @@ class RateProtectionMac : public Ieee80211Mac { std::vector ends; std::vector durations; std::vector rates; + std::vector lengths; + std::vector compressedFrames; bool suppressed = false; void sendDownFrame(Packet *packet) override { auto header = packet->peekAtFront(); @@ -97,6 +107,9 @@ class RateProtectionMac : public Ieee80211Mac { ends.push_back(simTime() + mode->getDuration(packet->getDataLength())); durations.push_back(header->getDurationField()); rates.push_back(mode->getDataMode()->getNetBitrate()); + lengths.push_back(packet->getDataLength()); + compressedFrames.push_back(bool(dynamicPtrCast(header)) || + bool(dynamicPtrCast(header))); Ieee80211Mac::sendDownFrame(packet); } protected: @@ -126,11 +139,21 @@ class RateProtectionHcf : public Hcf { handler->seed(MacAddress("02:00:00:00:00:02")); originatorBlockAckAgreementHandler = handler; } - void seedRecipient() { + void seedHtPeer(MacAddress peer) { + Enter_Method("seedHtPeer"); + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + auto radio = check_and_cast(getParentModule()->getParentModule()->getSubmodule("radio")); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + ASSERT(mib->findPeerHtState(peer) && mib->findPeerHtState(peer)->valid); + } + void seedRecipient(bool compressed = false) { Enter_Method("seedRecipient"); delete recipientBlockAckAgreementHandler; auto handler = new SeededRecipient(); - handler->seed(MacAddress("02:00:00:00:00:01")); + handler->seed(MacAddress("02:00:00:00:00:01"), compressed); recipientBlockAckAgreementHandler = handler; } void inject(int payload = 66, bool fragment = false) { @@ -148,21 +171,40 @@ class RateProtectionHcf : public Hcf { packet->insertAtBack(makeShared()); processUpperFrame(packet, header); } - void sendSyntheticBlockAck(simtime_t calculatedField) { + void sendSyntheticBlockAck(simtime_t calculatedField, bool compressed = false) { Enter_Method("sendSyntheticBlockAck"); - auto request = makeShared(); + Ptr request = compressed ? Ptr(makeShared()) : + Ptr(makeShared()); + if (compressed) { + auto bar = dynamicPtrCast(request); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + } request->setTransmitterAddress(MacAddress("02:00:00:00:00:07")); request->setReceiverAddress(mac->getAddress()); auto received = new Packet("synthetic-bar", request); auto mode = modeSet->getMode(bps(6000000)); setFrameMode(received, request, mode); - auto updatedRequest = received->removeAtFront(); - updatedRequest->setDurationField(calculatedField + modeSet->getSifsTime() + mode->getDuration(LENGTH_BASIC_BLOCKACK)); + auto updatedRequest = received->removeAtFront(); + updatedRequest->setDurationField(calculatedField + modeSet->getSifsTime() + + mode->getDuration(compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK)); received->insertAtFront(updatedRequest); request = updatedRequest; - auto response = makeShared(); + Ptr response; + if (compressed) { + auto ba = makeShared(); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(0)); + ba->setBlockAckBitmap(BitVector(0u, 64)); + response = ba; + } + else + response = makeShared(); response->setReceiverAddress(request->getTransmitterAddress()); - response->setDurationField(recipientAckPolicy->computeBasicBlockAckDurationField(received, request)); + response->setDurationField(compressed ? recipientAckPolicy->computeCompressedBlockAckDurationField(received, + dynamicPtrCast(request)) : + recipientAckPolicy->computeBasicBlockAckDurationField(received, + dynamicPtrCast(request))); auto packet = new Packet("synthetic-block-ack", response); Hcf::transmitControlResponseFrame(packet, response, received, request); delete received; @@ -199,17 +241,25 @@ class RateProtectionTest : public cSimpleModule { auto receiver = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac.hcf")); auto senderMac = check_and_cast(getModuleByPath("^.sender.wlan[0].mac")); auto receiverMac = check_and_cast(getModuleByPath("^.receiver.wlan[0].mac")); - int scenario = senderMac->par("scenario"); + int configuredScenario = senderMac->par("scenario"); + bool compressed = configuredScenario >= 7; + int scenario = configuredScenario == 7 ? 0 : configuredScenario == 8 ? 1 : configuredScenario == 9 ? 5 : configuredScenario; + if (compressed) { + sender->seedHtPeer(MacAddress("02:00:00:00:00:02")); + receiver->seedHtPeer(MacAddress("02:00:00:00:00:01")); + } if (scenario == 0 || scenario == 1) { sender->seedOriginator(); - receiver->seedRecipient(); + receiver->seedRecipient(compressed); } if (scenario == 2 || scenario == 3) check_and_cast(getModuleByPath("^.sender.wlan[0].mac.hcf.rateControl"))->seedPeer(); if (scenario == 5) { - receiver->sendSyntheticBlockAck(SimTime(-10, SIMTIME_US)); + if (compressed) + receiver->seedHtPeer(MacAddress("02:00:00:00:00:07")); + receiver->sendSyntheticBlockAck(SimTime(-10, SIMTIME_US), compressed); wait(SimTime(300, SIMTIME_US)); - receiver->sendSyntheticBlockAck(SimTime(31.2e-6)); + receiver->sendSyntheticBlockAck(SimTime(31.2e-6), compressed); } else { sender->inject(66, scenario == 6); @@ -223,6 +273,19 @@ class RateProtectionTest : public cSimpleModule { ASSERT(sender->managementAcknowledgments == 1); ASSERT(std::find(senderMac->types.begin(), senderMac->types.end(), ST_BLOCKACK_REQ) != senderMac->types.end()); ASSERT(receiverMac->types[0] == ST_CTS); + if (compressed) { + for (size_t i = 0; i < senderMac->types.size(); i++) + if (senderMac->types[i] == ST_BLOCKACK_REQ) { + ASSERT(senderMac->compressedFrames[i]); + ASSERT(senderMac->lengths[i] == B(24)); + } + for (size_t i = 0; i < receiverMac->types.size(); i++) + if (receiverMac->types[i] == ST_BLOCKACK) { + ASSERT(receiverMac->compressedFrames[i]); + ASSERT(receiverMac->lengths[i] == B(32)); + ASSERT(receiverMac->rates[i] == bps(6000000)); + } + } auto firstRtsEndpoint = senderMac->ends[0] + senderMac->durations[0]; if (scenario == 1) { ASSERT(firstRtsEndpoint - senderMac->starts[0] == SimTime(280, SIMTIME_US)); @@ -272,6 +335,12 @@ class RateProtectionTest : public cSimpleModule { ASSERT(receiverMac->types.size() == 2); ASSERT(receiverMac->durations[0] == 0); ASSERT(receiverMac->durations[1] == SimTime(32, SIMTIME_US)); + if (compressed) + for (size_t i = 0; i < 2; i++) { + ASSERT(receiverMac->compressedFrames[i]); + ASSERT(receiverMac->lengths[i] == B(32)); + ASSERT(receiverMac->rates[i] == bps(6000000)); + } } if (scenario == 6) { ASSERT(senderMac->types.size() == 2); @@ -279,7 +348,10 @@ class RateProtectionTest : public cSimpleModule { ASSERT(senderMac->ends[0] + senderMac->durations[0] - senderMac->starts[0] == SimTime(340, SIMTIME_US)); ASSERT(sender->acknowledgments == 1); } - std::cout << "Basic Block Ack, response conversion, and rate estimates passed.\n"; + if (compressed) + std::cout << "Compressed Block Ack protection and response duration checks passed.\n"; + else + std::cout << "Basic Block Ack, response conversion, and rate estimates passed.\n"; } }; Define_Module(RateProtectionTest); @@ -341,20 +413,22 @@ record-scalar-results = false *.radioMedium.propagation.propagationTime = 0s **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${scenario} >= 7 ? "ac" : "a" +**.bandName = ${scenario} >= 7 ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +**.centerFrequency = ${scenario} >= 7 ? 5.18GHz : 2.412GHz **.wlan[*].bitrate = 6Mbps **.wlan[*].mac.typename = "RateProtectionMac" -**.wlan[*].mac.scenario = ${scenario=0..6} +**.wlan[*].mac.scenario = ${scenario=0..9} **.wlan[*].mac.qosStation = true **.wlan[*].mac.rx.typename = "RateProtectionRx" **.wlan[*].mac.hcf.typename = "RateProtectionHcf" -**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == 0 || ${scenario} == 1 || ${scenario} == 5 -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == 0 || ${scenario} == 1 || ${scenario} == 5 || ${scenario} >= 7 +**.wlan[*].mac.hcf.blockAckVariant = ${scenario} >= 7 ? "compressed" : "basic" **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 1 **.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 4096B -*.sender.wlan[0].mac.hcf.rateControl.typename = ${rateControl="","","TraceAarf","TraceAarf","DecreaseRateControl","","" ! scenario} +*.sender.wlan[0].mac.hcf.rateControl.typename = ${rateControl="","","TraceAarf","TraceAarf","DecreaseRateControl","","","","","" ! scenario} *.sender.wlan[0].mac.hcf.rateControl.initialRate = 6Mbps *.sender.wlan[0].mac.hcf.rateControl.interval = 100us **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = ${scenario} >= 2 && ${scenario} <= 4 ? -1bps : 6Mbps @@ -365,7 +439,7 @@ record-scalar-results = false **.wlan[*].mac.hcf.rtsPolicy.rtsThreshold = 0B **.wlan[*].mac.hcf.edca.edcaf[*].cwMin = 0 **.wlan[*].mac.hcf.edca.edcaf[*].cwMax = 0 -**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 0 ? 1024us : 0s +**.wlan[*].mac.hcf.edca.edcaf[*].txopProcedure.txopLimit = ${scenario} == 0 || ${scenario} == 7 ? 1024us : 0s *.sender.wlan[0].mac.hcf.edca.edcaf[*].txopProcedure.protectionMechanism = "multiple" *.sender.wlan[0].address = "02:00:00:00:00:01" *.receiver.wlan[0].address = "02:00:00:00:00:02" @@ -386,3 +460,6 @@ record-scalar-results = false %contains: stdout Basic Block Ack, response conversion, and rate estimates passed. + +%contains: stdout +Compressed Block Ack protection and response duration checks passed. diff --git a/tests/module/Ieee80211RecipientNav_1.test b/tests/module/Ieee80211RecipientNav_1.test index c4812c86883..bb8a4008e40 100644 --- a/tests/module/Ieee80211RecipientNav_1.test +++ b/tests/module/Ieee80211RecipientNav_1.test @@ -5,8 +5,10 @@ Retain received NAV refusal and DCF local NAV updates for holder frames. Require zero HCF local NAV updates for holder frames. Check a Basic Block Ack response after an ADDBA exchange through production HCF. Require its positive Duration/ID field without a local NAV update. +Repeat the response checks with Compressed and valid supplied HT peer state. +The Compressed case uses a 32-byte response MPDU and retains the same NAV invariant. -%extraargs: -r 0..2 +%extraargs: -r 0..3 %file: RecipientNav.cc #include "inet/common/packet/chunk/ByteCountChunk.h" @@ -14,6 +16,8 @@ Require its positive Duration/ID field without a local NAV update. #include "inet/linklayer/ieee80211/mac/Rx.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" namespace inet::ieee80211 { class RecipientNavRx : public Rx { @@ -32,6 +36,9 @@ class RecipientNavMac : public Ieee80211Mac { std::vector durations; simtime_t addbaResponseEnd = -1; Ptr basicBlockAck; + Ptr compressedBlockAck; + units::values::b blockAckLength; + const physicallayer::IIeee80211Mode *blockAckMode = nullptr; void sendDownFrame(Packet *packet) override { auto header = packet->peekAtFront(); types.push_back(header->getType()); @@ -43,6 +50,11 @@ class RecipientNavMac : public Ieee80211Mac { } if (auto response = dynamicPtrCast(header)) basicBlockAck = response; + if (auto response = dynamicPtrCast(header)) { + compressedBlockAck = response; + blockAckLength = packet->getDataLength(); + blockAckMode = packet->getTag()->getMode(); + } Ieee80211Mac::sendDownFrame(packet); } void injectBlockAckSetup() { @@ -58,12 +70,33 @@ class RecipientNavMac : public Ieee80211Mac { request->setStartingSequenceNumber(SequenceNumberCyclic(0)); injectBlockAckFrame(request); } - void injectBlockAckRequest() { + void prepareCompressedPeer() { + Enter_Method("prepareCompressedPeer"); + const MacAddress peer("02:00:00:00:00:02"); + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + auto radio = check_and_cast(getParentModule()->getSubmodule("radio")); + auto transmitter = check_and_cast(radio->getTransmitter()); + auto channel = transmitter->getChannel(); + mib->setPrimaryChannel(channel->getChannelNumber(), channel->getBand()); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + ASSERT(mib->findPeerHtState(peer) && mib->findPeerHtState(peer)->valid); + } + void injectBlockAckRequest(bool compressed = false) { Enter_Method("injectBlockAckRequest"); - auto request = makeShared(); + Ptr request; + if (compressed) { + auto bar = makeShared(); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request = bar; + } + else { + auto bar = makeShared(); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request = bar; + } request->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); - request->setTidInfo(5); - request->setStartingSequenceNumber(SequenceNumberCyclic(0)); request->setDurationField(SimTime(500, SIMTIME_US)); injectBlockAckFrame(request); } @@ -77,7 +110,7 @@ class RecipientNavMac : public Ieee80211Mac { header->setReceiverAddress(getAddress()); auto packet = new Packet("received-block-ack-exchange", header); packet->addTag()->setMode( - physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000))); + physicallayer::Ieee80211ModeSet::getModeSet(mib->isHtOperationSupported() ? "n(mixed-2.4Ghz)" : "a")->getMode(bps(6000000))); auto trailer = makeShared(); trailer->setFcsMode(FCS_DECLARED_CORRECT); packet->insertAtBack(trailer); @@ -140,8 +173,10 @@ class RecipientNavTest : public cSimpleModule { public: RecipientNavTest() : cSimpleModule(65536) {} protected: - void checkBasicBlockAck(RecipientNavMac *mac, RecipientNavRx *rx) { - auto mode = physicallayer::Ieee80211ModeSet::getModeSet("a")->getMode(bps(6000000)); + void checkBlockAck(RecipientNavMac *mac, RecipientNavRx *rx, bool compressed) { + auto mode = physicallayer::Ieee80211ModeSet::getModeSet(compressed ? "n(mixed-2.4Ghz)" : "a")->getMode(bps(6000000)); + if (compressed) + mac->prepareCompressedPeer(); mac->injectBlockAckSetup(); for (int i = 0; i < 100 && mac->addbaResponseEnd < SIMTIME_ZERO; i++) wait(SimTime(10, SIMTIME_US)); @@ -154,27 +189,40 @@ class RecipientNavTest : public cSimpleModule { ASSERT(rx->isMediumFree()); auto updatesBeforeResponse = rx->localUpdates; auto framesBeforeResponse = mac->types.size(); - mac->injectBlockAckRequest(); + mac->injectBlockAckRequest(compressed); wait(SimTime(350, SIMTIME_US)); ASSERT(mac->types.size() == framesBeforeResponse + 1); - ASSERT(mac->basicBlockAck != nullptr); - ASSERT(mac->basicBlockAck->getReceiverAddress() == MacAddress("02:00:00:00:00:02")); - ASSERT(mac->basicBlockAck->getTidInfo() == 5); - ASSERT(mac->basicBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + Ptr response; + if (compressed) { + ASSERT(mac->compressedBlockAck != nullptr && mac->basicBlockAck == nullptr); + ASSERT(mac->compressedBlockAck->getTidInfo() == 5); + ASSERT(mac->compressedBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(mac->compressedBlockAck->getBlockAckBitmap().getSize() == 64); + ASSERT(mac->blockAckLength == B(32)); + ASSERT(mac->blockAckMode == mode && mac->blockAckMode->getHtMcsIndex() < 0); + response = mac->compressedBlockAck; + } + else { + ASSERT(mac->basicBlockAck != nullptr); + ASSERT(mac->basicBlockAck->getTidInfo() == 5); + ASSERT(mac->basicBlockAck->getStartingSequenceNumber() == SequenceNumberCyclic(0)); + response = mac->basicBlockAck; + } + ASSERT(response->getReceiverAddress() == MacAddress("02:00:00:00:00:02")); // IEEE Std 802.11-2024, 9.2.5.6: subtract SIFS and response airtime from the request duration. - auto expectedDuration = SimTime(500, SIMTIME_US) - mode->getSifsTime() - mode->getDuration(LENGTH_BASIC_BLOCKACK); + auto expectedDuration = SimTime(500, SIMTIME_US) - mode->getSifsTime() - mode->getDuration(compressed ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK); ASSERT(expectedDuration > SIMTIME_ZERO); - ASSERT(mac->basicBlockAck->getDurationField() == expectedDuration); + ASSERT(response->getDurationField() == expectedDuration); ASSERT(rx->localUpdates == updatesBeforeResponse); ASSERT(rx->isMediumFree()); - std::cout << "Basic Block Ack recipient NAV checks passed.\n"; + std::cout << (compressed ? "Compressed" : "Basic") << " Block Ack recipient NAV checks passed.\n"; } void activity() override { wait(SimTime(1, SIMTIME_MS)); auto mac = check_and_cast(getModuleByPath("^.host.wlan[0].mac")); auto rx = check_and_cast(mac->getSubmodule("rx")); - if (par("basicBlockAckOnly").boolValue()) { - checkBasicBlockAck(mac, rx); + if (par("basicBlockAckOnly").boolValue() || par("compressedBlockAckOnly").boolValue()) { + checkBlockAck(mac, rx, par("compressedBlockAckOnly").boolValue()); std::cout << "Recipient NAV production checks passed.\n"; return; } @@ -231,6 +279,7 @@ simple RecipientNavRx extends Rx { simple RecipientNavTest extends SimpleModule { parameters: bool basicBlockAckOnly = default(false); + bool compressedBlockAckOnly = default(false); @class(::inet::ieee80211::RecipientNavTest); } network RecipientNavNetwork { @@ -251,14 +300,15 @@ record-vector-results = false record-scalar-results = false **.wlan[*].agent.typename = "" **.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" -**.wlan[*].opMode = "a" +**.wlan[*].opMode = ${scenario} == "compressedBlockAck" ? "n(mixed-2.4Ghz)" : "a" **.wlan[*].bitrate = 6Mbps **.wlan[*].mac.typename = "RecipientNavMac" -**.wlan[*].mac.qosStation = ${scenario="hcf","dcf","blockAck"} != "dcf" +**.wlan[*].mac.qosStation = ${scenario="hcf","dcf","blockAck","compressedBlockAck"} != "dcf" **.wlan[*].mac.rx.typename = "RecipientNavRx" -**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == "blockAck" -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} == "blockAck" || ${scenario} == "compressedBlockAck" +**.wlan[*].mac.hcf.blockAckVariant = ${scenario} == "compressedBlockAck" ? "compressed" : "basic" *.test.basicBlockAckOnly = ${scenario} == "blockAck" +*.test.compressedBlockAckOnly = ${scenario} == "compressedBlockAck" **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.originatorMacDataService.mpduAggregationPolicy.typename = "" **.wlan[*].mac.hcf.rateSelection.dataFrameBitrate = 6Mbps @@ -287,3 +337,6 @@ Recipient NAV production checks passed. %contains: stdout Basic Block Ack recipient NAV checks passed. + +%contains: stdout +Compressed Block Ack recipient NAV checks passed. diff --git a/tests/protocol/wifi/11ac/Ac_BlockAck.test b/tests/protocol/wifi/11ac/Ac_BlockAck.test index eeaa886371b..cd274cf2543 100644 --- a/tests/protocol/wifi/11ac/Ac_BlockAck.test +++ b/tests/protocol/wifi/11ac/Ac_BlockAck.test @@ -1,13 +1,19 @@ %description: 802.11ac Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] +Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. + +%extraargs: -r 0..1 %file: Ac_BlockAck.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { +using namespace inet::ieee80211; + // Block Ack session on 11ac: STA sends ADDBA Request (type 13), // then BlockAckReq (type 24), AP replies with BlockAck (type 25). Define_ProtocolTest(wifi_11ac_block_ack) @@ -18,21 +24,63 @@ Define_ProtocolTest(wifi_11ac_block_ack) .describe("ADDBA action frame from sta1").within(2.5)) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) .describe("BlockAckReq from sta1").within(1.0)) .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (int i = 0; i < 64; i++) + for (unsigned int j = 0; j < ba->getBlockAckBitmap(i).getSize(); j++) + if (ba->getBlockAckBitmap(i).getBit(j)) return true; + return false; + }) .describe("BlockAck received at sta1").within(0.02)); } +Define_ProtocolTest(wifi_11ac_compressed_exchange) +{ + return ProtocolTest("wifi_11ac_compressed_exchange") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 13") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return response && response->getStatusCode() == 0 && response->getBlockAckPolicy(); + }) + .describe("successful immediate ADDBA response").within(2.5)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) + .describe("Compressed BAR from sta1").within(1.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (unsigned int i = 0; i < ba->getBlockAckBitmap().getSize(); i++) + if (ba->getBlockAckBitmap().getBit(i)) return true; + return false; + }) + .describe("Compressed Block Ack with positive receipt evidence").within(0.02)); +} + } // namespace protocoltest } // namespace inet %inifile: test.ini [General] -*.tester.testName = "wifi_11ac_block_ack" +*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -50,3 +98,6 @@ include ../../ini/_base.ini %contains: stdout PROTOCOLTEST wifi_11ac_block_ack: PASS + +%contains: stdout +PROTOCOLTEST wifi_11ac_compressed_exchange: PASS diff --git a/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test b/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test new file mode 100644 index 00000000000..dd86abc74e5 --- /dev/null +++ b/tests/protocol/wifi/11ac/Ac_BlockAckFallback.test @@ -0,0 +1,186 @@ +%description: +Check runtime Compressed fallback under a controlled peer HT capability gap. +Simplified management supplies static association and valid HT state after initialization. +Before DATA, the fixture removes peer HT records at both ends of both wireless hops. +The fixture retains association, local HT support, ac operation, and Compressed Block Ack support. +During traffic, require delivered Normal Ack QoS DATA without ADDBA, BAR, or Block Ack. +Check absent peer HT records during traffic and at the final audit. +This fixture does not establish an on-air association exchange or a limitation of all ac stations. + +%file: Ac_BlockAckFallback.cc +#include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +namespace inet::protocoltest { +using namespace inet::ieee80211; + +class FallbackHcf : public Hcf { + public: + bool hasNoAgreements() const { + return originatorBlockAckAgreementHandler->getAgreements().empty() && + recipientBlockAckAgreementHandler->getAgreements().empty(); + } +}; +Define_Module(FallbackHcf); + +class BlockAckFallbackAudit : public cSimpleModule, public cListener { + protected: + Ieee80211Mib *ap = nullptr; + Ieee80211Mib *sta1 = nullptr; + Ieee80211Mib *sta2 = nullptr; + std::vector hcfs; + std::map macs; + bool removed = false; + bool completed = false; + int delivered = 0; + int sentFirstHop = 0; + int receivedFirstHop = 0; + int sentSecondHop = 0; + int receivedSecondHop = 0; + simsignal_t sentSignal; + simsignal_t receivedSignal; + simsignal_t appSignal; + + void checkAssociation() const { + for (auto sta : {sta1, sta2}) { + ASSERT(sta->bssStationData.isAssociated); + ASSERT(sta->bssData.bssid == ap->address); + auto it = ap->bssAccessPointData.stations.find(sta->address); + ASSERT(it != ap->bssAccessPointData.stations.end() && it->second == Ieee80211Mib::ASSOCIATED); + } + for (auto mib : {ap, sta1, sta2}) + ASSERT(mib->isHtOperationSupported() && mib->localHtCapabilitiesValid); + for (auto hcf : hcfs) { + ASSERT(hcf->par("isBlockAckSupported").boolValue()); + ASSERT(std::string(hcf->par("blockAckVariant").stringValue()) == "compressed"); + ASSERT(hcf->hasNoAgreements()); + } + } + void checkGap() const { + checkAssociation(); + ASSERT(ap->findPeerHtState(sta1->address) == nullptr); + ASSERT(ap->findPeerHtState(sta2->address) == nullptr); + ASSERT(sta1->findPeerHtState(ap->address) == nullptr); + ASSERT(sta2->findPeerHtState(ap->address) == nullptr); + } + void initialize() override { + sentSignal = registerSignal("packetSentToLower"); + receivedSignal = registerSignal("packetReceivedFromLower"); + appSignal = registerSignal("packetReceived"); + scheduleAt(SIMTIME_ZERO, new cMessage("remove")); + scheduleAt(SimTime(1100, SIMTIME_MS), new cMessage("audit")); + } + void handleMessage(cMessage *message) override { + bool remove = message->isName("remove"); + delete message; + if (remove) { + ap = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + sta1 = check_and_cast(getModuleByPath("^.sta1.wlan[0].mib")); + sta2 = check_and_cast(getModuleByPath("^.sta2.wlan[0].mib")); + for (const char *name : {"ap", "sta1", "sta2"}) { + auto wlan = getParentModule()->getSubmodule(name)->getSubmodule("wlan", 0); + ASSERT(std::string(wlan->par("opMode").stringValue()) == "ac"); + auto mac = check_and_cast(wlan->getSubmodule("mac")); + macs[name] = mac; + hcfs.push_back(check_and_cast(mac->getSubmodule("hcf"))); + mac->subscribe(sentSignal, this); + mac->subscribe(receivedSignal, this); + } + checkAssociation(); + for (auto sta : {sta1, sta2}) { + auto atAp = ap->findPeerHtState(sta->address); + auto atSta = sta->findPeerHtState(ap->address); + ASSERT(atAp && atAp->valid && atSta && atSta->valid); + } + for (auto sta : {sta1, sta2}) { + ap->removePeerHtCapabilities(sta->address); + sta->removePeerHtCapabilities(ap->address); + } + getModuleByPath("^.sta2.app[0]")->subscribe(appSignal, this); + removed = true; + checkGap(); + } + else { + ASSERT(removed); + checkGap(); + ASSERT(delivered >= 5); + ASSERT(sentFirstHop >= 5 && receivedFirstHop >= 5); + ASSERT(sentSecondHop >= 5 && receivedSecondHop >= 5); + completed = true; + std::cout << "Absent peer HT fallback: Normal Ack delivered=" << delivered + << " firstHop=" << receivedFirstHop << " secondHop=" << receivedSecondHop << "\n"; + emit(registerSignal("fallbackAuditPassed"), intval_t(1)); + } + } + void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *) override { + ASSERT(removed); + checkGap(); + auto packet = check_and_cast(object); + if (signal == appSignal) { + delivered++; + return; + } + auto header = packet->peekAtFront(); + ASSERT(!dynamicPtrCast(header)); + ASSERT(!dynamicPtrCast(header)); + ASSERT(header->getType() != ST_BLOCKACK_REQ && header->getType() != ST_BLOCKACK); + auto data = dynamicPtrCast(header); + if (!data) + return; + ASSERT(data->getType() == ST_DATA_WITH_QOS && data->getAckPolicy() == NORMAL_ACK); + bool firstHop = data->getTransmitterAddress() == sta1->address && data->getReceiverAddress() == ap->address; + bool secondHop = data->getTransmitterAddress() == ap->address && data->getReceiverAddress() == sta2->address; + if (signal == sentSignal) { + sentFirstHop += firstHop && source == macs.at("sta1"); + sentSecondHop += secondHop && source == macs.at("ap"); + } + else if (signal == receivedSignal) { + receivedFirstHop += firstHop && source == macs.at("ap"); + receivedSecondHop += secondHop && source == macs.at("sta2"); + } + } + void finish() override { ASSERT(completed); } +}; +Define_Module(BlockAckFallbackAudit); + +Define_ProtocolTest(wifi_11ac_blockack_fallback) +{ + return ProtocolTest("wifi_11ac_blockack_fallback") + .once(on("audit").signal("fallbackAuditPassed") + .describe("Normal Ack delivery with absent peer HT state on both hops").within(1.2)); +} +} + +%file: FallbackNetwork.ned +import inet.protocoltest.wifi.WifiInfraNetwork; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +module FallbackHcf extends Hcf { parameters: @class(::inet::protocoltest::FallbackHcf); } +simple BlockAckFallbackAudit { parameters: @class(::inet::protocoltest::BlockAckFallbackAudit); } +network FallbackNetwork extends WifiInfraNetwork { + submodules: + audit: BlockAckFallbackAudit; +} + +%inifile: test.ini +[General] +network = FallbackNetwork +*.tester.testName = "wifi_11ac_blockack_fallback" +*.ap.wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified" +*.sta*.wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified" +*.sta*.wlan[*].agent.typename = "" +*.sta*.wlan[*].mgmt.accessPointAddress = "ap" +**.wlan[*].mac.hcf.typename = "FallbackHcf" +**.wlan[*].mac.hcf.isBlockAckSupported = true +**.wlan[*].mac.hcf.blockAckVariant = "compressed" +*.sta1.app[0].sendInterval = 0.1ms +*.sta1.app[0].stopTime = 1.01s +include ../../ini/_ac.ini +include ../../ini/_base.ini +**.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 10000B +**.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 100 +**.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 + +%contains: stdout +PROTOCOLTEST wifi_11ac_blockack_fallback: PASS diff --git a/tests/protocol/wifi/11n/N_BlockAck.test b/tests/protocol/wifi/11n/N_BlockAck.test index d5ab66019fe..98f60e3b1d7 100644 --- a/tests/protocol/wifi/11n/N_BlockAck.test +++ b/tests/protocol/wifi/11n/N_BlockAck.test @@ -1,13 +1,19 @@ %description: 802.11n Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] +Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. + +%extraargs: -r 0..1 %file: N_BlockAck.cc #include "ProtocolTest.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { namespace protocoltest { +using namespace inet::ieee80211; + // Block Ack session flow: STA sends ADDBA Request (action frame, type 13), // AP responds with ADDBA Response (type 13), then after enough frames are // queued, STA sends BlockAckReq (type 24) and AP sends BlockAck (type 25). @@ -19,21 +25,63 @@ Define_ProtocolTest(wifi_11n_block_ack) .describe("ADDBA action frame from sta1").within(2.5)) .once(on("sta1.wlan[0].mac").signal("packetSentToLower") .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) .describe("BlockAckReq from sta1").within(1.0)) .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (int i = 0; i < 64; i++) + for (unsigned int j = 0; j < ba->getBlockAckBitmap(i).getSize(); j++) + if (ba->getBlockAckBitmap(i).getBit(j)) return true; + return false; + }) .describe("BlockAck received at sta1").within(0.02)); } +Define_ProtocolTest(wifi_11n_compressed_exchange) +{ + return ProtocolTest("wifi_11n_compressed_exchange") + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 13") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto response = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return response && response->getStatusCode() == 0 && response->getBlockAckPolicy(); + }) + .describe("successful immediate ADDBA response").within(2.5)) + .once(on("sta1.wlan[0].mac").signal("packetSentToLower") + .filterPacket("ieee80211mac.type == 24") + .filterEvent([](const MatchContext& c) { + return c.event.packet && c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + }) + .describe("Compressed BAR from sta1").within(1.0)) + .once(on("sta1.wlan[0].mac").signal("packetReceivedFromLower") + .filterPacket("ieee80211mac.type == 25") + .filterEvent([](const MatchContext& c) { + if (!c.event.packet) return false; + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + if (!ba) return false; + for (unsigned int i = 0; i < ba->getBlockAckBitmap().getSize(); i++) + if (ba->getBlockAckBitmap().getBit(i)) return true; + return false; + }) + .describe("Compressed Block Ack with positive receipt evidence").within(0.02)); +} + } // namespace protocoltest } // namespace inet %inifile: test.ini [General] -*.tester.testName = "wifi_11n_block_ack" +*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -51,3 +99,6 @@ include ../../ini/_base.ini %contains: stdout PROTOCOLTEST wifi_11n_block_ack: PASS + +%contains: stdout +PROTOCOLTEST wifi_11n_compressed_exchange: PASS diff --git a/tests/protocol/wifi/11n/N_CompressedBa.test b/tests/protocol/wifi/11n/N_CompressedBa.test index 8d7910bb180..ffc72bf4215 100644 --- a/tests/protocol/wifi/11n/N_CompressedBa.test +++ b/tests/protocol/wifi/11n/N_CompressedBa.test @@ -1,5 +1,5 @@ %description: -802.11n Block Ack received at sta1 with non-empty bitmap (acknowledges at least one frame). [Required] +802.11n Compressed Block Ack received at sta1 with a positive receipt bit. [Required] %file: N_CompressedBa.cc @@ -11,10 +11,6 @@ namespace protocoltest { using namespace inet::ieee80211; -// INET sends Ieee80211BasicBlockAck (not CompressedBlockAck) for all block ack sessions. -// The Basic Block Ack carries a 64x16-bit bitmap acknowledging MPDUs. -// We assert that a BasicBlockAck is received and its bitmap has at least one bit set, -// confirming the AP has positively acknowledged at least one received data MPDU. Define_ProtocolTest(wifi_11n_compressed_ba) { return ProtocolTest("wifi_11n_compressed_ba") @@ -22,18 +18,14 @@ Define_ProtocolTest(wifi_11n_compressed_ba) .filterPacket("ieee80211mac.type == 25") .filterEvent([](const MatchContext& c) { if (!c.event.packet) return false; - auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + auto ba = c.event.packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); if (!ba) return false; - // At least one bit set means at least one MPDU was acknowledged. - for (int i = 0; i < 64; i++) { - const auto& bv = ba->getBlockAckBitmap(i); - for (unsigned int j = 0; j < bv.getSize(); j++) { - if (bv.getBit(j)) return true; - } - } + const auto& bitmap = ba->getBlockAckBitmap(); + for (unsigned int i = 0; i < bitmap.getSize(); i++) + if (bitmap.getBit(i)) return true; return false; }) - .describe("a BasicBlockAck with at least one acknowledged MPDU").within(2.5)); + .describe("a Compressed Block Ack with a positive receipt bit").within(2.5)); } } // namespace protocoltest @@ -44,7 +36,7 @@ Define_ProtocolTest(wifi_11n_compressed_ba) [General] *.tester.testName = "wifi_11n_compressed_ba" *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = "basic" +**.wlan[*].mac.hcf.blockAckVariant = "compressed" *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms diff --git a/tests/unit/Ieee80211BlockAckScheduling_1.test b/tests/unit/Ieee80211BlockAckScheduling_1.test index aeb1b29a0f3..ce6049d666d 100644 --- a/tests/unit/Ieee80211BlockAckScheduling_1.test +++ b/tests/unit/Ieee80211BlockAckScheduling_1.test @@ -149,6 +149,48 @@ choice = policy.computeBlockAckReqParameters(&frames, nullptr, &agreements); ASSERT(std::get<0>(choice) == peer && std::get<2>(choice) == 2); ASSERT(std::get<1>(choice) == SequenceNumberCyclic(100)); ASSERT(otherAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(4094)); +for (int start : {100, 4094}) { + for (int responseOffset : {0, 6, 100}) { + QosAckHandler compressedAck; + SchedulingFrames compressedFrames; + compressedFrames.configure(&compressedAck); + OriginatorBlockAckAgreement active(peer, 5, SequenceNumberCyclic(start), 64, false, false); + auto earlier = compressedFrames.add(peer, 5, (start + 2) % 4096); + auto requested = compressedFrames.add(peer, 5, (start + 4) % 4096); + auto unsent = compressedFrames.add(peer, 5, (start + 5) % 4096, false); + auto outsideWindow = compressedFrames.add(peer, 5, (start + 64) % 4096); + auto otherTid = compressedFrames.add(peer, 2, (start + 2) % 4096); + auto request = makeShared(); + request->setReceiverAddress(peer); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + compressedAck.processTransmittedBlockAckReq(request, &active); + ASSERT(compressedAck.getQoSDataAckStatus(earlier) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(compressedAck.getQoSDataAckStatus(requested) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + auto response = makeShared(); + response->setTransmitterAddress(peer); + response->setTidInfo(5); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + responseOffset); + BitVector bitmap; + for (int bit = 0; bit < 64; bit++) + bitmap.appendBit(responseOffset == 0 && (bit == 2 || bit == 5)); + response->setBlockAckBitmap(bitmap); + compressedAck.processReceivedCompressedBlockAck(response, &active); + ASSERT(compressedAck.getQoSDataAckStatus(earlier) == (responseOffset == 0 ? + QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + ASSERT(compressedAck.isEligibleToTransmit(requested)); + ASSERT(compressedAck.getQoSDataAckStatus(unsent) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(compressedAck.getQoSDataAckStatus(otherTid) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(compressedAck.getQoSDataAckStatus(outsideWindow) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + response->setStartingSequenceNumber(SequenceNumberCyclic(start) + 4); + BitVector outsideBitmap; + for (int bit = 0; bit < 64; bit++) + outsideBitmap.appendBit(bit == 60); + response->setBlockAckBitmap(outsideBitmap); + compressedAck.processReceivedCompressedBlockAck(response, &active); + ASSERT(compressedAck.getQoSDataAckStatus(outsideWindow) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + } +} auto duration = SimTime(1, SIMTIME_US); EdcaTransmitLifetimeHandler lifetime(duration, duration, duration, duration); frames.setLifetimeHandler(&lifetime); From 1dd4ca23785b14b058693e9e20434a4257c3831f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 17 Oct 2026 22:52:08 +0200 Subject: [PATCH 17/17] ieee80211: change: default HCF to immediate Compressed Block Ack Before this commit, a configuration without a profile selector uses Basic. The complete Compressed runtime requires an explicit selector. After this commit, HCF selects Compressed by default. Eligible HT links use immediate Compressed exchanges. Links without usable local or peer HT state retain Normal Ack. Explicit Basic retains its historical compatibility path and fragment bitmap. For example, an eligible n link without blockAckVariant now uses Compressed. The n and ac consumers cover explicit Basic, explicit Compressed, and the default. Migration cases cover capability fallback, profile conflict, and unsupported delayed policy. Existing Basic demonstrations retain explicit selectors. Execution records retain supported scope and evidence limits. A-MPDU, Multi-TID Block Ack, delayed Compressed policy, complete historical Basic conformance, and complete VHT transfer remain outside this result. Admission limits outside active Compressed agreements also remain outside the correction. Plan: plan/pending/ieee80211-compressed-blockack-replacement.md Change: src.ieee80211 | behavior.change | test whatsnew migration | ieee80211-compressed-blockack-replacement --- WHATSNEW | 2 +- doc/src/migration-guide/index.rst | 2 +- doc/src/users-guide/ch-80211.rst | 2 +- ...e80211-compressed-blockack-s5-execution.md | 227 ++++++++++++++++++ ...0211-compressed-blockack-txop-admission.md | 121 ++++++++++ showcases/wireless/blockack/doc/index.rst | 2 +- .../mac/coordinationfunction/Hcf.ned | 2 +- tests/module/Ieee80211BlockAckDeferral_1.test | 1 + ...Ieee80211CompressedBlockAckExchange_1.test | 1 - ...eee80211CompressedBlockAckMigration_1.test | 179 ++++++++++++++ tests/protocol/wifi/11ac/Ac_BlockAck.test | 25 +- tests/protocol/wifi/11n/N_BlockAck.test | 25 +- 12 files changed, 575 insertions(+), 14 deletions(-) create mode 100644 plan/pending/ieee80211-compressed-blockack-s5-execution.md create mode 100644 plan/pending/ieee80211-compressed-blockack-txop-admission.md create mode 100644 tests/module/Ieee80211CompressedBlockAckMigration_1.test diff --git a/WHATSNEW b/WHATSNEW index a0646fd959e..8cebaec1905 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -456,7 +456,7 @@ Notable backward incompatible changes are the following: 52. IEEE 802.11 immediate Compressed Block Ack - HCF supports explicit immediate Compressed Block Ack. Basic remains the default. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. + HCF defaults to immediate Compressed Block Ack. Eligible HT infrastructure links and the supported ac path use accepted agreements and 32-byte Compressed responses with FCS. Links without usable local or peer HT state retain Normal Ack. Select blockAckVariant = "basic" at both endpoints for historical Basic compatibility. An HT PHY mode alone does not supply peer capability state. Both endpoints must select the same blockAckVariant because ADDBA does not negotiate it. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, and profiles. Compressed rejects delayedAckPolicySupported = true in either agreement policy at initialization. diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 49984769ef5..8a171ab7075 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -46,7 +46,7 @@ Custom QoS data services must implement the reversible preparation operations in IEEE 802.11 Compressed Block Ack extension points ------------------------------------------------ -Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. +HCF defaults to ``mac.hcf.blockAckVariant = "compressed"``. Both endpoints must select the same profile. ADDBA does not negotiate this selector. A valid addressed BAR with the other profile stops the simulation before agreement updates. The diagnostic identifies the peer, TID, local profile, and received profile. Configure the same selectors before a new run. Compressed agreements require usable local HT support and directional peer HT state. An HT PHY mode alone does not supply that state. Without it, HCF retains Normal Ack DATA. Explicit ``blockAckVariant = "basic"`` retains historical Basic compatibility without that HT requirement. Compressed agreements suppress fragmentation for their peer and TID. Fragmentation outside those agreements retains its existing threshold. diff --git a/doc/src/users-guide/ch-80211.rst b/doc/src/users-guide/ch-80211.rst index e6f09b57bd6..fa35d64e63b 100644 --- a/doc/src/users-guide/ch-80211.rst +++ b/doc/src/users-guide/ch-80211.rst @@ -706,7 +706,7 @@ After the final exchange, the originator sends DELBA when its agreement has no q The agreement policies, ``mac.hcf.originatorBlockAckAgreementPolicy`` and ``mac.hcf.recipientBlockAckAgreementPolicy``, set the buffer size. ``maximumAllowedBufferSize`` defaults to 64. Both negotiated ``aMsduSupported`` flags control A-MSDU use; each defaults to ``true``. ``blockAckTimeoutValue`` defaults to ``0s``, which disables the inactivity timeout. Transfer completion and DATA lifetime expiry retain their separate teardown rules. -Select ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Basic remains the default. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. +HCF defaults to ``mac.hcf.blockAckVariant = "compressed"`` for immediate Compressed Block Ack. Block Ack support itself remains disabled by default. The response contains a 64-bit bitmap and occupies 32 bytes with FCS. Each positive bit acknowledges one unfragmented MPDU. The recipient retains receipt bits after delivery, so a later BAR can still obtain those positive bits. Missing receipt evidence produces zero. Compressed agreements require usable local HT support and directional peer HT state. Infrastructure association supplies that peer state for eligible HT links and the supported ``ac`` path. An HT radio mode alone does not supply peer capabilities. Without usable state, DATA uses Normal Ack. Local non-HT operation produces a warning when Block Ack support is enabled. HT ad hoc links without peer state retain Normal Ack. diff --git a/plan/pending/ieee80211-compressed-blockack-s5-execution.md b/plan/pending/ieee80211-compressed-blockack-s5-execution.md new file mode 100644 index 00000000000..e89c9e74f7d --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-s5-execution.md @@ -0,0 +1,227 @@ +# S5 execution: Compressed Block Ack beside Basic compatibility + +S5 is complete through C1, C2, C3, and C4. The authorized TXOP correction removes the active Compressed timing blocker. HCF now defaults to Compressed. Explicit Basic compatibility remains available. + +The comparison parent is `75559a49661e33f727fa93a8c81c021596cfb4c9`. The active checkout is `/home/user/omnetpp_ws/inet-compressed-blockack-s5`. The maintainer requested S5 execution on 16 October 2026. That request authorizes C1 through C4 in the [replacement plan](ieee80211-compressed-blockack-replacement.md#compressed-replacement-steps). + +The checkout contained changes to `AGENTS.md` and four prerequisite plans before execution. It also contained an untracked planning history. This work preserves those changes. The pinned parent contains the S4 implementation. The [S4 execution record](ieee80211-compressed-blockack-s4-execution.md) supplies its earlier evidence and limits. + +## Implementation contract + +HCF owns the immutable profile and validates received frame formats before agreement updates. An active agreement identifies one peer and TID. A valid conflicting BAR stops the simulation because ADDBA does not negotiate the profile. Malformed input cannot cause that diagnostic, receipt updates, or agreement recovery. + +The recipient record owns explicit Compressed receipt bits and its scoreboard window. The reorder service owns delivery order and its separate window. Delivery must preserve receipt bits. An absent receipt produces zero, including positions before the first received DATA. Basic retains its fragment bitmap and existing compatibility behavior. + +The rate selector consumes the MIB rate sets. It selects a non-HT Compressed response and distinguishes initial BAR from later BAR. Frame sequences supply the actual request profile. Protection, timeouts, and duration calculations use the corresponding response length. Compressed responses contain 32 bytes with FCS; Basic responses contain 152 bytes. + +For an active Compressed agreement, the originator suppresses fragmentation for that peer and TID. The recipient rejects fragmented DATA before receipt or reorder updates. Normal Ack outside that agreement retains its existing fragmentation behavior. + +## Recipient interframe design + +The recipient must distinguish receipt evidence from the interframe relation. An agreement with start 100 and size four first receives Normal Ack DATA 100. Delivery advances the reorder start to 101. DATA 104 advances the scoreboard start to 101 and preserves receipt evidence for 100. A qualifying interframe relation permits delivery of 104 despite the gap. A false receipt bit or absent relation retains 104. + +Local receive endpoints alone cannot establish the ACK-to-DATA relation with propagation delay. The medium therefore supplies one copied snapshot of physical endpoints. The snapshot contains transmission identity, transmitter identity, transmission endpoints, and cached arrival endpoints. It retains no frame, transmission, arrival, or radio pointer. + +The PHY derives the interval at the current transmitter. For the same transmitter, it subtracts the preceding transmission end from the current transmission start. Otherwise, it subtracts the preceding signal's cached arrival end at the current transmitter. This calculation uses the propagation result that the medium already produced. It does not assume equal outward and return delay. + +HCF owns local event history. A pending candidate preserves the interval across PHY reception completion and later MAC delivery. A receiver-created transmission identifier connects those events. Unrelated receptions replace prior history. Failed, abandoned, and overlapping receptions must not preserve stale eligibility. Every local transmission clears prior history; completed ACK transmission can establish new history. + +SIFS recognition uses the reconstructed interval after signal extension. The current PHY model uses baseline timing without coverage-class adjustment. The permitted bound is one tenth of the slot time. Integer tick division rounds that bound inward. Physical propagation remains separate from this MAC timing parameter. + +RIFS recognition requires permitted HT operation, the same transmitter and RA, and no preceding SIFS response obligation. It uses the preceding PPDU endpoint without signal extension. Management must preserve the advertised RIFS Mode. VHT operation does not permit RIFS. This recipient rule adds no RIFS transmitter scheduler. + +The review found that ordinary HT transmission at 2.4 GHz omitted the six-microsecond signal extension. The correction belongs in the authoritative PHY duration. HCF must not add it again. Existing short-GI duration differences remain inherited PHY fidelity limits; they do not independently establish an interval error. + +## Standards evidence + +The standards source is IEEE Std 802.11-2024. The fresh local corpus is `/home/user/omnetpp_ws/inet/standards/processed`. The corpus lint output is `/tmp/inet-s5-standards-lint.json`. + +| Canonical node | Physical PDF pages | Requirement | +| --- | --- | --- | +| `ieee80211-2024:table:9-36` | 719–720 | BAR variant Type encoding. | +| `ieee80211-2024:table:9-37` | 722–723 | Block Ack variant Type encoding. | +| `ieee80211-2024:clause:10.25.6.3` | 2060–2061 | Full-state recipient receipt state. | +| `ieee80211-2024:clause:10.25.6.6.2.1` | 2065–2066 | Recipient delivery and interframe conditions. | +| `ieee80211-2024:clause:10.3.2.3.3` | 1889–1890 | SIFS definition and tolerance. | +| `ieee80211-2024:clause:10.3.2.3.2` | 1888–1889 | RIFS definition and conditions. | +| `ieee80211-2024:clause:10.22.5` | 1997–1998 | Coverage-class timing behavior. | +| `ieee80211-2024:table:19-25` | 3493–3494 | Band-specific HT signal extension. | + +These provisions are normative. The selected corpus clauses resolve uniquely. The review inspected the original PDF for Clause 11.38.1 on physical page 2886. That clause requires a VHT AP to clear RIFS Mode. The corpus result without children does not include all later prose in that clause. + +## Verification boundary + +The initial full debug build and subsequent focused rebuilds pass. A build supplies no exchange or timing proof. The focused wire fixture passes independent octet checks. The constructed serializer fixture passes 278 cases. The HCF input fixture passes both configured profiles with supplied recipient agreements. Those results do not establish negotiation or PHY interval recognition. + +Five initial protocol simulations pass across the selected n/ac fixtures. They retain Basic coverage and add actual Compressed exchanges. This initial boundary precedes complete infrastructure timing classification and final interframe execution. Later sections record those results and the authorized correction. + +An isolated observer reproduces a DATA continuation overrun in the pinned parent. With a one-millisecond TXOP, the third DATA ends 178 microseconds after the bound. The candidate reaches the same endpoints. The parent also reproduces the first observed A-MSDU overrun. These observations identify inherited DATA admission limits. They do not establish general TXOP conformance or excuse a new Compressed violation. + +No recorded fingerprint baseline changed. This record does not authorize a baseline change or publication. + +## Confirmed timing blocker + +The candidate before the TXOP correction introduced a failing supported case that the exact parent did not reproduce. General run 6 uses HT operation, 24 Mbit/s DATA, single protection, aggregation, a one-millisecond TXOP, and seed zero. That candidate transmits two later A-MSDUs beyond their TXOP bounds. Each overrun is 310 microseconds. The affected TXOP contains two DATA frames, so the initial-MSDU exception does not apply. + +The second DATA ends at 1.014426 seconds. Its TXOP bound is 1.014116 seconds. The violation exists before any Block Ack response. The aggregate contains 1532 bytes in both versions. The exact parent has no aggregate overrun in run 6. Parent run 14 demonstrates a similar admission defect under different conditions. That other case does not make candidate run 6 acceptable. + +A direct run 6 reproduction also fails with the final reviewed debug library. Its output is `/tmp/inet-s5-reviewed-run6.out`; its error is `/tmp/inet-s5-reviewed-run6.err`. That run stops at the first later A-MSDU violation, at 1.014426 seconds. The earlier non-fatal observer campaign records both violations. + +The inherited admission mechanism causes the new result. `HcfFs` tests positive remaining time before a later exchange. It does not establish that the complete exchange fits. HCF excludes Block Ack support from its existing complete-cost DATA guard. The corrected HT control airtime and Compressed responses change the trajectory. The comparison does not isolate their separate contributions. + +All sixteen initial parent and candidate runs deliver thirty payloads, with 21000 payload bytes per run. All observed BAR/Block Ack exchanges remain within their applicable bounds. Those successes do not establish DATA containment. An independent review rejected that candidate under the [plan's timing condition](ieee80211-compressed-blockack-replacement.md#airtime-acceptance-and-admission-dependencies). The later correction and C3 acceptance below supersede that rejection. + +The [separate correction proposal](ieee80211-compressed-blockack-txop-admission.md) addresses preparation and admission. A final transmit guard alone cannot solve the problem. Aggregation already deletes constituent frames before HCF selects the actual mode. An oversized aggregate can therefore remain prepared and fail every later attempt. The proposal must preserve progress through ordinary MSDUs and the permitted initial-MSDU exception. + +## Focused verification after source review + +The reviewed debug library has SHA-256 `1635793563ff1c0912a4a0e55a70507879af2a20ef83a0b88502562748a347db`. Its build log is `/tmp/inet-s5-reviewed-debug-build.log`. The interframe fixture and direct run 6 reproduction use this library. The earlier mixed-interface regression batch is discarded. + +The unit and legacy results below use their recorded fresh libraries before the final bounded PHY review corrections. Those corrections repair empty-cache lookup, remove an unused virtual, and expose a watch. The protocol campaign uses library `1c57b8f562dbeb5cddbba49b77a3a676042fea7aef9b9e5578e1980c63109f56`. These earlier results retain their recorded source boundaries; they are not new executions against the final relink. + +| Check | Result | Evidence | +| --- | --- | --- | +| Focused units | Nine fixtures pass. | `/tmp/inet-s5-final-unit.log` | +| HT Operation conversion | One fixture passes both RIFS permission values and operation equality checks. | `/tmp/inet-s5-ht-operation-unit.log` | +| Legacy and HCF input modules | Thirty fixtures pass across the main batch and two repaired-fixture reruns. | `/tmp/inet-s5-final-module.log`, `/tmp/inet-s5-repaired-module.log` | +| EDCA DATA lifetime modules | Nine fixtures pass. | `/tmp/inet-s5-lifetime-module.log` | +| Production interframe fixture | All 28 runs pass with seed zero. | `/tmp/inet-s5-interframe-final/` | +| Constructed serializers | All 278 cases pass. | `/tmp/inet-s5-serializer-roundtrip.log` | +| n/ac protocol fixtures | Five simulations pass with Basic and Compressed coverage. | `/tmp/inet-s5-exchange-protocol-final.log` | +| Lowest response mode | Two real infrastructure runs pass at 6 Mbit/s. | `/tmp/inet-s5-exchange-lowest-config.out` | + +The two legacy fixtures required valid synthetic frames. The Action-dispatch fixture now supplies the BAR trailer. The fragment-retirement fixture now supplies all 64 fragment bitmap entries. Their behavioral assertions remain intact. + +The interframe fixture supplies accepted agreements. It uses real PHY reception and HCF ACK completion. It verifies SIFS and RIFS bounds, sequence wrap, prior receipt truth, unrelated traffic, retirement, and cache expiry. Its BUSY control uses a signal below reception sensitivity but above energy detection. Its 168 malformed BAR receptions each leave zero pending candidates. Separate infrastructure tests establish real association and ADDBA; the interframe fixture does not claim negotiation coverage. + +The source reviewers report no remaining confirmed correctness defect in their bounded core and interframe scopes. Architecture gates pass for the changed MAC and PHY subtrees. Review supplies no runtime proof beyond the executed fixtures. + +## Extension contracts in this candidate + +These obligations describe the current candidate. They require migration guidance before publication. They do not change the C3 blocker or activate the default. + +| Contract | Obligation for a custom implementation | +| --- | --- | +| `IQosRateSelection::computeCompressedBlockAckReqMode()` | Select a legal non-HT BAR mode from the supplied initial-or-later TXOP position and current rate sets. | +| `IRecipientQosAckPolicy::computeCompressedBlockAckDurationField()` | Subtract SIFS and the actual 32-byte response airtime from the request reservation. Preserve the Basic method. | +| `IRecipientQosMacDataService::dataFrameReceived()` | Accept the new interframe argument. Preserve its meaning through recipient processing. A false value supplies no interval evidence. | +| `QoSContext` | Supply HCF's resolved profile to the exchange. Existing callers retain the Basic default unless they explicitly supply Compressed. | +| `RecipientBlockAckAgreement::setVariant()` | Set the profile during agreement creation. Do not change it during that agreement's lifetime. Compressed creation initializes explicit receipt state. | +| `IRadioMedium` | Implement the new snapshot and gap queries with copied physical facts. Unavailable transmissions or arrival endpoints produce an absent result. | +| `ICommunicationCache::findCachedArrival()` | Return a cached arrival without insertion, resizing, geometry recalculation, or other state changes. | +| `IIeee80211Mode::getSignalExtensionTime()` | Supply the nominal extension for the mode. `Ieee80211ModeBase` supplies zero; ERP and HT override it where necessary. | + +## Common regression result + +All nineteen selected rows match their recorded non-graphical fingerprints. The runner uses debug mode and the original configuration, run, time limit, and requested non-graphical ingredients. It completes in 182.173 seconds with exit status zero. The selected input is `/tmp/inet-s5-fingerprint-selection.csv`; the log is `/tmp/inet-s5-fingerprint.log`. + +The command runs from `tests/fingerprint`: + +```sh +./fingerprinttest -d \ + -m '(showcases/wireless/(blockack|aggregation|fragmentation|qos|txop)|examples/(wireless/qos|adhoc/qos))' \ + -f tplx -f '~tNl' -f '~tND' -t 6 \ + /tmp/inet-s5-fingerprint-selection.csv +``` + +The graphical `tyf` ingredient remains unverified. No fingerprint baseline changed. These matches preserve the selected legacy regression evidence. They do not override the direct Compressed timing failure. + +These initial regression matches precede the separate TXOP correction and C4. No commit or publication occurred during this execution. + +## Build state before the authorized TXOP correction + +Both debug and release builds pass. The final release freshness check uses `make MODE=release -j6` from the repository root. Its log is `/tmp/inet-s5-reviewed-release-build.log`. The final debug build uses `make MODE=debug -j6`; its log is `/tmp/inet-s5-reviewed-debug-build.log`. The later NED edit corrects a comment and retains the Basic default. + +The separate TXOP proposal passed a consistency review as a scope request. On 2026-10-16, the maintainer authorized that correction and subsequent S5 completion. Source validation and independent review resolved the RTS boundary, lifetime ownership, and mode validation before implementation. The retained strict fixture preserves the inherited outside-agreement failure separately from active Compressed acceptance. + +## Authorized TXOP correction and C3 acceptance + +The [TXOP contract](ieee80211-compressed-blockack-txop-admission.md#resolved-implementation-contract) defines source ownership and each event boundary. The service creates duplicate previews without source removal. HCF selects one DATA mode, evaluates the aggregate, and evaluates an ordinary fallback with that same mode. Commitment transfers the same preview pointer only before DATA submission. Refusal preserves queue frames, deadlines, sequence state, and allocation state. + +RTS failure before DATA updates QSRC and the contention window without a provisional sequence counter. Original MSDU lifetimes bound repeated attempts. Expiry, capability loss, or changed response inputs cancel invalid preparation. Single protection reserves only the current exchange. A later BAR can require fresh contention. Basic and traffic outside active Compressed agreements retain their existing preparation and admission. + +Runtime verification found two effective selection callers that bypassed the first implementation. `RtsCtsFs` still extracted originals after admission. The multicast repeat predicate could also extract a prepared unicast successor before it ended the group sequence. Both callers now use the context-selected frame. Their focused regression cases preserve original queue state when no DATA submission occurs. + +The first retry policy required containment for all committed frames. A real BAR collision exposed a progress failure at 6 Mbit/s. One ordinary protected exchange needs 1.166 milliseconds under a nominal one-millisecond limit. Its first attempt qualifies for an exception, but conservative retry containment refused every later attempt. The implementation now uses the standard same-size single-MPDU exception with no earlier Data or Management frame. The supported path preserves committed MPDU size across retries. + +New previews clear Retry from the previous wireless hop. Committed retries derive Retry from local acknowledgment state. An oversized first-attempt A-MSDU still cannot use the retry exception. Internal collision commits only ordinary queued DATA before its retained recovery action. Multiple protection reserves the actual complete exchange for a permitted overrun. + +The corrected debug library has SHA-256 `0298dbd4c9625cd6f306865f5c1cfd0a3412230c6339b3a84289eb68eba713df`. The release library has SHA-256 `4b32335cdb2f558d60ada0761e46fde84c9879889e1c46b3c3f69bd11d9ae780`. Debug build 4 and the release build pass. Their logs are `/tmp/inet-s5-txop-debug-build-4.log` and `/tmp/inet-s5-txop-release-build.log`. + +The focused admission fixture passes all 24 cases with seed zero. It checks refusal, ordinary fallback, ownership, deadlines, sequence state, one mode query, and RTS cancellation. It also checks QSRC rollover, lifetime expiry, capability changes, internal collision, and multicast-to-unicast refusal. Cases 22 and 23 require actual ordinary and A-MSDU retries with unchanged size and DATA airtime above one millisecond. Evidence is `/tmp/inet-s5-admission24.out` and `/tmp/inet-s5-admission24.err`. + +All sixteen explicit-profile infrastructure cases pass the scoped timing assertions. The observer records agreement scope at DATA submission, before later teardown can change lookup results. Active Compressed checks include DATA with Normal Ack. No active later DATA or ACK overrun remains. All 98 matched BAR/Block Ack exchanges finish within their bounds. All 32 hop summaries report zero Block Ack overruns. + +The former run 6 failure now passes with zero DATA, ACK, or Block Ack overruns on both hops. Those hops transmit 23 and 11 active aggregates. The separate two-run lowest-response campaign records 29 actual 32-byte responses at 6 Mbit/s. Their airtime is 74 microseconds, with exactly 10 microseconds of SIFS. No Block Ack response exceeds its bound. + +The infrastructure observer separately records inherited violations outside active agreements. `StrictAllTiming` retains the original aggregate assertions and reproduces the pre-agreement aggregate failure in run 4. This acceptance establishes no timing compliance for those retained paths. The default change does not repair Basic or outside-agreement admission. + +The cleanup assertion permits only the idle `WAITING_FOR_SUCCESSOR` procedure with `SIMTIME_MAX` response deadline. Queues, in-progress frames, agreements, pending BAR state, and agreement timers must remain empty or inactive. That phase records a possible future setup after a completed Normal Ack transfer. It holds no unresolved exchange. + +Independent review accepts C3 for this scope and permits C4. The verdict is `/tmp/inet-s5-c3-acceptance-review.md`. Explicit-profile evidence is `/tmp/inet-s5-exchange-scoped-general16.out` and `/tmp/inet-s5-exchange-scoped-lowest2.out`. The scope table is `/tmp/inet-s5-exchange-scoped-general16.csv`. The unchanged strict failure remains in `/tmp/inet-s5-exchange-strict-all-inherited.out` and its `.err` file. + +## C4 default and migration verification + +`Hcf.ned` now defaults to Compressed. The infrastructure fixture omits the selector after C3 and requires actual Compressed frames. All sixteen default cases pass. Both lowest-response cases also pass after default activation. Evidence is `/tmp/inet-s5-default-exchange16.out` and `/tmp/inet-s5-default-lowest2.out`. + +All six external-style migration cases pass without an explicit selector. Legacy g and a links, and HT ad hoc links without peer records, deliver twelve UDP payloads through Normal Ack. Support-disabled controls produce no irrelevant warning. The fixture checks the resolved default, fallback diagnostics, absent ADDBA/BAR/Block Ack, and empty active state. Evidence is `/tmp/inet-s5-default-migration6.out`. + +All twelve initialization cases pass after default activation. They cover the omitted selector, explicit Basic, explicit Compressed, unknown values, and both delayed-policy guards. Evidence is `/tmp/inet-s5-default-variant12.out`. Retained Basic fragment, bitmap, and public API checks remain separate regression obligations. + +Both `N_BlockAck` and `Ac_BlockAck` also pass three consumer runs: explicit Basic, explicit Compressed, and the NED default. The third run uses bare INI `default`, which invokes OMNeT++ `acceptDefault()`. It verifies the resolved profile at all three HCF instances. Actual Compressed BAR/Block Ack and positive receipt bits complete that run. All six simulations and both diagnostic scripts pass. Evidence is `/tmp/inet-s5-default-consumer-protocol6.log` and `/tmp/inet-s5-default-consumer-{n3,ac3}.out`. + +Both final build freshness checks pass after the NED change. Their logs are `/tmp/inet-s5-c4-debug-build.log` and `/tmp/inet-s5-c4-release-build.log`. The C4 change affects the runtime NED default; the recorded debug and release library hashes remain unchanged. + +## Final regression evidence + +All final checks use the active checkout and seed zero where a simulation requires a seed. Debug mode retains runtime assertions. The release build supplies a separate compilation check. No fingerprint expectation changed. + +| Check | Final result | Evidence | +| --- | --- | --- | +| Unit fixtures | All ten pass. | `/tmp/inet-s5-c4-unit.log` | +| Module regression batch | Forty-one pass initially. The retained Basic deferral fixture passes after its explicit profile correction. All 42 selected fixtures therefore pass across the batch and focused rerun. | `/tmp/inet-s5-c4-module-regression.log`, `/tmp/inet-s5-c4-deferral-module.log` | +| Default infrastructure and migration | Both additional module fixtures pass: sixteen infrastructure cases and six migration cases. Both lowest-response cases pass. | `/tmp/inet-s5-default-module.log`, `/tmp/inet-s5-default-lowest2.out` | +| Serializer | All 278 constructed cases pass, with zero unexpected or stale coverage entries. The separate golden-octet unit also passes. | `/tmp/inet-s5-c4-serializer.log`, `/tmp/inet-s5-c4-unit.log` | +| Wi-Fi protocol files | Five supported files pass. Four deferred files retain expected failures. No unexpected result occurs. | `/tmp/inet-s5-default-plan-protocol9.log`, `/tmp/inet-s5-default-protocol-deferred.txt` | +| Fingerprint rows | All nineteen selected rows match in 153.811 seconds. | `/tmp/inet-s5-c4-fingerprint.log` | +| Architecture | All three scoped checks pass. | `/tmp/inet-s5-c4-architecture-mac.log`, `/tmp/inet-s5-c4-architecture-common.log`, `/tmp/inet-s5-c4-architecture-wifi.log` | + +The module union contains 44 distinct fixtures. It preserves Basic teardown, fragmentation, NAV, setup, association reset, response modes, and lifetime checks. It repeats the 28 physical interframe cases, 24 admission cases, and 76 scheduling cases after default activation. The scheduling fixture retains all original Basic cases. It adds actual Compressed shifted responses through active frame sequences and small-window arbitration. + +The recipient integration checks DATA 100, 102, and 101 through HCF. Delivery order is 100, 101, and 102. A subsequent BAR produces three positive receipt bits and zero holes after delivery. The six focused originator cases call the HCF control boundary directly. The scheduling fixture separately supplies actual response matching. These fixtures supply accepted agreements; the infrastructure and protocol fixtures prove negotiation separately. + +The legacy deferral fixture omitted a selector while it used non-HT mode `a`. The Compressed default correctly prevented its expected Basic setup. The fixture now selects Basic explicitly. All twelve original scenarios pass without assertion changes. This correction preserves the fixture's intended historical profile. + +The deferred protocol results remain explicit. `N_Ampdu` and `Ac_Ampdu` first miss an A-MPDU subframe. `Ac_MultiTidBa` passes both TID observations before it misses Multi-TID Block Ack. `WifiQosDelayedBa` observes a delayed request before recipient delayed acceptance fails. No expectation changed, and these results establish no support for those features. + +The interface gates report two MAC findings and three PHY findings. Their output is byte-identical to the pinned parent's output. The implicated source files remain unchanged. Logs are `/tmp/inet-s5-c4-interfaces-mac.log` and `/tmp/inet-s5-c4-interfaces-phy.log`, with corresponding `/tmp/inet-s5-parent-interfaces-*.log` files. This work adds no interface-gate finding. + +The final unit command runs from the checkout root: + +```sh +inet_run_unit_tests -m debug -f '(Ieee80211BlockAck(Wire|Scheduling|ActionWire)_1|Ieee80211CompressedBlockAck(Rate|Record)_1|Ieee80211(ResponseModeSelection|ResponseTimeout|HtModeSet|HtCapabilities|VhtHtCapabilities)_1)\.test$' +``` + +The module regression command runs from the checkout root: + +```sh +inet_run_module_tests -m debug -f 'Ieee80211(BlockAck.*|EdcaDataLifetime.*|PreparedCancellation_1|RecipientNav_1|Txnav_1|Hcf(SingleProtection_1|MultipleProtection_[12]|Header_1)|SingleProtectionGroup_1|VhtHtSupport_1|HtAssociation_1|Mgmt(StateCommit|RatePublication)_1|CompressedBlockAck(Input|Interframe|Admission|Originator)_1)\.test$' +inet_run_module_tests -m debug -f 'Ieee80211BlockAckDeferral_1\.test$' +inet_run_module_tests -m debug -f '(Ieee80211CompressedBlockAckExchange_1|Ieee80211CompressedBlockAckMigration_1|Ieee80211BlockAckVariant_1)\.test$' +``` + +The serializer and protocol commands run from the checkout root: + +```sh +inet_run_serializer_tests -m debug -f 'serializer_chunk_roundtrip\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|N_CompressedBa|N_Amsdu|N_Ampdu|Ac_BlockAck|Ac_BlockAckFallback|Ac_Ampdu|Ac_MultiTidBa|WifiQosDelayedBa)\.test$' +inet_run_protocol_tests -m debug -w '^tests/protocol/wifi$' -f '(N_BlockAck|Ac_BlockAck)\.test$' +``` + +The final fingerprint command repeats the nineteen-row command above from `tests/fingerprint`. It uses the original configurations, runs, time limits, and selected non-graphical ingredients. Graphical `tyf` remains unverified. No CSV or other recorded baseline changed. + +Current user and migration guides describe the default, fallback, profile conflict, fragmentation boundary, and added interface obligations. Release notes preserve the supported `ac` path and all four deferred protocol requirements. The Basic showcase retains its explicit profile, topology, and result rows. + +The evidence remains limited to the recorded deterministic cases and seed. Production HT catalogs provide no distinct legal alternate response for the tested 32-byte frame. The selector unit verifies that alternate-rate contract separately. The recipient prefix release remains an interpretation of ordered delivery together with the individual-MPDU release condition. This work establishes no complete historical Basic conformance or complete VHT transfer conformance. + +The implementation checks above precede the separate commit, push, and pull request phase. The user authorized that phase after implementation. The original local plan changes and untracked history remain outside the publication scope. diff --git a/plan/pending/ieee80211-compressed-blockack-txop-admission.md b/plan/pending/ieee80211-compressed-blockack-txop-admission.md new file mode 100644 index 00000000000..6664e786d49 --- /dev/null +++ b/plan/pending/ieee80211-compressed-blockack-txop-admission.md @@ -0,0 +1,121 @@ +# TXOP prerequisite: admit Compressed DATA before destructive preparation + +This correction is complete and removes the active Compressed timing blocker. The [replacement plan](ieee80211-compressed-blockack-replacement.md#excluded-features-and-fragmentation) excludes complete-cost TXOP admission from its original scope. On 2026-10-16, the maintainer authorized this separate correction and subsequent S5 completion. The resolved contract below passed source validation and independent review before production edits. The [S5 execution record](ieee80211-compressed-blockack-s5-execution.md#authorized-txop-correction-and-c3-acceptance) supplies the final runtime evidence and scope limits. + +## Problem and required result + +The final S5 candidate sends two later A-MSDUs beyond their TXOP bounds in a supported case. The exact parent has no such violation in that case. The [S5 execution record](ieee80211-compressed-blockack-s5-execution.md#confirmed-timing-blocker) supplies the comparison and its limits. IEEE Std 802.11-2024, Clause 10.23.2.9 does not permit the observed continuation. + +The correction must establish the complete exchange cost before DATA commitment. An aggregate must fit its applicable bound. A later ordinary MSDU must also fit. The correction must retain the permitted initial unaggregated MSDU exception. It must not strand an oversized aggregate until lifetime expiry. + +A zero TXOP limit permits one SU PPDU with one MSDU or A-MSDU, plus its required protection and response. It does not mean zero permitted airtime. With a positive limit, the initial unaggregated MSDU exception permits at most one Data or Management frame. RTS and CTS airtime counts toward the exchange cost, but those Control frames do not increase that count. A same-size retransmission has a separate permitted exception under Clause 10.23.2.9; the implementation contract must state its retry policy. + +Hypothetical example: a TXOP has 200 microseconds left. The proposed aggregate needs 400 microseconds, while its first ordinary MSDU needs 150 microseconds. The service must commit that ordinary MSDU and leave the other MSDUs queued. If neither form qualifies, the sequence must end without queue removal or sequence assignment. + +The initial implementation scope covers active Compressed agreements. Explicit Basic and Normal Ack fallback outside an agreement retain their current preparation paths. This boundary avoids an unrelated compatibility repair. It does not claim that those retained paths satisfy every TXOP constraint. + +## Confirmed cause + +The [data service](../../src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc) removes aggregation candidates before HCF selects the actual DATA mode. The aggregation builder deletes the constituent frames. The service replaces their lifetime records and assigns a sequence number. At that point, a final HCF refusal cannot recover the original queue form. + +The [frame sequence](../../src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc) tests whether positive TXOP time remains. That test does not prove that a complete later exchange fits. HCF excludes Block Ack support from its existing complete-cost DATA guard. A guard at final submission therefore needs a preparation correction as well. + +AARF and Onoe can change state when the rate selector queries them. Separate mode queries for a preview, RTS protection, and DATA submission can produce different decisions. The cost calculation must use the actual selected mode without extra rate-control queries. + +## Proposed ownership and sequence + +| Step | Responsible component and proposed action | Expected result and verification | +| --- | --- | --- | +| 1. Prepare without commitment. | The data service obtains borrowed candidates from the aggregation policy. It constructs a preview from duplicates. | Queue ownership, deadlines, and sequence counters remain unchanged. A focused fixture records them before and after refusal. | +| 2. Select one DATA mode. | HCF selects the mode once for this preparation. The frame-sequence context owns that decision and the preparation state. | Aggregate evaluation, ordinary-MSDU evaluation, protection, and submission use one mode decision. A test rate control counts queries. | +| 3. Calculate complete cost. | HCF derives the applicable IFS, DATA duration, protection, and immediate response from their existing owners. | Admission checks the TXOP bound and the applicable TXNAV bound. PHY mode objects supply durations. | +| 4. Select a permitted form. | HCF first evaluates the aggregate. If it fails, HCF evaluates the selected ordinary MSDU with the same mode. | An oversized aggregate does not cause permanent refusal. The ordinary MSDU can fit or qualify for the initial-MSDU exception. | +| 5. Defer when necessary. | The frame sequence ends normally when neither form qualifies. | The queue remains intact. Refusal causes no DATA retry outcome, sequence assignment, or aggregate lifetime replacement. | +| 6. Commit at a safe boundary. | The data service commits only the accepted form. HCF uses its retained mode for the actual exchange. | Extraction, lifetime replacement, and sequence assignment occur once. No committed aggregate waits indefinitely because its form cannot fit. | + +BAR arbitration must precede DATA preparation, as S4 requires. The correction adds no BAR forecast. A final BAR can obtain a later TXOP through the existing arbitration path. + +The existing final false-transmit result remains a safety check. The frame-sequence handler removes a refused step and ends the sequence without a transmission-failure result. Normal admission refusal must occur before destructive source commitment. + +## Boundary requirements + +RTS creates an event boundary between preparation and DATA submission. A capability update, lifetime expiry, or RTS failure can occur before DATA starts. The implementation contract must identify the owner of each preview and original frame throughout that interval. + +The resolved contract retains reversible preparation until the first DATA submission. A failed RTS or invalidated capability discards the preview and its selected mode. Original queue frames retain their existing deadlines and sequence state. A later attempt obtains a new preparation and mode decision. The source trace established that this boundary fits the existing ownership and lifetime contracts. + +The frame-sequence API must distinguish preparation refusal from an empty queue and a failed transmission. `InProgressFrames` must not convert those outcomes into a DATA retry. The integration must preserve S4's preview and BAR-first contracts. + +Admission refusal causes no DATA retry outcome. An actual RTS failure retains its required recovery behavior while the service discards reversible preparation. The implementation contract must keep those paths separate. + +For DATA under an active Block Ack agreement, an actual RTS failure updates QSRC and the contention window. QSRC is the short retry counter for an access category. Clause 10.23.2.12.1 applies its associated-RTS increment rule to MSDUs and A-MSDUs outside a Block Ack agreement. A preview must not create a sequence-keyed counter before sequence assignment. QSRC rollover therefore does not discard queued DATA; its lifetime still bounds repeated attempts. This changes INET's former per-frame RTS discard behavior on this scoped path. Basic and Normal Ack outside an agreement retain their existing recovery. + +At the preceding exchange completion, TXNAV admission compares complete exchange cost strictly below the remaining TXNAV. That cost excludes the leading IFS. Positive-limit TXOP containment includes the leading IFS and permits equality with the bound. Clauses 10.23.2.8 and 9.2.5.2 support these separate comparisons. The correction must not move the strict TXNAV comparison to a later reference that rejects an exact single-protection reservation. + +The correction must not retain an illegal mode after peer capabilities change. It must also avoid a second adaptive-rate query merely to validate an unchanged prepared decision. The implementation contract must specify that validation separately from mode selection. + +These are engineering checks for the approved scope. They do not require the maintainer to choose a cache format or callback signature. If source analysis needs a different behavior or a wider compatibility change, the owner must update this proposal first. + +## Resolved implementation contract + +The data service owns duplicate previews through `PreparedQosData`. Each preparation records original frame IDs and its source queue. It retains no original frame pointer across events. `prepareFrame(queue, allowAggregation)` creates one form without extraction. HCF first requests the aggregate form. If that form fails admission, HCF requests the ordinary form with the same DATA mode. + +`QoSContext` owns the selected preparation and its retained modes. HCF supplies a preparation callback with three outcomes: `LEGACY`, `READY`, and `DEFERRED`. `HcfFs` calls it after BAR arbitration and before extraction. Refusal ends the repeat normally. Legacy contexts retain their existing extraction route. + +`isPreparationValid()` checks source membership, eligibility, deadlines, next sequence, and A-MSDU permission. HCF separately validates every retained mode and response input through pure queries. These checks include mode-set changes. A changed input cancels preparation without another adaptive DATA query. HCF repeats validation before RTS and before DATA submission. + +`commitPreparedFrame()` removes the accepted sources only before first DATA submission. It transfers the same preview pointer to `InProgressFrames`. Ordinary commitment transfers the original absolute deadline. Aggregate commitment starts the existing A-MSDU lifetime. Commitment assigns one sequence number and allocation index. Frame-sequence steps retain valid borrowed pointers until their disposal. + +```mermaid +flowchart TD + Queue[Original MSDUs remain queued.] --> Preview[The service creates duplicate previews.] + Preview --> Admission{Does a permitted form qualify?} + Admission -->|No| Refuse[HCF ends the sequence without commitment.] + Admission -->|Yes| Protection{Does DATA require RTS?} + Protection -->|No| Validate{Are sources and retained modes valid?} + Protection -->|Yes| RTS[HCF submits RTS with the retained decision.] + RTS -->|CTS succeeds| Validate + RTS -->|CTS fails for valid sources| Recovery[Recovery updates QSRC and CW.] + RTS -->|Sources become invalid| Discard + Recovery --> Discard[Context disposal deletes the preview.] + Validate -->|No| Discard + Validate -->|Yes| Commit[The service commits the same preview pointer.] + Commit --> Data[HCF submits DATA with its retained mode.] +``` + +Expiry during CTS wait can remove originals safely because preparation stores their IDs. An invalid preparation ends without stale DATA or a synthetic RTS failure. A valid preparation with failed CTS causes one actual RTS recovery outcome. Context disposal deletes unused previews after step disposal. + +The scoped single-protection path reserves only its current exchange. It makes no forecast for unprepared successor DATA. A later BAR can contend through the existing S4 route. Multiple protection retains its reservation formula. This choice can increase contention and change simulation trajectories for active Compressed agreements. + +Committed retransmissions use the same-size single-MPDU exception from Clause 10.23.2.9 when no earlier Data or Management frame occurred in the TXOP. This exception can include a retransmitted A-MSDU. It does not permit an oversized first-attempt A-MSDU. The supported Compressed path has no fragmentation or A-MPDU operation. Its committed MPDU retains its size across retries; Retry and Duration field changes do not change that size. Basic and outside-agreement fallback retain their existing paths. + +A bounded LLDB observation established why conservative retry containment cannot satisfy the supported exchange. AP DATA at 6 Mbit/s needs a 1.166-millisecond protected exchange within a nominal one-millisecond TXOP. Its first ordinary transmission qualifies for the initial exception. A real BAR collision makes that DATA retry eligible. Conservative containment then refuses every attempt despite the permitted same-size exception. The root validated the required policy correction before source edits. Evidence is `/tmp/inet-s5-txop-diagnostic-lldb-cost.out`. + +Internal collision has a separate preparation entry. The existing HCF branch extracts DATA before it updates collision recovery. On the scoped queued path, HCF instead commits one ordinary preview before that existing recovery action. It must not create an aggregate before admission. Other queued MSDUs retain their ownership and deadlines. An unsent ordinary MSDU still qualifies for the first-transmission exception when its conditions hold. + +Multiple protection must reserve the actual exchange when a permitted initial ordinary MSDU or same-size retransmission exceeds a positive limit. The scoped path uses the existing helper's current-exchange form for that case. It must not reduce protection to zero merely because DATA exceeds the limit. The helper's reservation formula remains unchanged. + +The root validated this contract against the current source. An independent review found no unresolved blocking gap. This contract review supplies no runtime proof. Production implementation and focused verification follow this gate. + +## Proposed verification + +The direct reproduction uses `Ieee80211CompressedBlockAckExchange_1.test`, General run 6, seed zero. It selects HT operation, 24 Mbit/s DATA, single protection, aggregation, and a one-millisecond TXOP. The candidate must eliminate both 310-microsecond continuation overruns. The raw comparison remains in `/tmp/inet-s5-exchange-parent-candidate-comparison.json`. + +Add a focused preparation fixture at the proposed path `tests/module/Ieee80211CompressedBlockAckAdmission_1.test`. That fixture does not yet exist. Its cases must establish these results: + +- An initial aggregate that cannot fit becomes an ordinary MSDU before commitment. +- A permitted initial unaggregated MSDU can exceed the TXOP limit under the applicable exception. +- A smaller ordinary MSDU can replace an aggregate when it fits the remaining interval. +- Refusal preserves queue ownership, deadlines, and sequence counters when neither form qualifies. +- RTS failure, lifetime expiry, and capability changes discard invalid preparation without stale DATA transmission. +- RTS protection, DATA duration, and admission consume one selected mode. +- Basic controls preserve their existing behavior. + +Repeat S5's complete infrastructure campaign after the correction. Retain the lowest-response cases and exact BAR/Block Ack bounds. Repeat the affected lifetime, setup, frame-sequence, and fragmentation fixtures. Repeat the nineteen selected fingerprint rows without a baseline update. + +C3 can pass only after the supported corrected exchanges meet their applicable bounds. C4 and default activation must follow that result. This proposal grants no publication or fingerprint-baseline authority. + +## Execution result + +All 24 focused admission cases pass. All sixteen infrastructure cases pass the active Compressed timing assertions. Both lowest-response cases pass. Independent review accepts C3. C4 activates the default only after that decision and repeats its required checks. + +The former run 6 violation no longer occurs on either hop. Outside-agreement admission retains its inherited limits. `StrictAllTiming` preserves their separate reproduction. Basic settings and recorded fingerprint expectations remain unchanged. The final nineteen-row fingerprint campaign passes without a baseline update. diff --git a/showcases/wireless/blockack/doc/index.rst b/showcases/wireless/blockack/doc/index.rst index 2d6aa48f04e..b4808ed7c33 100644 --- a/showcases/wireless/blockack/doc/index.rst +++ b/showcases/wireless/blockack/doc/index.rst @@ -50,7 +50,7 @@ Block Acknowledgment in INET In INET, the block ack mechanism can be enabled by setting the :par:`isBlockAckSupported` parameter of the MAC's :ned:`Hcf` submodule to ``true`` (it is disabled by default). -This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. Basic remains the default. Other configurations can select Compressed explicitly. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. +This showcase selects ``blockAckVariant = "basic"`` on both endpoints to retain the historical profile. HCF defaults to Compressed for other configurations. Compressed agreements require usable local and peer HT state. Without that state, DATA uses Normal Ack. HCF rejects unknown selector values during initialization, also when Block Ack support is disabled. Several submodules of :ned:`Hcf` are involved in the block ack mechanism. These modules have parameters controlling the block ack behavior. The following paragraphs diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index e38a6636622..19462280f07 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -29,7 +29,7 @@ module Hcf extends Module like IHcf { parameters: bool isBlockAckSupported = default(false); - string blockAckVariant = default("basic"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. + string blockAckVariant = default("compressed"); // Compressed requires usable HT state. Select Basic explicitly for historical compatibility. string rxModule; string txModule; diff --git a/tests/module/Ieee80211BlockAckDeferral_1.test b/tests/module/Ieee80211BlockAckDeferral_1.test index 58f6614bc96..b1d38cbbfa9 100644 --- a/tests/module/Ieee80211BlockAckDeferral_1.test +++ b/tests/module/Ieee80211BlockAckDeferral_1.test @@ -384,6 +384,7 @@ record-vector-results = false **.wlan[*].mac.qosStation = true **.wlan[*].mac.hcf.typename = "DeferralHcf" **.wlan[*].mac.hcf.isBlockAckSupported = ${scenario} != 4 +**.wlan[*].mac.hcf.blockAckVariant = "basic" **.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.typename = ${policy="OriginatorBlockAckAgreementPolicy","LongTimeoutPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy","OriginatorBlockAckAgreementPolicy" ! scenario} **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.typename = ${aggregation="","","","","","","","","","","","BasicMsduAggregationPolicy" ! scenario} **.wlan[*].mac.hcf.originatorMacDataService.msduAggregationPolicy.subframeNumThreshold = 2 diff --git a/tests/module/Ieee80211CompressedBlockAckExchange_1.test b/tests/module/Ieee80211CompressedBlockAckExchange_1.test index 4ccb39e660a..2988432ef82 100644 --- a/tests/module/Ieee80211CompressedBlockAckExchange_1.test +++ b/tests/module/Ieee80211CompressedBlockAckExchange_1.test @@ -352,7 +352,6 @@ network ExchangeNetwork { %inifile: omnetpp.ini [General] -**.hcf.blockAckVariant = "compressed" network = ExchangeNetwork ned-path = .;../../../../src sim-time-limit = 1.3s diff --git a/tests/module/Ieee80211CompressedBlockAckMigration_1.test b/tests/module/Ieee80211CompressedBlockAckMigration_1.test new file mode 100644 index 00000000000..72db8ff5672 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckMigration_1.test @@ -0,0 +1,179 @@ +%description: +An external configuration enables Block Ack without an explicit profile. +Legacy g and a links, and an HT ad hoc link without peer HT records, must deliver twelve UDP payloads with Normal Ack. +Require a queued successor without ADDBA, BAR, or BA. Check profile and capability diagnostics with support enabled and disabled. + +%extraargs: -r 0..5 + +%postrun-command: python3 check-diagnostics.py + +%file: Migration.cc +#include +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet::ieee80211 { +class MigrationHcf : public Hcf { + public: + int normalData = 0; + int requests = 0; + int bars = 0; + int responses = 0; + bool queuedSuccessor = false; + bool compressed() const { return std::string(par("blockAckVariant").stringValue()) == "compressed"; } + bool clean() const { + return (!originatorBlockAckAgreementHandler || (originatorBlockAckAgreementHandler->getAgreements().empty() && + originatorBlockAckAgreementHandler->getProcedures().empty())) && + (!recipientBlockAckAgreementHandler || recipientBlockAckAgreementHandler->getAgreements().empty()); + } + protected: + bool transmitFrame(Packet *packet, simtime_t ifs) override { + auto header = packet->peekAtFront(); + bool accepted = Hcf::transmitFrame(packet, ifs); + if (!accepted) + return false; + requests += dynamicPtrCast(header) != nullptr; + bars += dynamicPtrCast(header) != nullptr; + responses += dynamicPtrCast(header) != nullptr; + if (auto data = dynamicPtrCast(header)) { + ASSERT(data->getAckPolicy() == NORMAL_ACK); + ASSERT(!mib->findPeerHtState(data->getReceiverAddress())); + normalData++; + } + return true; + } + void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& header, AccessCategory ac) override { + queuedSuccessor |= edca->getEdcaf(ac)->getPendingQueue()->getNumPackets() > 0; + Hcf::originatorProcessTransmittedDataFrame(packet, header, ac); + } +}; +Define_Module(MigrationHcf); + +class MigrationTest : public cSimpleModule, public cListener { + int received = 0; + int bytes = 0; + protected: + void initialize() override { scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("subscribe")); } + void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + auto packet = check_and_cast(value); + received++; + bytes += packet->getByteLength(); + } + void handleMessage(cMessage *message) override { + if (message->isName("subscribe")) { + delete message; + getModuleByPath("^.host2.app[0]")->subscribe(packetReceivedSignal, this); + scheduleAt(SimTime(99, SIMTIME_MS), new cMessage("verify")); + return; + } + delete message; + ASSERT(received == 12 && bytes == 8400); + for (const char *path : {"^.host1.wlan[0].mac.hcf", "^.host2.wlan[0].mac.hcf"}) { + auto hcf = check_and_cast(getModuleByPath(path)); + ASSERT(hcf->compressed() && hcf->clean()); + ASSERT(hcf->requests == 0 && hcf->bars == 0 && hcf->responses == 0); + if (hcf->normalData > 0) + ASSERT(hcf->normalData >= 12 && hcf->queuedSuccessor); + } + ASSERT(check_and_cast(getModuleByPath("^.host1.wlan[0].mac.hcf"))->normalData >= 12); + std::cout << "Compressed default migration assertions passed.\n"; + endSimulation(); + } +}; +Define_Module(MigrationTest); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.inet.AdhocHost; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module MigrationHcf extends Hcf { parameters: @class(::inet::ieee80211::MigrationHcf); } +simple MigrationTest { parameters: string opMode; bool supported; @class(::inet::ieee80211::MigrationTest); } +network MigrationNetwork { + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host1: AdhocHost; + host2: AdhocHost; + test: MigrationTest; +} + +%inifile: omnetpp.ini +[General] +network = MigrationNetwork +ned-path = .;../../../../src +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = false +cmdenv-event-banners = false +**.mac.hcf.originatorBlockAckAgreementPolicy.cmdenv-log-level = debug +**.mac.hcf.cmdenv-log-level = info +**.cmdenv-log-level = off +record-vector-results = false +record-scalar-results = false +*.test.opMode = ${opMode="g(mixed)","a","n(mixed-2.4Ghz)"} +*.test.supported = ${supported=true,false} +*.host1.numApps = 1 +*.host1.app[0].typename = "UdpBasicApp" +*.host1.app[0].destAddresses = "host2" +*.host1.app[0].destPort = 1000 +*.host1.app[0].messageLength = 700B +*.host1.app[0].startTime = 10ms +*.host1.app[0].stopTime = 11.15ms +*.host1.app[0].sendInterval = 100us +*.host2.numApps = 1 +*.host2.app[0].typename = "UdpSink" +*.host2.app[0].localPort = 1000 +*.host*.ipv4.arp.typename = "GlobalArp" +*.host*.wlan[*].opMode = ${opMode} +*.host*.wlan[*].radio.bandName = ${opMode} == "a" ? "5 GHz (mapped 20 MHz)" : "2.4 GHz" +*.host*.wlan[*].radio.channelNumber = 0 +*.host*.wlan[*].bitrate = 6Mbps +*.host*.wlan[*].classifier.typename = "QosClassifier" +*.host*.wlan[*].mac.qosStation = true +*.host*.wlan[*].mac.hcf.typename = "MigrationHcf" +*.host*.wlan[*].mac.hcf.isBlockAckSupported = ${supported} +**.originatorMacDataService.msduAggregationPolicy.typename = "" +**.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 2346B +*.host*.wlan[*].radio.receiver.energyDetection = -105dBm +*.host*.wlan[*].radio.receiver.sensitivity = -105dBm +*.host*.wlan[*].radio.receiver.snirThreshold = 0dB +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.host1.mobility.initialX = 0m +*.host2.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%file: check-diagnostics.py +from pathlib import Path +import re + +text = Path("test.out").read_text() +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", text)[1:] +assert len(runs) == 6, len(runs) +warning = "Compressed Block Ack has no usable local HT support" +profile = "Block Ack profile: compressed" +for run in runs: + ht = '$opMode="n(mixed-2.4Ghz)"' in run + supported = "$supported=true" in run + assert "Compressed default migration assertions passed." in run + assert run.count(profile) == (2 if supported else 0), run + assert run.count(warning) == (2 if supported and not ht else 0), run + if supported: + reason = "peer HT state is absent" if ht else "local HT state is absent" + assert f"Compressed Block Ack setup refused: {reason}; DATA uses Normal Ack." in run, run + else: + assert "Compressed Block Ack setup refused:" not in run, run +print("Compressed default migration diagnostics passed.") + +%contains: stdout +Compressed default migration assertions passed. diff --git a/tests/protocol/wifi/11ac/Ac_BlockAck.test b/tests/protocol/wifi/11ac/Ac_BlockAck.test index cd274cf2543..e6407799617 100644 --- a/tests/protocol/wifi/11ac/Ac_BlockAck.test +++ b/tests/protocol/wifi/11ac/Ac_BlockAck.test @@ -1,8 +1,11 @@ %description: 802.11ac Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] -Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. +Check explicit Basic, explicit Compressed, and the NED default with concrete BAR/BA types and positive receipt bits. +The third run uses the unquoted INI keyword default. OMNeT++ accepts the parameter's NED default for this keyword. -%extraargs: -r 0..1 +%extraargs: -r 0..2 + +%postrun-command: python3 check-default.py %file: Ac_BlockAck.cc @@ -78,9 +81,9 @@ Define_ProtocolTest(wifi_11ac_compressed_exchange) %inifile: test.ini [General] -*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange"} +*.tester.testName = ${test="wifi_11ac_block_ack","wifi_11ac_compressed_exchange","wifi_11ac_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed",default ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -96,6 +99,20 @@ include ../../ini/_base.ini **.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 *.sta*.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +%file: check-default.py +from pathlib import Path +import re + +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", Path("test.out").read_text())[1:] +assert len(runs) == 3, len(runs) +for run, profile in zip(runs, ["basic", "compressed", "default"]): + assert f'$profile={profile if profile == "default" else chr(34) + profile + chr(34)}' in run + resolved = "basic" if profile == "basic" else "compressed" + assert run.count(f"Block Ack profile: {resolved}") == 3 + test = "wifi_11ac_block_ack" if profile == "basic" else "wifi_11ac_compressed_exchange" + assert f"PROTOCOLTEST {test}: PASS" in run +print("Basic, explicit Compressed, and NED default exchanges passed.") + %contains: stdout PROTOCOLTEST wifi_11ac_block_ack: PASS diff --git a/tests/protocol/wifi/11n/N_BlockAck.test b/tests/protocol/wifi/11n/N_BlockAck.test index 98f60e3b1d7..0ae29c49cad 100644 --- a/tests/protocol/wifi/11n/N_BlockAck.test +++ b/tests/protocol/wifi/11n/N_BlockAck.test @@ -1,8 +1,11 @@ %description: 802.11n Block Ack session: ADDBA action (type 13), BlockAckReq (type 24), BlockAck (type 25). [Required] -Check explicit Basic and Compressed with concrete BAR/BA types and positive receipt bits. +Check explicit Basic, explicit Compressed, and the NED default with concrete BAR/BA types and positive receipt bits. +The third run uses the unquoted INI keyword default. OMNeT++ accepts the parameter's NED default for this keyword. -%extraargs: -r 0..1 +%extraargs: -r 0..2 + +%postrun-command: python3 check-default.py %file: N_BlockAck.cc @@ -79,9 +82,9 @@ Define_ProtocolTest(wifi_11n_compressed_exchange) %inifile: test.ini [General] -*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange"} +*.tester.testName = ${test="wifi_11n_block_ack","wifi_11n_compressed_exchange","wifi_11n_compressed_exchange"} *.sta*.wlan[*].mac.hcf.isBlockAckSupported = true -**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed" ! test} +**.wlan[*].mac.hcf.blockAckVariant = ${profile="basic","compressed",default ! test} *.ap.wlan[*].mac.hcf.isBlockAckSupported = true *.sta*.wlan[*].mac.hcf.originatorAckPolicy.blockAckReqThreshold = 5 *.sta1.app[0].sendInterval = 0.1ms @@ -97,6 +100,20 @@ include ../../ini/_base.ini **.hcf.originatorMacDataService.msduAggregationPolicy.aggregationLengthThreshold = 10000 *.sta*.wlan[*].mac.hcf.originatorAckPolicy.maxBlockAckPolicyFrameLength = 10000B +%file: check-default.py +from pathlib import Path +import re + +runs = re.split(r"Preparing for running configuration General, run #\d+\.\.\.", Path("test.out").read_text())[1:] +assert len(runs) == 3, len(runs) +for run, profile in zip(runs, ["basic", "compressed", "default"]): + assert f'$profile={profile if profile == "default" else chr(34) + profile + chr(34)}' in run + resolved = "basic" if profile == "basic" else "compressed" + assert run.count(f"Block Ack profile: {resolved}") == 3 + test = "wifi_11n_block_ack" if profile == "basic" else "wifi_11n_compressed_exchange" + assert f"PROTOCOLTEST {test}: PASS" in run +print("Basic, explicit Compressed, and NED default exchanges passed.") + %contains: stdout PROTOCOLTEST wifi_11n_block_ack: PASS