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 1/4] 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 2/4] 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 3/4] 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 4/4] 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.