From bfdff40f49b63fa91610e2c857237f68554ff2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 1/9] ieee80211: normalize final newlines in changed files Keep incidental end-of-file changes separate from the rate-control fixes so their functional diffs contain only the behavior being reviewed. Remove surplus final blank lines from the affected MAC files and terminate the migration guide with a newline. --- doc/src/migration-guide/index.rst | 2 +- src/inet/linklayer/ieee80211/mac/contract/IRateControl.h | 1 - src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc | 1 - src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc | 1 - .../ieee80211/mac/originator/NonQosRecoveryProcedure.cc | 1 - .../ieee80211/mac/originator/NonQosRecoveryProcedure.h | 1 - .../linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc | 1 - .../linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h | 1 - src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h | 1 - src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc | 1 - src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h | 1 - .../linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned | 1 - src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc | 1 - src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h | 1 - 14 files changed, 1 insertion(+), 14 deletions(-) diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 942633d269f..4c83dcc6ebb 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -381,4 +381,4 @@ Computing and verifying checksums is up to the protocol implementations, and it is independent of the actual representation of the header. In general, protocols should have parameters to declare the checksum correct/incorrect or to actually compute and verify it. Of course, for emulation, one should enable computing and -verifying checksums. \ No newline at end of file +verifying checksums. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h index 85a2258ea87..37d13a059d1 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h @@ -36,4 +36,3 @@ class INET_API IRateControl } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index ddb1cf9aa65..105fea0bc80 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -429,4 +429,3 @@ Dcf::~Dcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index e64ce4db852..108145af86d 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -828,4 +828,3 @@ Hcf::~Hcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 4074ff0004c..44d9c05de37 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -247,4 +247,3 @@ bool NonQosRecoveryProcedure::isMulticastFrame(const Ptr Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 2/9] ieee80211agentsta: scope beacon loss to its interface Host-level beacon-loss subscriptions receive notifications from other WLANs. Scanning in response disassociates healthy interfaces and can interrupt an existing scan. Match the notification payload to the containing NIC. Cover independent beacon timeouts and overlapping scans on two WLANs. --- .../ieee80211/mgmt/Ieee80211AgentSta.cc | 3 +- .../module/Ieee80211AgentStaBeaconLoss_1.test | 114 ++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/module/Ieee80211AgentStaBeaconLoss_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc index f98f3f8e699..574da6d080a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc @@ -117,8 +117,7 @@ void Ieee80211AgentSta::receiveSignal(cComponent *source, simsignal_t signalID, printSignalBanner(signalID, obj, details); - if (signalID == l2BeaconLostSignal) { - // TODO should check details if it's about this NIC + if (signalID == l2BeaconLostSignal && obj == getContainingNicModule(this)) { EV << "beacon lost, starting scanning again\n"; getContainingNode(this)->bubble("Beacon lost!"); // sendDisassociateRequest(); diff --git a/tests/module/Ieee80211AgentStaBeaconLoss_1.test b/tests/module/Ieee80211AgentStaBeaconLoss_1.test new file mode 100644 index 00000000000..c4edbde7934 --- /dev/null +++ b/tests/module/Ieee80211AgentStaBeaconLoss_1.test @@ -0,0 +1,114 @@ +%description: +A beacon timeout affects only its own WLAN, including when the other WLAN is +already scanning. Exercise real management timers, host-propagated signals, +and agent-to-management scan commands on two interfaces of one station. + +%file: BeaconLossTest.cc +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" + +namespace inet { +namespace ieee80211 { + +class BeaconLossMgmt : public Ieee80211MgmtSta +{ + public: + int scanCommands = 0; + + void associateUntil(const MacAddress& address, simtime_t timeout) + { + Enter_Method("associateUntil"); + mib->bssData.bssid = address; + mib->bssStationData.isAssociated = true; + assocAP.address = address; + assocAP.channel = 1; + assocAP.ssid = "test"; + assocAP.beaconInterval = SimTime(100, SIMTIME_MS); + // MK_BEACON_TIMEOUT, the production management timer kind. + assocAP.beaconTimeoutMsg = new cMessage("beaconTimeout", 6); + scheduleAfter(timeout, assocAP.beaconTimeoutMsg); + } + + bool associated() const { return mib->bssStationData.isAssociated; } + bool scanningNow() const { return isScanning; } + + protected: + void processScanCommand(Ieee80211Prim_ScanRequest *request) override + { + scanCommands++; + Ieee80211MgmtSta::processScanCommand(request); + } +}; +Define_Module(BeaconLossMgmt); + +class BeaconLossTest : public cSimpleModule +{ + public: + BeaconLossTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto first = check_and_cast(getModuleByPath("^.sta.wlan[0].mgmt")); + auto second = check_and_cast(getModuleByPath("^.sta.wlan[1].mgmt")); + first->associateUntil(MacAddress("02:00:00:00:00:01"), SimTime(1, SIMTIME_US)); + second->associateUntil(MacAddress("02:00:00:00:00:02"), SimTime(3, SIMTIME_US)); + wait(SimTime(2, SIMTIME_US)); + ASSERT(first->scanCommands == 1 && first->scanningNow() && !first->associated()); + ASSERT(second->scanCommands == 0 && !second->scanningNow() && second->associated()); + wait(SimTime(2, SIMTIME_US)); + ASSERT(first->scanCommands == 1 && first->scanningNow()); + ASSERT(second->scanCommands == 1 && second->scanningNow() && !second->associated()); + std::cout << "Beacon loss isolated to the emitting WLAN, including overlapping scans.\n"; + } +}; +Define_Module(BeaconLossTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtSta; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple BeaconLossMgmt extends Ieee80211MgmtSta +{ + parameters: + @class(::inet::ieee80211::BeaconLossMgmt); +} +simple BeaconLossTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::BeaconLossTest); +} +network BeaconLossNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: WirelessHost; + test: BeaconLossTest; +} + +%inifile: omnetpp.ini +[General] +network = BeaconLossNetwork +sim-time-limit = 10us +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.numWlanInterfaces = 2 +*.sta.wlan[*].mgmt.typename = "BeaconLossMgmt" +*.sta.wlan[*].agent.startingTime = 1s +*.sta.wlan[*].agent.activeScan = false +*.sta.wlan[*].agent.channelsToScan = "1" + +%contains: stdout +Beacon loss isolated to the emitting WLAN, including overlapping scans. From de5e514eaf82fc96c076f3c5cf175051d52dc053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 3/9] aarfratecontrol: follow packet feedback semantics Elapsed-time probing and failure handling diverged from INRIA RR-5208, Appendix A. Failed attempts preserved success streaks, triggered fallback too early or ended recovery before a successful transmission. Advance the timer from packet feedback and space ordinary fallback using the MAC retry count. Keep recovery until success, adapt the packet timeout with the success threshold, and cap threshold growth. Only an actual rate increase starts a probe; idle queries cannot change adaptation state. Replace interval with packet-timer parameters and document migration. Cover DCF/HCF feedback, receiver isolation, recovery, timer boundaries and rate bounds, and adapt the showcase configuration to the packet timer. AarfRateControl and InstrumentShowcase both use the corrected controller. Their existing tplx, ~tNl and ~tND expectations in showcases.csv and store.json belong with this correction: the packet-feedback transitions change adaptation and delivery trajectories. Module assertions cover the intended state transitions. Graphical expectations remain unchanged. --- doc/src/migration-guide/index.rst | 18 +++ showcases/wireless/ratecontrol/omnetpp.ini | 4 +- .../mac/ratecontrol/AarfRateControl.cc | 123 +++++++++--------- .../mac/ratecontrol/AarfRateControl.h | 16 +-- .../mac/ratecontrol/AarfRateControl.ned | 11 +- tests/fingerprint/showcases.csv | 4 +- tests/fingerprint/store.json | 24 ++-- .../AarfRateControlFailureStreak_1.test | 95 ++++++++++++++ .../AarfRateControlFreshFrameFeedback_1.test | 120 +++++++++++++++++ .../AarfRateControlGroupAddressed_1.test | 17 +-- .../module/AarfRateControlMaximumRate_1.test | 83 ++++++++++++ .../module/AarfRateControlPerReceiver_1.test | 9 -- tests/module/AarfRateControlRecovery_1.test | 103 +++++++++++++++ .../AarfRateControlRetryFeedback_1.test | 100 ++++++++++++++ .../AarfRateControlSuccessStreak_1.test | 98 ++++++++++++++ .../module/AarfRateControlThresholdCap_1.test | 82 ++++++++++++ .../module/AarfRateControlThresholdOne_1.test | 81 ++++++++++++ .../AarfRateControlTimerFeedback_1.test | 79 +++++++++++ tests/module/AarfRateControlTimerProbe_1.test | 112 ++++++++++++++++ .../AarfRateControlTimerSuccessStreak_1.test | 85 ++++++++++++ .../Ieee80211HtAntennaRateControl_1.test | 2 +- 21 files changed, 1151 insertions(+), 115 deletions(-) create mode 100644 tests/module/AarfRateControlFailureStreak_1.test create mode 100644 tests/module/AarfRateControlFreshFrameFeedback_1.test create mode 100644 tests/module/AarfRateControlMaximumRate_1.test create mode 100644 tests/module/AarfRateControlRecovery_1.test create mode 100644 tests/module/AarfRateControlRetryFeedback_1.test create mode 100644 tests/module/AarfRateControlSuccessStreak_1.test create mode 100644 tests/module/AarfRateControlThresholdCap_1.test create mode 100644 tests/module/AarfRateControlThresholdOne_1.test create mode 100644 tests/module/AarfRateControlTimerFeedback_1.test create mode 100644 tests/module/AarfRateControlTimerProbe_1.test create mode 100644 tests/module/AarfRateControlTimerSuccessStreak_1.test diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 4c83dcc6ebb..63a12835dd2 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -4,6 +4,24 @@ Migrating Code from INET 3.x ============================ Release: |release| +Migrating AARF Rate Control +-------------------------- + +``AarfRateControl`` now follows the packet-timer and recovery transitions in +INRIA RR-5208, Appendix A. Remove assignments to its former ``interval`` parameter +and configure the packet timer through the parameters declared in +:ned:`AarfRateControl`. There is no general conversion from seconds to packet +counts: choose the packet threshold for the experiment's intended probing policy. + +Ordinary fallback now uses each packet's MAC retry count, rather than combining +failures across packets. Recovery persists through failed attempts until success; +a failed probe adapts both the success threshold and packet timeout. Rate queries +and idle time no longer cause probes. These corrections change rate trajectories +and simulation results, so existing studies should be revalidated. + +Subclasses that used the former elapsed-time helpers or the receiver-wide failure +counter must adapt to the new packet-feedback state machine. + Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/showcases/wireless/ratecontrol/omnetpp.ini b/showcases/wireless/ratecontrol/omnetpp.ini index d592b13bead..e936c48e0f5 100644 --- a/showcases/wireless/ratecontrol/omnetpp.ini +++ b/showcases/wireless/ratecontrol/omnetpp.ini @@ -63,10 +63,10 @@ sim-time-limit = 25s *.*Host.wlan[*].mac.*.rateSelection.dataFrameBitrate = 54Mbps # use 54Mbps rate when not using rate control [Config AarfRateControl] -seed-set = 1 # only for unchanged fingerprints +seed-set = 1 *.*Host.wlan[*].mac.*.rateControl.typename = "AarfRateControl" *.*Host.wlan[*].mac.*.rateControl.initialRate = 54Mbps *.*Host.wlan[*].mac.*.rateSelection.dataFrameBitrate = -1bps # let ratecontrol set the rate *.*Host.wlan[*].mac.dcf.rateControl.increaseThreshold = 20 *.*Host.wlan[*].mac.dcf.rateControl.decreaseThreshold = 5 -*.*Host.wlan[*].mac.dcf.rateControl.interval = 1s +*.*Host.wlan[*].mac.dcf.rateControl.minTimerThreshold = 30 diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc index 0b2e47b79f7..d7ac2a95f4d 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc @@ -5,6 +5,9 @@ // +#include +#include + #include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" namespace inet { @@ -19,16 +22,18 @@ void AarfRateControl::initialize(int stage) RateControlBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { factor = par("increaseThresholdFactor"); + initialIncreaseThreshold = par("increaseThreshold"); maxIncreaseThreshold = par("maxIncreaseThreshold"); decreaseThreshold = par("decreaseThreshold"); - interval = par("interval"); + minTimerThreshold = par("minTimerThreshold"); + timerThresholdFactor = par("timerThresholdFactor"); + if (initialIncreaseThreshold < 1 || maxIncreaseThreshold < initialIncreaseThreshold || + decreaseThreshold < 1 || minTimerThreshold < 1 || + !std::isfinite(factor) || factor < 1 || + !std::isfinite(timerThresholdFactor) || timerThresholdFactor < 1 || + timerThresholdFactor * maxIncreaseThreshold > std::numeric_limits::max()) + throw cRuntimeError("Invalid AARF thresholds or factors"); WATCH_EXPR("numStations", (int)stations.size()); - WATCH(factor); - WATCH(maxIncreaseThreshold); - WATCH(decreaseThreshold); - WATCH(interval); - } - else if (stage == INITSTAGE_LINK_LAYER) { } } @@ -44,8 +49,8 @@ AarfRateControl::State& AarfRateControl::getState(const MacAddress& receiverAddr State state; state.address = receiverAddress; state.mode = getInitialMode(); - state.increaseThreshold = par("increaseThreshold"); - state.timer = simTime(); // the interval starts when the station is first seen, not at t=0 + state.increaseThreshold = initialIncreaseThreshold; + state.timerThreshold = minTimerThreshold; it = stations.insert({receiverAddress, state}).first; emitDatarateChangedSignal(state.address, state.mode); } @@ -54,63 +59,58 @@ AarfRateControl::State& AarfRateControl::getState(const MacAddress& receiverAddr void AarfRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) { + Enter_Method("frameTransmitted"); State& state = getState(getReceiverAddress(frame)); - increaseRateIfTimerIsExpired(state); - - if (!isSuccessful && state.probing) { // probing packet failed - state.numberOfConsSuccTransmissions = 0; - state.mode = decreaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.address, state.mode); - EV_DETAIL << "Decreased rate to " << *state.mode << endl; - multiplyIncreaseThreshold(state, factor); - resetTimer(state); + auto previousMode = state.mode; + + // Lacage et al., INRIA RR-5208, Appendix A, pp. 22-24. + // Retry counts belong to the packet's MAC recovery procedure, whereas the + // adaptive thresholds and recovery phase persist for this receiver. + if (isSuccessful) { + if (state.numberOfConsSuccTransmissions < state.increaseThreshold) + state.numberOfConsSuccTransmissions++; + auto fasterMode = increaseRateIfPossible(state.mode); + if ((state.numberOfConsSuccTransmissions >= state.increaseThreshold || state.timer >= state.timerThreshold) && + fasterMode != state.mode) { + state.mode = fasterMode; + state.timer = 0; + state.numberOfConsSuccTransmissions = 0; + state.probing = true; + } + else { + if (state.timer < state.timerThreshold) + state.timer++; + state.probing = false; + } } - else if (!isSuccessful && retryCount >= decreaseThreshold - 1) { // decreaseThreshold consecutive failed transmissions + else { + ASSERT(retryCount >= 1); + if (state.timer < state.timerThreshold) + state.timer++; state.numberOfConsSuccTransmissions = 0; - state.mode = decreaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.address, state.mode); - EV_DETAIL << "Decreased rate to " << *state.mode << endl; - resetIncreaseThreshdold(state); - resetTimer(state); + if (state.probing) { + state.timer = 0; + if (retryCount == 1) { + state.increaseThreshold = std::min(state.increaseThreshold * factor, double(maxIncreaseThreshold)); + state.timerThreshold = std::max(int(timerThresholdFactor * state.increaseThreshold), minTimerThreshold); + state.mode = decreaseRateIfPossible(state.mode); + } + } + else { + // Generalize the appendix's retries 2, 4, 6, 8, 10 to the configured + // spacing and MAC retry limit, without owning a second retry counter. + if (retryCount % decreaseThreshold == 0) { + state.increaseThreshold = initialIncreaseThreshold; + state.timerThreshold = minTimerThreshold; + state.mode = decreaseRateIfPossible(state.mode); + } + if (retryCount >= decreaseThreshold) + state.timer = 0; + } } - else if (isSuccessful && retryCount == 0) - state.numberOfConsSuccTransmissions++; - - if (state.numberOfConsSuccTransmissions == state.increaseThreshold) { - state.numberOfConsSuccTransmissions = 0; - state.mode = increaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.address, state.mode); - EV_DETAIL << "Increased rate to " << *state.mode << endl; - resetTimer(state); - state.probing = true; - } - else - state.probing = false; -} - -void AarfRateControl::multiplyIncreaseThreshold(State& state, double factor) -{ - if (state.increaseThreshold * factor <= maxIncreaseThreshold) - state.increaseThreshold *= factor; -} - -void AarfRateControl::resetIncreaseThreshdold(State& state) -{ - state.increaseThreshold = par("increaseThreshold"); -} - -void AarfRateControl::resetTimer(State& state) -{ - state.timer = simTime(); -} - -void AarfRateControl::increaseRateIfTimerIsExpired(State& state) -{ - if (simTime() - state.timer >= interval) { - state.mode = increaseRateIfPossible(state.mode); + if (state.mode != previousMode) { emitDatarateChangedSignal(state.address, state.mode); - EV_DETAIL << "Increased rate to " << *state.mode << endl; - resetTimer(state); + EV_DETAIL << "Changed rate to " << *state.mode << endl; } } @@ -122,7 +122,6 @@ const IIeee80211Mode *AarfRateControl::getRate(const MacAddress& receiverAddress { Enter_Method("getRate"); State& state = getState(receiverAddress); - increaseRateIfTimerIsExpired(state); EV_INFO << "The current mode is " << state.mode << " the net bitrate is " << state.mode->getDataMode()->getNetBitrate() << std::endl; return state.mode; } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h index eeb35081f64..c74b273255c 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h @@ -19,19 +19,22 @@ namespace ieee80211 { class INET_API AarfRateControl : public RateControlBase { protected: - // Per-receiver adaptive state (formerly single-instance module members). + // Per-receiver state from RR-5208, Appendix A. struct State { MacAddress address; // the receiver this state belongs to (for per-station rate attribution) const physicallayer::IIeee80211Mode *mode = nullptr; - simtime_t timer = SIMTIME_ZERO; - bool probing = false; + int timer = 0; + int timerThreshold = -1; + bool probing = false; // recovery persists through failures until a successful transmission int increaseThreshold = -1; int numberOfConsSuccTransmissions = 0; }; std::map stations; // configuration, shared across stations - simtime_t interval = SIMTIME_ZERO; + int initialIncreaseThreshold = -1; + int minTimerThreshold = -1; + double timerThresholdFactor = -1; int maxIncreaseThreshold = -1; int decreaseThreshold = -1; double factor = -1; @@ -44,11 +47,6 @@ class INET_API AarfRateControl : public RateControlBase virtual State& getState(const MacAddress& receiverAddress); virtual void resetRateControl() override { stations.clear(); } - virtual void multiplyIncreaseThreshold(State& state, double factor); - virtual void resetIncreaseThreshdold(State& state); - virtual void resetTimer(State& state); - virtual void increaseRateIfTimerIsExpired(State& state); - public: virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned index 9ec36a21ebc..1eed1766d9f 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned @@ -14,16 +14,19 @@ import inet.linklayer.ieee80211.mac.contract.IRateControl; // Implements the Adaptive ARF (AARF) rate control mechanism, which was // initially described in IEEE 802.11 Rate Adaptation: A Practical Approach, // by M. Lacage, M.H. Manshaei, and T. Turletti, 2004. +// Follows INRIA RR-5208 Appendix A with per-receiver state. Counters saturate +// and threshold checks use >=; ordinary fallback extends to every configured +// retry multiple, beyond the appendix's explicit list ending at ten. // simple AarfRateControl extends SimpleModule like IRateControl { parameters: @class(AarfRateControl); double initialRate @unit(bps) = default(-1bps); // -1 means the fastest mandatory rate - double interval @unit(s) = default(50ms); // The rate (unconditionally) increases after each time interval. - int increaseThreshold = default(10); // Number of successful transmissions needed to increase the rate - int decreaseThreshold = default(2); // Number of consecutive unsuccessful transmissions (in the sense that the ACK does not arrive within the timeout) - // needed to increase the rate + int minTimerThreshold = default(15); // Minimum packet timer threshold (>0); checked on success before counting that attempt. Idle time and rate queries do not advance it. + double timerThresholdFactor = default(1.5); // On a failed probe, timer threshold becomes max(minTimerThreshold, floor(factor * updated increaseThreshold)); finite and >=1. + int increaseThreshold = default(10); // Consecutive successful attempts needed to increase the rate (>0) + int decreaseThreshold = default(2); // Outside recovery, decrease on each multiple of this per-packet retry count (>0) double increaseThresholdFactor = default(2); // When the transmission of the probing packet fails, increaseThreshold is multiplied by increaseThresholdFactor. int maxIncreaseThreshold = default(50); // Upper bound for increaseThreshold. displayStringTextFormat = default("{numStations} stations"); diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index ef401f4edb1..86efc7b1838 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -130,7 +130,7 @@ /showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c PacketCounts -r 0, 5s, 271c-6821/tplx;3c0c-db82/~tNl;d4d4-a4d5/~tND;b145-6576/tyf, PASS, wireless Ipv4 # VisualizingSubmoduleInformation extended /showcases/visualizer/canvas/submoduleinfo/, -f omnetpp.ini -c MACStates -r 0, 5s, 271c-6821/tplx;3c0c-db82/~tNl;d4d4-a4d5/~tND;4677-fc97/tyf, PASS, wireless Ipv4 -/showcases/visualizer/canvas/instrumentfigures/, -f omnetpp.ini -c General -r 0, 3s, 6622-adb9/tplx;28ca-9f8c/~tNl;adfa-1bdd/~tND;4266-0d42/tyf, PASS, wireless Ipv4 +/showcases/visualizer/canvas/instrumentfigures/, -f omnetpp.ini -c General -r 0, 3s, 57b6-e4d8/tplx;94c4-61d4/~tNl;53c6-f75e/~tND;4266-0d42/tyf, PASS, wireless Ipv4 /showcases/visualizer/canvas/interfacetable/, -f omnetpp.ini -c EnablingVisualization -r 0, 5s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND;0000-0000/tyf, PASS, /showcases/visualizer/canvas/interfacetable/, -f omnetpp.ini -c AdvancedFeatures -r 0, 5s, dfdf-7e0e/tplx;7bfc-e297/~tNl;a42e-6cc2/~tND;b2d8-b329/tyf, PASS, wireless EthernetMac Ipv4 @@ -317,7 +317,7 @@ /showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 1a49-72b3/tplx;e605-d79d/~tNl;10fc-7bb0/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c NoRateControl -r 0, 14s, 7ee9-503a/tplx;0816-e58f/~tNl;648e-6e84/~tND;dad3-7f89/tyf, PASS, wireless Ipv4 -/showcases/wireless/ratecontrol/, -f omnetpp.ini -c AarfRateControl -r 0, 12s, a7bc-05bb/tplx;9de0-4dd3/~tNl;1209-101b/~tND;7539-d32d/tyf, PASS, wireless Ipv4 +/showcases/wireless/ratecontrol/, -f omnetpp.ini -c AarfRateControl -r 0, 12s, 4e2e-ee4e/tplx;baf5-19a2/~tNl;d107-c7ed/~tND;7539-d32d/tyf, PASS, wireless Ipv4 # /showcases/wireless/scaling/, -f omnetpp.ini -c PingBase -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, PASS, # PingBase extended # /showcases/wireless/scaling/, -f omnetpp.ini -c UDPBase -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, PASS, # UDPBase extended diff --git a/tests/fingerprint/store.json b/tests/fingerprint/store.json index 321b6b5c23a..1d8c8e1c730 100644 --- a/tests/fingerprint/store.json +++ b/tests/fingerprint/store.json @@ -41947,8 +41947,8 @@ "sim_time_limit": "3s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "6622-adb9", - "timestamp": 1681992799.6309373, + "fingerprint": "57b6-e4d8", + "timestamp": 1789046750.6735091, "itervars": "$repetition==0" }, { @@ -41971,8 +41971,8 @@ "sim_time_limit": "3s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "3417-d991", - "timestamp": 1681992799.631032, + "fingerprint": "53c6-f75e", + "timestamp": 1789046750.6735148, "itervars": "$repetition==0" }, { @@ -41983,8 +41983,8 @@ "sim_time_limit": "3s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "28ca-9f8c", - "timestamp": 1681992799.6309953, + "fingerprint": "94c4-61d4", + "timestamp": 1789046750.6735165, "itervars": "$repetition==0" }, { @@ -46399,8 +46399,8 @@ "sim_time_limit": "12s", "test_result": "PASS", "ingredients": "tplx", - "fingerprint": "a7bc-05bb", - "timestamp": 1681992799.698713, + "fingerprint": "4e2e-ee4e", + "timestamp": 1789046750.6735425, "itervars": "$repetition==0" }, { @@ -46423,8 +46423,8 @@ "sim_time_limit": "12s", "test_result": "PASS", "ingredients": "~tND", - "fingerprint": "b5e0-5707", - "timestamp": 1681992799.6988778, + "fingerprint": "d107-c7ed", + "timestamp": 1789046750.673544, "itervars": "$repetition==0" }, { @@ -46435,8 +46435,8 @@ "sim_time_limit": "12s", "test_result": "PASS", "ingredients": "~tNl", - "fingerprint": "9de0-4dd3", - "timestamp": 1681992799.6987863, + "fingerprint": "baf5-19a2", + "timestamp": 1789046750.6735446, "itervars": "$repetition==0" }, { diff --git a/tests/module/AarfRateControlFailureStreak_1.test b/tests/module/AarfRateControlFailureStreak_1.test new file mode 100644 index 00000000000..61b81a0542a --- /dev/null +++ b/tests/module/AarfRateControlFailureStreak_1.test @@ -0,0 +1,95 @@ +%description: +RR-5208 Appendix A: per-packet retry spacing, receiver isolation, minimum rate and mode-set reset. + +%file: FeedbackTest.cc +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress otherReceiver("02:00:00:00:00:02"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + // RR-5208 Appendix A: retry is local to each packet. + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 1, false, true); + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 2, false, false); + ASSERT(rate() == 18e6); + control->frameTransmitted(frame, 3, false, false); + ASSERT(rate() == 18e6); + control->frameTransmitted(frame, 4, false, true); + ASSERT(rate() == 12e6); + ASSERT(control->getRate(otherReceiver)->getDataMode()->getNetBitrate().get() == 24e6); + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate() == 12e6); + for (int retry = 1; retry <= 10; retry++) + control->frameTransmitted(frame, retry, false, retry == 10); + ASSERT(rate() == 6e6); + auto modes = const_cast(physicallayer::Ieee80211ModeSet::getModeSet("g(erp)")); + getModuleByPath("^.sta.wlan[0]")->emit(modesetChangedSignal, modes); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate() == 24e6); + delete frame; + std::cout << "AarfRateControlFailureStreak verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 1ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 100000 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlFailureStreak verified. diff --git a/tests/module/AarfRateControlFreshFrameFeedback_1.test b/tests/module/AarfRateControlFreshFrameFeedback_1.test new file mode 100644 index 00000000000..536feca6628 --- /dev/null +++ b/tests/module/AarfRateControlFreshFrameFeedback_1.test @@ -0,0 +1,120 @@ +%description: +Verify AARF's decrease threshold using actual DCF and HCF failed transmissions. +The receiver is below sensitivity at all rates. Both MACs must report failure +count 1 for each fresh packet; Appendix A must not combine these into one retry sequence. +The first two HCF failures also belong to distinct video/voice access categories. + +%file: RetryFeedback.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { +class RetryFeedbackControl : public AarfRateControl +{ + protected: + using cListener::finish; + int failures = 0; + int firstTid = -1; + + void finish() override + { + ASSERT(failures >= 2); + std::cout << getFullPath() << ": retry threshold verified.\n"; + } + + public: + void frameTransmitted(Packet *frame, int retryCount, bool successful, bool givenUp) override + { + ASSERT(!successful); + failures++; + ASSERT(retryCount == 1); + ASSERT(givenUp); + auto header = frame->peekAtFront(); + if (header->getType() == ST_DATA_WITH_QOS) { + if (failures == 1) + firstTid = header->getTid(); + else if (failures == 2) + ASSERT(header->getTid() != firstTid); + } + AarfRateControl::frameTransmitted(frame, retryCount, successful, givenUp); + double rate = getRate(getReceiverAddress(frame))->getDataMode()->getNetBitrate().get(); + if (failures == 1) + ASSERT(rate == 24e6); + else if (failures == 2) + ASSERT(rate == 24e6); + } +}; +Define_Module(RetryFeedbackControl); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.linklayer.ieee80211.mac.ratecontrol.AarfRateControl; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple RetryFeedbackControl extends AarfRateControl +{ + parameters: + @class(::inet::ieee80211::RetryFeedbackControl); +} +network RetryFeedbackNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source[2]: AdhocHost; + sink: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +network = RetryFeedbackNetwork +sim-time-limit = 50ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +*.source[*].mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +*.*.ipv4.arp.typename = "GlobalArp" +*.source[1].wlan[0].mac.qosStation = true +*.source[0].wlan[0].mac.dcf.rateControl.typename = "RetryFeedbackControl" +*.source[1].wlan[0].mac.hcf.rateControl.typename = "RetryFeedbackControl" +*.source[*].wlan[0].mac.*.rateControl.initialRate = 24Mbps +*.source[*].wlan[0].mac.*.rateControl.minTimerThreshold = 100000 +*.source[*].wlan[0].mac.*.rateControl.decreaseThreshold = 2 +*.source[*].wlan[0].mac.*.rateSelection.dataFrameBitrate = -1bps +*.sink.mobility.initialX = 100m +*.sink.wlan[0].radio.receiver.sensitivity = 0dBm +# HCF alternates video and voice access categories towards the same receiver. +*.source[1].wlan[0].classifier.typename = "QosClassifier" +*.source[1].numApps = 2 +*.source[1].app[0].sendInterval = 1s +*.source[1].app[1].typename = "UdpBasicApp" +*.source[1].app[1].destAddresses = "sink" +*.source[1].app[1].destPort = 4000 +*.source[1].app[1].messageLength = 100B +*.source[1].app[1].startTime = 11ms +*.source[1].app[1].stopTime = 12ms +*.source[1].app[1].sendInterval = 1s +*.source[*].numApps = 1 +*.source[*].app[0].typename = "UdpBasicApp" +*.source[*].app[0].destAddresses = "sink" +*.source[*].app[0].destPort = 5000 +*.source[*].app[0].messageLength = 100B +*.source[*].app[0].startTime = 1ms +*.source[*].app[0].stopTime = 22ms +*.source[*].app[0].sendInterval = 10ms +**.shortRetryLimit = 1 +**.longRetryLimit = 1 + +%contains: stdout +source[0].wlan[0].mac.dcf.rateControl: retry threshold verified. +%contains: stdout +source[1].wlan[0].mac.hcf.rateControl: retry threshold verified. diff --git a/tests/module/AarfRateControlGroupAddressed_1.test b/tests/module/AarfRateControlGroupAddressed_1.test index edfcfe9712d..00b4ddb1f5d 100644 --- a/tests/module/AarfRateControlGroupAddressed_1.test +++ b/tests/module/AarfRateControlGroupAddressed_1.test @@ -1,17 +1,6 @@ %description: -Tests that AarfRateControl keeps no adaptive state for a group-addressed receiver. +Group-addressed traffic bypasses AARF: no per-receiver rate state or rate signals. -A group-addressed frame is never acknowledged, so the rate control never gets feedback about -it. If such a frame were served from adaptive state of its own, that state would be created on -the first broadcast and then raised once per interval by the periodic increase, with nothing -able to lower it again. - -The source sends a unicast stream to sink and a broadcast stream at the same time. Every rate -the rate control reports must therefore belong to sink: the count of the aggregate -datarateChanged statistic must equal the count of sink's own dataratePerStation series, because -a rate emitted for a group address would be untagged and would show up only in the aggregate. - -%#-------------------------------------------------------------------------------------------------------------- %file: test.ned import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; import inet.node.inet.AdhocHost; @@ -53,10 +42,10 @@ cmdenv-express-mode = true *.sink.mobility.initialY = 0m *.sink.mobility.initialZ = 0m -# a short interval, so an unfed group-addressed state would be raised many times over the run +# Rate control must remain unused for group-addressed data. *.source.wlan[*].mac.dcf.rateControl.typename = "AarfRateControl" *.source.wlan[*].mac.dcf.rateControl.initialRate = 6Mbps -*.source.wlan[*].mac.dcf.rateControl.interval = 0.05s +*.source.wlan[*].mac.dcf.rateControl.minTimerThreshold = 100000 *.source.wlan[*].mac.dcf.rateSelection.dataFrameBitrate = -1bps # a unicast stream; the group-addressed frames come from ARP, which broadcasts its requests diff --git a/tests/module/AarfRateControlMaximumRate_1.test b/tests/module/AarfRateControlMaximumRate_1.test new file mode 100644 index 00000000000..c004f83d341 --- /dev/null +++ b/tests/module/AarfRateControlMaximumRate_1.test @@ -0,0 +1,83 @@ +%description: +At the maximum rate, reaching the success threshold must not arm a failed probe. +Ordinary failure-threshold fallback still applies. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress otherReceiver("02:00:00:00:00:02"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 54e6); + // Cross both success and timer thresholds at the maximum rate. + for (int i = 0; i < 30; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 54e6); + control->frameTransmitted(frame, 1, false, false); + wait(SimTime(60, SIMTIME_MS)); + ASSERT(rate() == 54e6); + control->frameTransmitted(frame, 2, false, true); + ASSERT(rate() == 48e6); + delete frame; + std::cout << "AarfRateControlMaximumRate verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 200ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 54Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 15 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlMaximumRate verified. diff --git a/tests/module/AarfRateControlPerReceiver_1.test b/tests/module/AarfRateControlPerReceiver_1.test index 51f1c1c1c7a..4a1199f07f3 100644 --- a/tests/module/AarfRateControlPerReceiver_1.test +++ b/tests/module/AarfRateControlPerReceiver_1.test @@ -7,15 +7,6 @@ away on a link poor enough that frames to it are lost and retried. The rate towa the near peer must therefore stay at the initial 54 Mbps, while the rate towards the far peer falls well below it, and both peers must keep receiving. -Measured here: 54 Mbps towards the near peer and 24.7 Mbps towards the far one, with -3653 and 1350 packets delivered. - -With one set of adaptive state per module (the behavior before rate control was keyed -on the receiver) the two peers share a single rate, which the far peer's retries drag -down to a blend of the two: 29.6 Mbps time-average, with the near peer served at that -blended rate instead of the 54 Mbps its own link supports, delivering 3080 packets to -the near peer and only 380 to the far one. - %#-------------------------------------------------------------------------------------------------------------- %file: test.ned import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; diff --git a/tests/module/AarfRateControlRecovery_1.test b/tests/module/AarfRateControlRecovery_1.test new file mode 100644 index 00000000000..83358dbb008 --- /dev/null +++ b/tests/module/AarfRateControlRecovery_1.test @@ -0,0 +1,103 @@ +%description: +RR-5208 Appendix A: recovery retry spacing, exhaustion, and successful-probe exit. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + // RR-5208 Appendix A: recovery persists until a success, even + // across retry exhaustion. Only retry 1 invokes recovery fallback. + for (int i = 0; i < 10; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 24e6); + for (int retry = 2; retry <= 4; retry++) { + control->frameTransmitted(frame, retry, false, retry == 4); + ASSERT(rate() == 24e6); + } + // Next packet still enters recovery: threshold doubles to 40. + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 18e6); + control->frameTransmitted(frame, 2, false, false); + ASSERT(rate() == 18e6); + control->frameTransmitted(frame, 2, true, false); + for (int i = 0; i < 38; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 18e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + // Successful probe exits recovery. Ordinary fallback restores ten. + control->frameTransmitted(frame, 0, true, false); + control->frameTransmitted(frame, 1, false, false); + control->frameTransmitted(frame, 2, false, true); + ASSERT(rate() == 18e6); + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 18e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + delete frame; + std::cout << "AarfRateControlRecovery verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 1ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 100000 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlRecovery verified. diff --git a/tests/module/AarfRateControlRetryFeedback_1.test b/tests/module/AarfRateControlRetryFeedback_1.test new file mode 100644 index 00000000000..7348316d4e0 --- /dev/null +++ b/tests/module/AarfRateControlRetryFeedback_1.test @@ -0,0 +1,100 @@ +%description: +Verify AARF's decrease threshold using actual DCF and HCF failed transmissions. +The receiver is below sensitivity at all rates. Both MACs must report failure +counts 1 through 4; Appendix A lowers the rate at retry 2 and 4, not 3. + +%file: RetryFeedback.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { +class RetryFeedbackControl : public AarfRateControl +{ + protected: + using cListener::finish; + int failures = 0; + + void finish() override + { + ASSERT(failures >= 4); + std::cout << getFullPath() << ": retry threshold verified.\n"; + } + + public: + void frameTransmitted(Packet *frame, int retryCount, bool successful, bool givenUp) override + { + ASSERT(!successful); + failures++; + if (failures <= 4) + ASSERT(retryCount == failures); + AarfRateControl::frameTransmitted(frame, retryCount, successful, givenUp); + double rate = getRate(getReceiverAddress(frame))->getDataMode()->getNetBitrate().get(); + if (failures == 1) + ASSERT(rate == 24e6); + else if (failures == 2 || failures == 3) + ASSERT(rate == 18e6); + else if (failures == 4) + ASSERT(rate == 12e6); + } +}; +Define_Module(RetryFeedbackControl); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.linklayer.ieee80211.mac.ratecontrol.AarfRateControl; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple RetryFeedbackControl extends AarfRateControl +{ + parameters: + @class(::inet::ieee80211::RetryFeedbackControl); +} +network RetryFeedbackNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source[2]: AdhocHost; + sink: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +network = RetryFeedbackNetwork +sim-time-limit = 50ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +*.source[*].mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +*.*.ipv4.arp.typename = "GlobalArp" +*.source[1].wlan[0].mac.qosStation = true +*.source[0].wlan[0].mac.dcf.rateControl.typename = "RetryFeedbackControl" +*.source[1].wlan[0].mac.hcf.rateControl.typename = "RetryFeedbackControl" +*.source[*].wlan[0].mac.*.rateControl.initialRate = 24Mbps +*.source[*].wlan[0].mac.*.rateControl.minTimerThreshold = 100000 +*.source[*].wlan[0].mac.*.rateControl.decreaseThreshold = 2 +*.source[*].wlan[0].mac.*.rateSelection.dataFrameBitrate = -1bps +*.sink.mobility.initialX = 100m +*.sink.wlan[0].radio.receiver.sensitivity = 0dBm +*.source[*].numApps = 1 +*.source[*].app[0].typename = "UdpBasicApp" +*.source[*].app[0].destAddresses = "sink" +*.source[*].app[0].destPort = 5000 +*.source[*].app[0].messageLength = 100B +*.source[*].app[0].startTime = 1ms +*.source[*].app[0].stopTime = 2ms +*.source[*].app[0].sendInterval = 1s + +%contains: stdout +source[0].wlan[0].mac.dcf.rateControl: retry threshold verified. +%contains: stdout +source[1].wlan[0].mac.hcf.rateControl: retry threshold verified. diff --git a/tests/module/AarfRateControlSuccessStreak_1.test b/tests/module/AarfRateControlSuccessStreak_1.test new file mode 100644 index 00000000000..07b75495816 --- /dev/null +++ b/tests/module/AarfRateControlSuccessStreak_1.test @@ -0,0 +1,98 @@ +%description: +Exercise initialized rate-control modules through the public feedback and rate +query APIs. Fixed feedback sequences isolate adaptation from channel randomness. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress otherReceiver("02:00:00:00:00:02"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 24e6); + ASSERT(control->getRate(otherReceiver)->getDataMode()->getNetBitrate().get() == 24e6); + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + // A failure below the fallback threshold still breaks the streak. + control->frameTransmitted(frame, 1, false, false); + control->frameTransmitted(frame, 1, true, false); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + // The successful retry starts the new streak (Lacage et al., section 4.2). + // Ten successful attempts increase the rate; failed probing still falls back + // immediately, independently of the ordinary decrease threshold. + for (int i = 0; i < 8; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 24e6); + // The failed probe doubles the success threshold to twenty. + for (int i = 0; i < 19; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + ASSERT(control->getRate(otherReceiver)->getDataMode()->getNetBitrate().get() == 24e6); + delete frame; + std::cout << "AarfRateControlSuccessStreak verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 1ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 100000 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 5 + +%contains: stdout +AarfRateControlSuccessStreak verified. diff --git a/tests/module/AarfRateControlThresholdCap_1.test b/tests/module/AarfRateControlThresholdCap_1.test new file mode 100644 index 00000000000..144160da746 --- /dev/null +++ b/tests/module/AarfRateControlThresholdCap_1.test @@ -0,0 +1,82 @@ +%description: +Lacage et al., section 4.2: double the threshold, saturating at 50. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + // Lacage et al., section 4.2: double the threshold, saturating at 50. + for (int threshold : {10, 20, 40, 50, 50}) { + ASSERT(rate() == 24e6); + for (int i = 0; i < threshold - 1; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 24e6); + } + delete frame; + std::cout << "AarfRateControlThresholdCap verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 1ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 100000 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlThresholdCap verified. diff --git a/tests/module/AarfRateControlThresholdOne_1.test b/tests/module/AarfRateControlThresholdOne_1.test new file mode 100644 index 00000000000..6171f715522 --- /dev/null +++ b/tests/module/AarfRateControlThresholdOne_1.test @@ -0,0 +1,81 @@ +%description: +Threshold one lowers the rate on every failed callback, including fresh frames. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress otherReceiver("02:00:00:00:00:02"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 54e6); + for (int i = 0; i < 10; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 54e6); + // Threshold one must fall back on every failure. + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate() == 48e6); + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate() == 36e6); + delete frame; + std::cout << "AarfRateControlThresholdOne verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 1ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 54Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 100000 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 1 + +%contains: stdout +AarfRateControlThresholdOne verified. diff --git a/tests/module/AarfRateControlTimerFeedback_1.test b/tests/module/AarfRateControlTimerFeedback_1.test new file mode 100644 index 00000000000..6d798484beb --- /dev/null +++ b/tests/module/AarfRateControlTimerFeedback_1.test @@ -0,0 +1,79 @@ +%description: +Idle time and rate queries cannot trigger a probe or erase failed-attempt feedback. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 24e6); + wait(SimTime(60, SIMTIME_MS)); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 1, false, false); + wait(SimTime(60, SIMTIME_MS)); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 2, false, true); + ASSERT(rate() == 18e6); + delete frame; + std::cout << "AarfRateControlTimerFeedback verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 200ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 15 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlTimerFeedback verified. diff --git a/tests/module/AarfRateControlTimerProbe_1.test b/tests/module/AarfRateControlTimerProbe_1.test new file mode 100644 index 00000000000..fb1a18b4968 --- /dev/null +++ b/tests/module/AarfRateControlTimerProbe_1.test @@ -0,0 +1,112 @@ +%description: +RR-5208 Appendix A: packet timer boundary, successful-feedback trigger and adaptive timeout after failed probing. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 24e6); + wait(SimTime(60, SIMTIME_MS)); + ASSERT(rate() == 24e6); + // Nine successes, one failed attempt, then five successes: timer=15, + // success=5. Appendix A tests the timer before counting the next success. + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + control->frameTransmitted(frame, 1, false, false); + for (int i = 0; i < 5; i++) + control->frameTransmitted(frame, i == 0 ? 1 : 0, true, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 24e6); + // Failed probe: success threshold=20, timer threshold=30. + // A successful retry exits recovery, then isolated losses break streaks. + control->frameTransmitted(frame, 1, true, false); + for (int i = 0; i < 14; i++) { + control->frameTransmitted(frame, 1, false, false); + control->frameTransmitted(frame, 1, true, false); + ASSERT(rate() == 24e6); + } + control->frameTransmitted(frame, 1, false, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 1, true, false); + ASSERT(rate() == 36e6); + // Successful probe, then ordinary fallback resets the adaptive timeout + // from 30 to 15 as well as restoring the success threshold to ten. + control->frameTransmitted(frame, 0, true, false); + control->frameTransmitted(frame, 1, false, false); + control->frameTransmitted(frame, 2, false, true); + ASSERT(rate() == 24e6); + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + control->frameTransmitted(frame, 1, false, false); + for (int i = 0; i < 5; i++) + control->frameTransmitted(frame, i == 0 ? 1 : 0, true, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + delete frame; + std::cout << "AarfRateControlTimerProbe verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 200ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 15 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlTimerProbe verified. diff --git a/tests/module/AarfRateControlTimerSuccessStreak_1.test b/tests/module/AarfRateControlTimerSuccessStreak_1.test new file mode 100644 index 00000000000..6068e8c2b83 --- /dev/null +++ b/tests/module/AarfRateControlTimerSuccessStreak_1.test @@ -0,0 +1,85 @@ +%description: +Packet-timer probing resets success and timer counts for the new rate. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 24e6); + // A short configured timer starts the probe on the fourth success. + for (int i = 0; i < 3; i++) + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + // Old successes and timer counts cannot carry into the next rate. + for (int i = 0; i < 3; i++) { + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 36e6); + } + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 48e6); + delete frame; + std::cout << "AarfRateControlTimerSuccessStreak verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 200ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 3 +*.sta.wlan[0].mac.dcf.rateControl.decreaseThreshold = 2 + +%contains: stdout +AarfRateControlTimerSuccessStreak verified. diff --git a/tests/module/Ieee80211HtAntennaRateControl_1.test b/tests/module/Ieee80211HtAntennaRateControl_1.test index 0518558627c..85a12e64fde 100644 --- a/tests/module/Ieee80211HtAntennaRateControl_1.test +++ b/tests/module/Ieee80211HtAntennaRateControl_1.test @@ -259,7 +259,7 @@ record-scalar-results = false *.sta.wlan[0].mac.dcf.rateControl.typename = "RecordingAarfRateControl" *.sta.wlan[0].mac.dcf.rateControl.initialRate = 65Mbps -*.sta.wlan[0].mac.dcf.rateControl.interval = 1ms +*.sta.wlan[0].mac.dcf.rateControl.minTimerThreshold = 1 *.sta.wlan[0].mac.dcf.rateControl.increaseThreshold = 1 *.sta.wlan[0].mac.dcf.rateSelection.dataFrameBitrate = -1bps From b0c4cda9471d7af6036fd1f899f802b0c37ff4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 4/9] onoeratecontrol: adapt from completed packet samples Counting individual retries mixed unfinished packets into samples and could count a failure again at success. Small samples earned credit too quickly, and failed-only intervals could leave the controller at an unusable rate. Follow MadWifi ath_rate/onoe/onoe.c at revision a7531fd223a1f454d3fd74a975b4581cde5411bb for completed normal-ACK feedback. Consume terminal recovery counts once before evaluating a due sample. Require ten completions for ordinary decisions, compare retries directly with successes, prohibit upward credit after errors, and retain small samples unless the rate changes. Failed-only samples can lower the rate. Preserve integer truncation and bounded credit, suppress false rate-change signals, and keep queries from consuming deadlines. Cover arithmetic and sample boundaries, receiver isolation, mode resets and real DCF/HCF completion counts, including interleaved HCF retries. --- .../mac/ratecontrol/OnoeRateControl.cc | 57 +++-- .../mac/ratecontrol/OnoeRateControl.h | 13 +- .../mac/ratecontrol/OnoeRateControl.ned | 11 +- .../OnoeRateControlCompletionFeedback_1.test | 172 ++++++++++++++ .../OnoeRateControlFailureInterval_1.test | 149 ++++++++++++ .../OnoeRateControlInterleavedFeedback_1.test | 169 ++++++++++++++ .../OnoeRateControlRetryAccounting_1.test | 133 +++++++++++ .../OnoeRateControlRetryFeedback_1.test | 166 ++++++++++++++ tests/module/OnoeRateControlSampling_1.test | 217 ++++++++++++++++++ 9 files changed, 1054 insertions(+), 33 deletions(-) create mode 100644 tests/module/OnoeRateControlCompletionFeedback_1.test create mode 100644 tests/module/OnoeRateControlFailureInterval_1.test create mode 100644 tests/module/OnoeRateControlInterleavedFeedback_1.test create mode 100644 tests/module/OnoeRateControlRetryAccounting_1.test create mode 100644 tests/module/OnoeRateControlRetryFeedback_1.test create mode 100644 tests/module/OnoeRateControlSampling_1.test diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index 31af934f903..98330c58de5 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -52,20 +52,24 @@ void OnoeRateControl::handleMessage(cMessage *msg) void OnoeRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) { State& state = getState(getReceiverAddress(frame)); - computeModeIfTimerIsExpired(state); + if (!isSuccessful && !isGivenUp) + return; + // Recovery owns the per-packet counter. Commit it once, at completion, so + // unfinished retries (including other TIDs) cannot enter an evaluated sample. + ASSERT(retryCount >= 0); + state.numOfRetries += retryCount; if (isSuccessful) state.numOfSuccTransmissions++; - else if (isGivenUp) + else state.numOfGivenUpTransmissions++; - if (retryCount > 0) - state.numOfRetries++; + computeModeIfTimerIsExpired(state); } void OnoeRateControl::computeModeIfTimerIsExpired(State& state) { if (simTime() - state.timer >= interval) { - computeMode(state); state.timer = simTime(); + computeMode(state); } } @@ -75,29 +79,35 @@ void OnoeRateControl::frameReceived(Packet *frame) void OnoeRateControl::computeMode(State& state) { - int numOfFrameTransmitted = state.numOfSuccTransmissions + state.numOfGivenUpTransmissions + state.numOfRetries; - state.avgRetriesPerFrame = double(state.numOfRetries) / (state.numOfSuccTransmissions + state.numOfGivenUpTransmissions); - - if (state.numOfSuccTransmissions > 0) { - if (numOfFrameTransmitted >= 10 && state.avgRetriesPerFrame > 1) { - state.mode = decreaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.address, state.mode); - EV_DETAIL << "Decreased rate to " << *state.mode << endl; - state.credit = 0; - } - else if (state.avgRetriesPerFrame >= 0.1) - state.credit--; - else - state.credit++; - - if (state.credit >= 10) { + // Decision semantics: MadWifi ath_rate/onoe/onoe.c, ath_rate_ctl(), revision + // a7531fd223a1f454d3fd74a975b4581cde5411bb. This is an implementation reference, + // not an IEEE 802.11 normative algorithm. + bool enough = state.numOfSuccTransmissions + state.numOfGivenUpTransmissions >= 10; + auto previousMode = state.mode; + if ((state.numOfGivenUpTransmissions > 0 && state.numOfSuccTransmissions == 0) || + (enough && state.numOfSuccTransmissions < state.numOfRetries)) + { + state.mode = decreaseRateIfPossible(state.mode); + state.credit = 0; + } + // Ten percent is integer-truncated in the reference. Dividing by ten is + // equivalent to multiplying by ten then dividing by 100, without overflow. + else if (enough && state.numOfGivenUpTransmissions == 0 && state.numOfRetries < state.numOfSuccTransmissions / 10) { + if (++state.credit == 10) { state.mode = increaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.address, state.mode); - EV_DETAIL << "Increased rate to " << *state.mode << endl; state.credit = 0; } + } + else if (enough && state.credit > 0) + state.credit--; + // A small failed-only sample at the floor is retained: attempting to lower + // the rate does not count as a rate change. + if (state.mode != previousMode || enough) resetStatisticalVariables(state); + if (state.mode != previousMode) { + EV_DETAIL << "Changed rate to " << *state.mode << endl; + emitDatarateChangedSignal(state.address, state.mode); } } @@ -105,7 +115,6 @@ const IIeee80211Mode *OnoeRateControl::getRate(const MacAddress& receiverAddress { Enter_Method("getRate"); State& state = getState(receiverAddress); - computeModeIfTimerIsExpired(state); EV_INFO << "The current mode is " << state.mode << " the net bitrate is " << state.mode->getDataMode()->getNetBitrate() << std::endl; return state.mode; } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index 8245b1fd271..158143a105d 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -14,21 +14,20 @@ namespace inet { namespace ieee80211 { /** - * Implements the Onoe rate control algorithms. + * Implements Onoe's completed-sample rate adaptation rules. */ class INET_API OnoeRateControl : public RateControlBase { protected: - // Per-receiver adaptive state (formerly single-instance module members). + // Completed-sample statistics and adaptation state belong to each receiver. struct State { MacAddress address; // the receiver this state belongs to (for per-station rate attribution) const physicallayer::IIeee80211Mode *mode = nullptr; simtime_t timer = SIMTIME_ZERO; - int numOfRetries = 0; - int numOfSuccTransmissions = 0; - int numOfGivenUpTransmissions = 0; - double avgRetriesPerFrame = 0; - int credit = 0; + int64_t numOfRetries = 0; // failed attempts of completed frames, including terminal failures + int64_t numOfSuccTransmissions = 0; + int64_t numOfGivenUpTransmissions = 0; + int credit = 0; // 0 through 9 after each evaluation }; std::map stations; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index 1370298b763..898d14f468a 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -11,8 +11,15 @@ import inet.common.SimpleModule; import inet.linklayer.ieee80211.mac.contract.IRateControl; // -// Implements ONOE, a credit-based rate control algorithm originally -// developed by Atheros. +// Implements Atsushi Onoe's credit-based rate adaptation rules, following +// MadWifi ath_rate/onoe/onoe.c (revision a7531fd223a1f454d3fd74a975b4581cde5411bb). +// Evaluates completed normal-ACK frames after the interval, including the +// completion that reaches the deadline. Insufficient samples are retained; +// upward credit requires at least ten completions and no terminal failures. +// A peer's first interval starts when it is first seen. Rate queries do not +// trigger evaluation. Startup uses initialRate and retries use the stored rate; +// MadWifi's multirate retry series is not modeled. RTS/CTS and Block Ack feedback +// equivalence, and HT-or-newer adaptation fidelity, are not established. // simple OnoeRateControl extends SimpleModule like IRateControl { diff --git a/tests/module/OnoeRateControlCompletionFeedback_1.test b/tests/module/OnoeRateControlCompletionFeedback_1.test new file mode 100644 index 00000000000..f5233e750af --- /dev/null +++ b/tests/module/OnoeRateControlCompletionFeedback_1.test @@ -0,0 +1,172 @@ +%description: +Establish Onoe's terminal retry-count input using real DCF and HCF transmissions: +clean success, two failures then success, and retry exhaustion after one or three +failures. The receiver applies deterministic errors; each source sends one packet. +This checks the INET adapter input to MadWifi ath_rate_tx_complete-style sampling. + +%file: CompletionFeedback.cc +#include "inet/common/ModuleAccess.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" + +namespace inet { +namespace ieee80211 { +class CompletionFeedbackControl : public OnoeRateControl +{ + protected: + using cListener::finish; + int failures = 0; + int completions = 0; + + void finish() override + { + int scenario = getContainingNode(this)->getIndex() / 2; + const int expectedFailures[] = {0, 2, 1, 3}; + ASSERT(failures == expectedFailures[scenario]); + ASSERT(completions == 1); + std::cout << getFullPath() << ": completion feedback verified.\n"; + } + + public: + void frameTransmitted(Packet *frame, int retryCount, bool successful, bool givenUp) override + { + int scenario = getContainingNode(this)->getIndex() / 2; + if (!successful) + failures++; + ASSERT(retryCount == failures); + if (successful || givenUp) { + completions++; + ASSERT(successful == (scenario < 2)); + ASSERT(givenUp == (scenario >= 2)); + } + std::cout << getFullPath() << ": retryCount=" << retryCount + << " success=" << successful << " givenUp=" << givenUp << "\n"; + OnoeRateControl::frameTransmitted(frame, retryCount, successful, givenUp); + } +}; +Define_Module(CompletionFeedbackControl); +} // namespace ieee80211 +namespace physicallayer { +class CompletionErrorModel : public Ieee80211NistErrorModel +{ + protected: + mutable std::map attempts; + mutable std::map errors; + + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override + { + auto transmission = snir->getReception()->getTransmission(); + auto packet = transmission->getPacket(); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto header = packet->peekAt(phyHeader->getChunkLength()); + if (header->getReceiverAddress().isMulticast()) + return 0; + auto node = getContainingNode(check_and_cast(transmission->getTransmitterRadio())); + int source = node->getIndex(); + int scenario = source / 2; + auto found = errors.find(transmission->getId()); + if (found != errors.end()) + return found->second; + int attempt = ++attempts[source]; + double error = scenario == 0 ? 0 : scenario == 1 ? (attempt <= 2 ? 1 : 0) : 1; + errors[transmission->getId()] = error; + return error; + } +}; +Define_Module(CompletionErrorModel); +} // namespace physicallayer +} // namespace inet + +%file: test.ned +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple CompletionFeedbackControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::CompletionFeedbackControl); +} +module CompletionErrorModel extends Ieee80211NistErrorModel +{ + parameters: + @class(::inet::physicallayer::CompletionErrorModel); +} +network CompletionFeedbackNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source[8]: AdhocHost; + sink: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +network = CompletionFeedbackNetwork +sim-time-limit = 1s +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +*.source[*].mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.*.ipv4.arp.typename = "GlobalArp" +*.source[1].wlan[0].mac.qosStation = true +*.source[3].wlan[0].mac.qosStation = true +*.source[5].wlan[0].mac.qosStation = true +*.source[7].wlan[0].mac.qosStation = true +*.source[0].wlan[0].mac.dcf.rateControl.typename = "CompletionFeedbackControl" +*.source[1].wlan[0].mac.hcf.rateControl.typename = "CompletionFeedbackControl" +*.source[2].wlan[0].mac.dcf.rateControl.typename = "CompletionFeedbackControl" +*.source[3].wlan[0].mac.hcf.rateControl.typename = "CompletionFeedbackControl" +*.source[4].wlan[0].mac.dcf.rateControl.typename = "CompletionFeedbackControl" +*.source[5].wlan[0].mac.hcf.rateControl.typename = "CompletionFeedbackControl" +*.source[6].wlan[0].mac.dcf.rateControl.typename = "CompletionFeedbackControl" +*.source[7].wlan[0].mac.hcf.rateControl.typename = "CompletionFeedbackControl" +*.source[*].wlan[0].mac.*.rateControl.initialRate = 24Mbps +*.source[*].wlan[0].mac.*.rateSelection.dataFrameBitrate = -1bps +*.source[4..5].wlan[0].mac.**.shortRetryLimit = 1 +*.source[*].wlan[0].mac.**.shortRetryLimit = 3 +*.sink.mobility.initialX = 1m +*.sink.wlan[0].radio.receiver.errorModel.typename = "CompletionErrorModel" +*.source[*].numApps = 1 +*.source[*].app[0].typename = "UdpBasicApp" +*.source[*].app[0].destAddresses = "sink" +*.source[*].app[0].destPort = 5000 +*.source[*].app[0].messageLength = 100B +*.source[0].app[0].startTime = 1ms +*.source[1].app[0].startTime = 101ms +*.source[2].app[0].startTime = 201ms +*.source[3].app[0].startTime = 301ms +*.source[4].app[0].startTime = 401ms +*.source[5].app[0].startTime = 501ms +*.source[6].app[0].startTime = 601ms +*.source[7].app[0].startTime = 701ms +*.source[*].app[0].sendInterval = 2s + +%contains: stdout +source[0].wlan[0].mac.dcf.rateControl: completion feedback verified. +%contains: stdout +source[1].wlan[0].mac.hcf.rateControl: completion feedback verified. +%contains: stdout +source[2].wlan[0].mac.dcf.rateControl: completion feedback verified. +%contains: stdout +source[3].wlan[0].mac.hcf.rateControl: completion feedback verified. +%contains: stdout +source[4].wlan[0].mac.dcf.rateControl: completion feedback verified. +%contains: stdout +source[5].wlan[0].mac.hcf.rateControl: completion feedback verified. +%contains: stdout +source[6].wlan[0].mac.dcf.rateControl: completion feedback verified. +%contains: stdout +source[7].wlan[0].mac.hcf.rateControl: completion feedback verified. diff --git a/tests/module/OnoeRateControlFailureInterval_1.test b/tests/module/OnoeRateControlFailureInterval_1.test new file mode 100644 index 00000000000..d97de9bdda8 --- /dev/null +++ b/tests/module/OnoeRateControlFailureInterval_1.test @@ -0,0 +1,149 @@ +%description: +Exercise initialized rate-control modules through the public feedback and rate +query APIs. Fixed feedback sequences isolate adaptation from channel randomness. +Observe downward rate-change signals, including suppression at the minimum rate. +Sparse traffic earns one credit per ten completed packets, following MadWifi +ath_rate_ctl(), revision a7531fd223a1f454d3fd74a975b4581cde5411bb. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" + +#include + +namespace inet { +namespace ieee80211 { + +class RateChangeListener : public cListener +{ + public: + std::vector rates; + + void receiveSignal(cComponent *source, simsignal_t signalID, double value, cObject *details) override + { + rates.push_back(value); + } +}; + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress otherReceiver("02:00:00:00:00:02"); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + auto rate = [&]() { return control->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + ASSERT(rate() == 24e6); + ASSERT(control->getRate(otherReceiver)->getDataMode()->getNetBitrate().get() == 24e6); + auto tick = [&]() { wait(SimTime(1, SIMTIME_S)); return rate(); }; + // Idle intervals must not manufacture upward credit. + for (int i = 0; i < 10; i++) + ASSERT(tick() == 24e6); + // A retry-only interval has no completed packet to evaluate yet. + control->frameTransmitted(frame, 1, false, false); + ASSERT(tick() == 24e6); + control->frameTransmitted(frame, 2, false, true); + ASSERT(tick() == 18e6); + ASSERT(tick() == 18e6); // evaluated failures were cleared + // Repeated sufficient neutral samples must not create negative credit. + for (int period = 0; period < 10; period++) { + for (int i = 0; i < 10; i++) { + control->frameTransmitted(frame, 1, false, false); + if (i == 9) + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 1, true, false); + } + ASSERT(rate() == 18e6); + } + // One clean completion each second needs 100 packets to earn ten credits. + for (int i = 0; i < 99; i++) { + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 18e6); + } + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 0, true, false); + ASSERT(rate() == 24e6); + // A sufficient high-retry sample lowers the rate on its last completion. + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + for (int i = 1; i <= 11; i++) + control->frameTransmitted(frame, i, false, false); + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 11, true, false); + ASSERT(rate() == 18e6); + // Failed-only intervals repeatedly lower the rate, respecting the floor. + RateChangeListener listener; + control->subscribe("datarateChanged", &listener); + const double expectedRates[] = {12e6, 9e6, 6e6, 6e6}; + for (double expectedRate : expectedRates) { + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate() == expectedRate); + } + ASSERT((listener.rates == std::vector{12e6, 9e6, 6e6})); + // The retained floor failure and nine successes form a sufficient sample. + for (int i = 0; i < 8; i++) + control->frameTransmitted(frame, 0, true, false); + for (int i = 1; i <= 10; i++) + control->frameTransmitted(frame, i, false, false); + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 10, true, false); + ASSERT(tick() == 6e6); + ASSERT(tick() == 6e6); + ASSERT(control->getRate(otherReceiver)->getDataMode()->getNetBitrate().get() == 24e6); + ASSERT(listener.rates.size() == 3); + control->unsubscribe("datarateChanged", &listener); + delete frame; + std::cout << "OnoeRateControlFailureInterval verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 150s +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "OnoeRateControl" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.interval = 1s + +%contains: stdout +OnoeRateControlFailureInterval verified. diff --git a/tests/module/OnoeRateControlInterleavedFeedback_1.test b/tests/module/OnoeRateControlInterleavedFeedback_1.test new file mode 100644 index 00000000000..0bcc3a659d9 --- /dev/null +++ b/tests/module/OnoeRateControlInterleavedFeedback_1.test @@ -0,0 +1,169 @@ +%description: +Real HCF feedback from two access categories sharing one receiver. A background +frame fails once; its failure triggers a voice frame, which wins access before +the background retry. Onoe must sample the clean voice completion without the +unfinished background retry, then include that retry with its own completion. +Reference: MadWifi ath_rate_tx_complete, a7531fd223a1f454d3fd74a975b4581cde5411bb. + +%file: InterleavedFeedback.cc +#include "inet/common/ModuleAccess.h" +#include "inet/applications/udpapp/UdpBasicApp.h" +#include "inet/linklayer/common/UserPriorityTag_m.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" + +namespace inet { +class PriorityApp : public UdpBasicApp +{ + protected: + void sendPacket() override { sendPriorityPacket(1); } + + public: + void sendPriorityPacket(int priority) + { + Enter_Method("sendPriorityPacket"); + auto packet = new Packet("priority"); + packet->insertAtBack(makeShared(B(par("messageLength")))); + packet->addTag()->setUserPriority(priority); + socket.sendTo(packet, chooseDestAddr(), destPort); + numSent++; + } +}; +Define_Module(PriorityApp); + +namespace ieee80211 { +class InterleavedControl : public OnoeRateControl +{ + protected: + using cListener::finish; + int feedbackCount = 0; + + void finish() override + { + ASSERT(feedbackCount == 3); + std::cout << "HCF interleaved completion feedback verified.\n"; + } + + public: + void frameTransmitted(Packet *frame, int retryCount, bool successful, bool givenUp) override + { + auto header = frame->peekAtFront(); + ASSERT(!givenUp); + feedbackCount++; + OnoeRateControl::frameTransmitted(frame, retryCount, successful, givenUp); + const auto& state = stations.at(header->getReceiverAddress()); + ASSERT(state.credit == 0); + ASSERT(state.numOfGivenUpTransmissions == 0); + if (feedbackCount == 1) { + ASSERT(header->getTid() == 1 && !successful && retryCount == 1); + ASSERT(state.numOfSuccTransmissions == 0 && state.numOfRetries == 0); + auto app = check_and_cast(getContainingNode(this)->getSubmodule("app", 0)); + app->sendPriorityPacket(6); + } + else if (feedbackCount == 2) { + ASSERT(header->getTid() == 6 && successful && retryCount == 0); + ASSERT(state.numOfSuccTransmissions == 1 && state.numOfRetries == 0); + } + else { + ASSERT(feedbackCount == 3); + ASSERT(header->getTid() == 1 && successful && retryCount == 1); + ASSERT(state.numOfSuccTransmissions == 2 && state.numOfRetries == 1); + } + std::cout << "feedback " << feedbackCount << ": tid=" << int(header->getTid()) + << " retries=" << retryCount << " success=" << successful << "\n"; + } +}; +Define_Module(InterleavedControl); +} // namespace ieee80211 + +namespace physicallayer { +class BackgroundErrorModel : public Ieee80211NistErrorModel +{ + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override + { + auto packet = snir->getReception()->getTransmission()->getPacket(); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto header = dynamicPtrCast( + packet->peekAt(phyHeader->getChunkLength())); + return header && header->getTid() == 1 && !header->getRetry() ? 1 : 0; + } +}; +Define_Module(BackgroundErrorModel); +} // namespace physicallayer +} // namespace inet + +%file: test.ned +import inet.applications.udpapp.UdpBasicApp; +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple PriorityApp extends UdpBasicApp +{ + parameters: + @class(::inet::PriorityApp); +} +simple InterleavedControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::InterleavedControl); +} +module BackgroundErrorModel extends Ieee80211NistErrorModel +{ + parameters: + @class(::inet::physicallayer::BackgroundErrorModel); +} +network InterleavedNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source: AdhocHost; + sink: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +network = InterleavedNetwork +sim-time-limit = 20ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +*.source.mobility.initialX = 0m +*.sink.mobility.initialX = 1m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.*.ipv4.arp.typename = "GlobalArp" +*.source.wlan[0].mac.qosStation = true +*.sink.wlan[0].mac.qosStation = true +*.source.wlan[0].mac.hcf.rateControl.typename = "InterleavedControl" +*.source.wlan[0].mac.hcf.rateControl.initialRate = 24Mbps +*.source.wlan[0].mac.hcf.rateControl.interval = 1us +*.source.wlan[0].mac.hcf.rateSelection.dataFrameBitrate = -1bps +*.source.wlan[0].mac.hcf.edca.edcaf[0].aifsn = 15 +*.source.wlan[0].mac.hcf.edca.edcaf[3].aifsn = 1 +*.source.wlan[0].mac.hcf.edca.edcaf[3].cwMin = 0 +*.source.wlan[0].mac.hcf.edca.edcaf[3].cwMax = 0 +*.sink.wlan[0].radio.receiver.errorModel.typename = "BackgroundErrorModel" +*.source.numApps = 1 +*.source.app[0].typename = "PriorityApp" +*.source.app[0].destAddresses = "sink" +*.source.app[0].destPort = 5000 +*.source.app[0].messageLength = 100B +*.source.app[0].startTime = 1ms +*.source.app[0].sendInterval = 1s +*.sink.numApps = 1 +*.sink.app[0].typename = "UdpSink" +*.sink.app[0].localPort = 5000 + +%contains: stdout +HCF interleaved completion feedback verified. diff --git a/tests/module/OnoeRateControlRetryAccounting_1.test b/tests/module/OnoeRateControlRetryAccounting_1.test new file mode 100644 index 00000000000..dc24709a666 --- /dev/null +++ b/tests/module/OnoeRateControlRetryAccounting_1.test @@ -0,0 +1,133 @@ +%description: +Check Onoe's completed-sample arithmetic and credit boundaries against MadWifi +ath_rate_ctl(), revision a7531fd223a1f454d3fd74a975b4581cde5411bb, +ath_rate/onoe/onoe.c. Distinct peers isolate samples; only the final completion +reaches the deadline. Expected values are fixed reference cases, not an oracle +copied from the implementation under test. + +%file: FeedbackTest.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" + +namespace inet { +namespace ieee80211 { + +class ObservedOnoe : public OnoeRateControl +{ + public: + void check(const MacAddress& receiver, int credit, int64_t successes, int64_t errors, int64_t retries) const + { + const auto& state = stations.at(receiver); + ASSERT(state.credit == credit); + ASSERT(state.numOfSuccTransmissions == successes); + ASSERT(state.numOfGivenUpTransmissions == errors); + ASSERT(state.numOfRetries == retries); + } +}; +Define_Module(ObservedOnoe); + +class FeedbackTest : public cSimpleModule +{ + public: + FeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateControl")); + int receiverIndex = 1; + auto sample = [&](int successes, int giveUps, int retries, int initialCredit, double expectedRate, int expectedCredit) { + MacAddress receiver("02:00:00:00:00:00"); + receiver.setAddressByte(5, receiverIndex++); + auto frame = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + ASSERT(control->getRate(receiver)->getDataMode()->getNetBitrate().get() == 24e6); + for (int period = 0; period < initialCredit; period++) { + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, 0, true, false); + } + control->check(receiver, initialCredit, 0, 0, 0); + for (int i = 0; i < successes + giveUps; i++) { + bool success = i < successes; + int failures = i == 0 ? retries - giveUps + (success ? 0 : 1) : (success ? 0 : 1); + for (int retry = 1; retry <= failures - (success ? 0 : 1); retry++) + control->frameTransmitted(frame, retry, false, false); + if (i == successes + giveUps - 1) + wait(SimTime(1, SIMTIME_S)); + control->frameTransmitted(frame, failures, success, !success); + } + ASSERT(control->getRate(receiver)->getDataMode()->getNetBitrate().get() == expectedRate); + bool retained = successes + giveUps < 10 && expectedRate == 24e6; + control->check(receiver, expectedCredit, retained ? successes : 0, retained ? giveUps : 0, retained ? retries : 0); + delete frame; + }; + sample(4, 0, 4, 0, 24e6, 0); + sample(5, 0, 5, 0, 24e6, 0); + sample(4, 0, 8, 2, 24e6, 2); // attempts do not make a sufficient sample or decay credit + sample(9, 0, 10, 2, 24e6, 2); // immediately below the completion threshold + sample(1, 2, 8, 0, 24e6, 0); + sample(2, 2, 8, 0, 24e6, 0); + sample(0, 1, 1, 2, 18e6, 0); // failed-only fallback does not require ten completions + sample(8, 2, 9, 2, 18e6, 0); + sample(10, 0, 10, 2, 24e6, 1); + sample(10, 0, 11, 2, 18e6, 0); + sample(10, 0, 0, 0, 24e6, 1); + sample(10, 0, 1, 0, 24e6, 0); + sample(19, 0, 1, 2, 24e6, 1); // integer truncation + sample(20, 0, 1, 2, 24e6, 3); + sample(100, 1, 1, 2, 24e6, 1); // errors prohibit credit even with few retries + sample(10, 0, 0, 9, 36e6, 0); + std::cout << "OnoeRateControlRetryAccounting verified.\n"; + } +}; +Define_Module(FeedbackTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple FeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::FeedbackTest); +} +simple ObservedOnoe extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::ObservedOnoe); +} +network FeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: FeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = FeedbackNetwork +sim-time-limit = 80s +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.dcf.rateControl.typename = "ObservedOnoe" +*.sta.wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta.wlan[0].mac.dcf.rateControl.interval = 1s + +%contains: stdout +OnoeRateControlRetryAccounting verified. diff --git a/tests/module/OnoeRateControlRetryFeedback_1.test b/tests/module/OnoeRateControlRetryFeedback_1.test new file mode 100644 index 00000000000..a6082f41e53 --- /dev/null +++ b/tests/module/OnoeRateControlRetryFeedback_1.test @@ -0,0 +1,166 @@ +%description: +Real DCF and HCF each send ten frames that fail once then succeed. A paired +DCF/HCF run adds one extra failure on the first frame. The tenth completion +crosses the interval: ten retries holds 24 Mbps, eleven lowers it to 18 Mbps. +Reference: MadWifi ath_rate_ctl(), a7531fd223a1f454d3fd74a975b4581cde5411bb. + +%file: RetryFeedback.cc +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" +#include "inet/common/ModuleAccess.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" + +namespace inet { +namespace ieee80211 { +class RetryFeedbackControl : public OnoeRateControl +{ + protected: + using cListener::finish; + int failures = 0; + int successes = 0; + MacAddress receiver; + + void finish() override + { + bool extraFailure = getContainingNode(this)->getIndex() >= 2; + ASSERT(failures == (extraFailure ? 11 : 10)); + ASSERT(successes == 10); + ASSERT(getRate(receiver)->getDataMode()->getNetBitrate().get() == (extraFailure ? 18e6 : 24e6)); + const auto& state = stations.at(receiver); + ASSERT(state.numOfSuccTransmissions == 0); + ASSERT(state.numOfRetries == 0); + ASSERT(state.credit == 0); + std::cout << getFullPath() << ": retry accounting verified.\n"; + } + + public: + void frameTransmitted(Packet *frame, int retryCount, bool successful, bool givenUp) override + { + bool extraFailure = getContainingNode(this)->getIndex() >= 2 && successes == 0; + ASSERT(retryCount == 1 || (extraFailure && retryCount == 2)); + if (successful) + ASSERT(retryCount == (extraFailure ? 2 : 1)); + ASSERT(!givenUp); + if (successful) + successes++; + else + failures++; + receiver = getReceiverAddress(frame); + OnoeRateControl::frameTransmitted(frame, retryCount, successful, givenUp); + if (successes < 10) + ASSERT(getRate(receiver)->getDataMode()->getNetBitrate().get() == 24e6); + } +}; +Define_Module(RetryFeedbackControl); +} // namespace ieee80211 +namespace physicallayer { +class FirstAttemptErrorModel : public Ieee80211NistErrorModel +{ + protected: + mutable std::map, int> attempts; + mutable std::map firstSequences; + mutable std::map errors; + + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override + { + auto transmission = snir->getReception()->getTransmission(); + auto packet = transmission->getPacket(); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto header = packet->peekAt(phyHeader->getChunkLength()); + auto dataHeader = dynamicPtrCast(header); + if (!dataHeader || header->getReceiverAddress().isMulticast()) + return 0; + auto found = errors.find(transmission->getId()); + if (found != errors.end()) + return found->second; + int source = getContainingNode(check_and_cast(transmission->getTransmitterRadio()))->getIndex(); + int sequence = dataHeader->getSequenceNumber().get(); + auto first = firstSequences.emplace(source, sequence).first; + int failures = source >= 2 && sequence == first->second ? 2 : 1; + double error = ++attempts[{source, sequence}] <= failures ? 1 : 0; + errors[transmission->getId()] = error; + return error; + } +}; +Define_Module(FirstAttemptErrorModel); +} // namespace physicallayer +} // namespace inet + +%file: test.ned +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple RetryFeedbackControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::RetryFeedbackControl); +} +module FirstAttemptErrorModel extends Ieee80211NistErrorModel +{ + parameters: + @class(::inet::physicallayer::FirstAttemptErrorModel); +} +network RetryFeedbackNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source[4]: AdhocHost; + sink: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +network = RetryFeedbackNetwork +sim-time-limit = 7500ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +*.source[*].mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +*.*.ipv4.arp.typename = "GlobalArp" +*.source[1].wlan[0].mac.qosStation = true +*.source[3].wlan[0].mac.qosStation = true +*.source[0].wlan[0].mac.dcf.rateControl.typename = "RetryFeedbackControl" +*.source[1].wlan[0].mac.hcf.rateControl.typename = "RetryFeedbackControl" +*.source[2].wlan[0].mac.dcf.rateControl.typename = "RetryFeedbackControl" +*.source[3].wlan[0].mac.hcf.rateControl.typename = "RetryFeedbackControl" +*.source[*].wlan[0].mac.*.rateControl.initialRate = 24Mbps +*.source[*].wlan[0].mac.*.rateControl.interval = 1s +*.source[*].wlan[0].mac.*.rateSelection.dataFrameBitrate = -1bps +*.sink.mobility.initialX = 1m +*.sink.wlan[0].radio.receiver.errorModel.typename = "FirstAttemptErrorModel" +*.source[*].numApps = 1 +*.source[*].app[0].typename = "UdpBasicApp" +*.source[*].app[0].destAddresses = "sink" +*.source[*].app[0].destPort = 5000 +*.source[*].app[0].messageLength = 100B +*.source[0].app[0].startTime = 1ms +*.source[1].app[0].startTime = 2001ms +*.source[2].app[0].startTime = 4001ms +*.source[3].app[0].startTime = 6001ms +*.source[0].app[0].stopTime = 1101ms +*.source[1].app[0].stopTime = 3101ms +*.source[2].app[0].stopTime = 5101ms +*.source[3].app[0].stopTime = 7101ms +*.source[*].app[0].sendInterval = 120ms + +%contains: stdout +source[0].wlan[0].mac.dcf.rateControl: retry accounting verified. +%contains: stdout +source[1].wlan[0].mac.hcf.rateControl: retry accounting verified. +%contains: stdout +source[2].wlan[0].mac.dcf.rateControl: retry accounting verified. +%contains: stdout +source[3].wlan[0].mac.hcf.rateControl: retry accounting verified. diff --git a/tests/module/OnoeRateControlSampling_1.test b/tests/module/OnoeRateControlSampling_1.test new file mode 100644 index 00000000000..e9f60d38948 --- /dev/null +++ b/tests/module/OnoeRateControlSampling_1.test @@ -0,0 +1,217 @@ +%description: +Exercise completion deadlines, retained samples, rate limits, peer/TID isolation, +and mode-set signal reset. Expected sampling/credit transitions follow MadWifi +ath_rate_ctl and ath_rate_tx_complete, a7531fd223a1f454d3fd74a975b4581cde5411bb. +The first-seen deadline and mode-set lifecycle are INET integration conventions. + +%file: SamplingTest.cc +#include "inet/common/ModuleAccess.h" +#include "inet/common/Simsignals.h" +#include "inet/networklayer/common/NetworkInterface.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" + +namespace inet { +namespace ieee80211 { +class SamplingControl : public OnoeRateControl +{ + public: + void check(const MacAddress& receiver, int credit, int64_t successes, int64_t errors, + int64_t retries, simtime_t timer) const + { + const auto& state = stations.at(receiver); + ASSERT(state.credit == credit); + ASSERT(state.numOfSuccTransmissions == successes); + ASSERT(state.numOfGivenUpTransmissions == errors); + ASSERT(state.numOfRetries == retries); + ASSERT(state.timer == timer); + } + + void changeModeSet() + { + Enter_Method("changeModeSet"); + auto replacement = const_cast(physicallayer::Ieee80211ModeSet::getModeSet("a")); + getContainingNicModule(this)->emit(modesetChangedSignal, replacement); + ASSERT(modeSet == replacement); + ASSERT(stations.empty()); + } +}; +Define_Module(SamplingControl); + +class RateListener : public cListener +{ + public: + int changes = 0; + void receiveSignal(cComponent *source, simsignal_t signal, double value, cObject *details) override + { + changes++; + } +}; + +class SamplingTest : public cSimpleModule +{ + public: + SamplingTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto control = check_and_cast(getModuleByPath("^.sta[0].wlan[0].mac.dcf.rateControl")); + auto floor = check_and_cast(getModuleByPath("^.sta[1].wlan[0].mac.dcf.rateControl")); + auto ceiling = check_and_cast(getModuleByPath("^.sta[2].wlan[0].mac.dcf.rateControl")); + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress other("02:00:00:00:00:02"); + auto makeFrame = [&](const MacAddress& address, int tid) { + auto packet = new Packet("feedback"); + auto header = makeShared(); + header->setReceiverAddress(address); + header->setTid(tid); + packet->insertAtFront(header); + return packet; + }; + auto frame = makeFrame(receiver, 0); + auto unfinished = makeFrame(receiver, 6); + auto otherFrame = makeFrame(other, 0); + auto rate = [&](SamplingControl *target) { return target->getRate(receiver)->getDataMode()->getNetBitrate().get(); }; + const SimTime second(1, SIMTIME_S); + const SimTime epsilon(1, SIMTIME_US); + ASSERT(rate(control) == 24e6); + ASSERT(control->getRate(other)->getDataMode()->getNetBitrate().get() == 24e6); + for (int i = 0; i < 9; i++) + control->frameTransmitted(frame, 0, true, false); + for (int i = 1; i <= 5; i++) + control->frameTransmitted(unfinished, i, false, false); + wait(second - epsilon); + control->frameTransmitted(frame, 0, true, false); + control->check(receiver, 0, 10, 0, 0, SIMTIME_ZERO); + wait(epsilon); + ASSERT(rate(control) == 24e6); // query at equality cannot consume the deadline + control->frameTransmitted(unfinished, 6, false, false); + control->check(receiver, 0, 10, 0, 0, SIMTIME_ZERO); + control->frameTransmitted(frame, 0, true, false); + control->check(receiver, 1, 0, 0, 0, simTime()); + control->check(other, 0, 0, 0, 0, SIMTIME_ZERO); + wait(second + epsilon); + control->frameTransmitted(unfinished, 6, true, false); + simtime_t lastEvaluation = simTime(); + control->check(receiver, 1, 1, 0, 6, lastEvaluation); + for (int i = 0; i < 10; i++) { + wait(second); + ASSERT(rate(control) == 24e6); + control->check(receiver, 1, 1, 0, 6, lastEvaluation); + } + // The idle peer's first completion consumes only one deadline, retaining + // its small sample instead of catching up ten empty intervals. + control->frameTransmitted(frame, 1, false, true); + control->check(receiver, 1, 1, 1, 7, simTime()); + + // Three completions per interval accumulate to twelve before one credit. + for (int period = 1; period <= 4; period++) { + control->frameTransmitted(otherFrame, 0, true, false); + control->frameTransmitted(otherFrame, 0, true, false); + wait(second); + control->frameTransmitted(otherFrame, 0, true, false); + control->check(other, period == 4 ? 1 : 0, period == 4 ? 0 : period * 3, 0, 0, simTime()); + } + + auto cleanSample = [&](SamplingControl *target) { + for (int i = 0; i < 9; i++) + target->frameTransmitted(frame, 0, true, false); + wait(second); + target->frameTransmitted(frame, 0, true, false); + }; + ASSERT(rate(floor) == 6e6); + cleanSample(floor); + floor->check(receiver, 1, 0, 0, 0, simTime()); + RateListener floorListener; + floor->subscribe("datarateChanged", &floorListener); + wait(second); + floor->frameTransmitted(frame, 1, false, true); + floor->check(receiver, 0, 0, 1, 1, simTime()); + for (int i = 0; i < 8; i++) + floor->frameTransmitted(frame, 1, false, true); + wait(second); + floor->frameTransmitted(frame, 1, false, true); + floor->check(receiver, 0, 0, 0, 0, simTime()); + ASSERT(rate(floor) == 6e6); + ASSERT(floorListener.changes == 0); + floor->unsubscribe("datarateChanged", &floorListener); + + ASSERT(rate(ceiling) == 54e6); + RateListener ceilingListener; + ceiling->subscribe("datarateChanged", &ceilingListener); + for (int i = 1; i <= 10; i++) { + cleanSample(ceiling); + ceiling->check(receiver, i % 10, 0, 0, 0, simTime()); + } + ASSERT(rate(ceiling) == 54e6); + ASSERT(ceilingListener.changes == 0); + ceiling->unsubscribe("datarateChanged", &ceilingListener); + + // A real mode-set signal clears both peers, including retained samples + // and credit. New state starts its first interval at the reset-time query. + control->changeModeSet(); + ASSERT(rate(control) == 24e6); + control->check(receiver, 0, 0, 0, 0, simTime()); + ASSERT(control->getRate(other)->getDataMode()->getNetBitrate().get() == 24e6); + control->check(other, 0, 0, 0, 0, simTime()); + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate(control) == 24e6); + wait(second); + control->frameTransmitted(frame, 1, false, true); + ASSERT(rate(control) == 18e6); + control->check(receiver, 0, 0, 0, 0, simTime()); + delete frame; + delete unfinished; + delete otherFrame; + std::cout << "OnoeRateControlSampling verified.\n"; + } +}; +Define_Module(SamplingTest); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple SamplingControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::SamplingControl); +} +simple SamplingTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::SamplingTest); +} +network SamplingNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta[3]: AdhocHost; + test: SamplingTest; +} + +%inifile: omnetpp.ini +[General] +network = SamplingNetwork +sim-time-limit = 50s +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta[*].wlan[0].mac.dcf.rateControl.typename = "SamplingControl" +*.sta[0].wlan[0].mac.dcf.rateControl.initialRate = 24Mbps +*.sta[1].wlan[0].mac.dcf.rateControl.initialRate = 6Mbps +*.sta[2].wlan[0].mac.dcf.rateControl.initialRate = 54Mbps +*.sta[*].wlan[0].mac.dcf.rateControl.interval = 1s + +%contains: stdout +OnoeRateControlSampling verified. From c3578da836bb83b7230893d757375311040dfb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 5/9] ieee80211: include RTS failures in completion feedback Lost CTS responses were absent from Onoe retry samples, and RTS exhaustion did not report a completed error. Supply the protected packet's short-plus- long retry total through DCF and HCF before recovery cleanup so each terminal outcome is counted once. Retire both recovery counters at completion to avoid stale totals when identities are reused. RateControlBase adapters preserve legacy data-attempt feedback, including AARF behavior. Document the new hooks, overload visibility and migration requirements for direct IRateControl implementations. Cover mixed RTS/data failures, both exhaustion paths, recovery cleanup and protected management frames. --- doc/src/migration-guide/index.rst | 45 +++ .../ieee80211/mac/contract/IRateControl.h | 10 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 11 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 16 +- .../mac/originator/NonQosRecoveryProcedure.cc | 36 +- .../mac/originator/NonQosRecoveryProcedure.h | 2 + .../mac/originator/QosRecoveryProcedure.cc | 36 +- .../mac/originator/QosRecoveryProcedure.h | 2 + .../mac/ratecontrol/AarfRateControl.h | 1 + .../mac/ratecontrol/OnoeRateControl.cc | 10 + .../mac/ratecontrol/OnoeRateControl.h | 2 + .../mac/ratecontrol/OnoeRateControl.ned | 6 +- .../mac/ratecontrol/RateControlBase.cc | 10 + .../mac/ratecontrol/RateControlBase.h | 6 + .../Ieee80211MgmtApHcfRtsTimeout_1.test | 6 + .../Ieee80211RecoveryRetryTotals_1.test | 155 ++++++++ .../module/OnoeRateControlRtsFeedback_1.test | 348 ++++++++++++++++++ 17 files changed, 653 insertions(+), 49 deletions(-) create mode 100644 tests/module/Ieee80211RecoveryRetryTotals_1.test create mode 100644 tests/module/OnoeRateControlRtsFeedback_1.test diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 63a12835dd2..7a216c633f3 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -22,6 +22,51 @@ and simulation results, so existing studies should be revalidated. Subclasses that used the former elapsed-time helpers or the receiver-wide failure counter must adapt to the new packet-feedback state machine. +Migrating Custom IEEE 802.11 Rate Controllers +------------------------------------------- + +``IRateControl`` adds two pure virtual feedback hooks. Classes implementing this +interface directly must implement both hooks, in addition to the existing +four-argument ``frameTransmitted`` method, or they remain abstract: + +.. code-block:: c++ + + void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, + bool isSuccessful, bool isGivenUp) override; + void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, + bool isGivenUp) override; + +To preserve an existing controller's data-attempt behavior, forward the new +``frameTransmitted`` overload to its four-argument overload using ``retryCount`` +and implement the RTS hook as a no-op: + +.. code-block:: c++ + + void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, + bool isSuccessful, bool isGivenUp) override + { + frameTransmitted(frame, retryCount, isSuccessful, isGivenUp); + } + + void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, + bool isGivenUp) override {} + +``RateControlBase`` already provides these adapters, so its subclasses need no +new overrides to preserve that behavior. If a subclass overrides the four-argument +method and callers use the five-argument overload through the subclass type, add +``using RateControlBase::frameTransmitted;`` to its public section to expose the +inherited overload. + +Controllers that account for completed packets can instead consume +``totalRetryCount``: the current sum of the packet's short and long retry counters, +including RTS failures and the final failed attempt on exhaustion. It is a +cumulative count, not an increment to add on every callback. The extended data +callback marks completion when ``isSuccessful`` or ``isGivenUp`` is true; the RTS +callback marks completion only when ``isGivenUp`` is true. Its ``frame`` is the +protected data or management packet, not the RTS frame. Successful CTS reception +is not packet completion. All feedback packets are borrowed for the duration of +the call and must not be deleted or retained by the controller. + Migrating ``FieldsChunkSerializer`` Subclasses --------------------------------------------- diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h index 37d13a059d1..f773cefccbd 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h @@ -28,7 +28,17 @@ class INET_API IRateControl // Returns the rate to use for a unicast frame addressed to the given receiver. virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) = 0; + // Legacy data/management attempt feedback. retryCount is the packet's data + // recovery count; successful first data attempts report zero even after RTS failures. + // Packets in all feedback methods are borrowed for the duration of the call. virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) = 0; + // Extended feedback: totalRetryCount is the current per-packet SRC + LRC, + // including RTS failures and the final failed attempt on exhaustion. Report + // exactly once per data attempt, before recovery clears the completed packet. + virtual void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) = 0; + // A failed RTS/CTS exchange, referring to the protected data/management packet. + // Only isGivenUp marks a completed packet; successful CTS is not a completion. + virtual void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) = 0; virtual void frameReceived(Packet *frame) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 105fea0bc80..c2dfcd117f4 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -280,7 +280,10 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) auto protectedHeader = packet->peekAtFront(); recoveryProcedure->rtsFrameTransmissionFailed(protectedHeader, stationRetryCounters); EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, protectedHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, protectedHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; - if (recoveryProcedure->isRtsFrameRetryLimitReached(packet, protectedHeader)) { + bool retryLimitReached = recoveryProcedure->isRtsFrameRetryLimitReached(packet, protectedHeader); + if (dataAndMgmtRateControl) + dataAndMgmtRateControl->rtsFrameTransmissionFailed(packet, recoveryProcedure->getTotalRetryCount(protectedHeader), retryLimitReached); + if (retryLimitReached) { recoveryProcedure->retryLimitReached(packet, protectedHeader); channelAccess->getInProgressFrames()->dropFrame(packet); ackHandler->dropFrame(protectedHeader); @@ -333,7 +336,8 @@ void Dcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTra auto lastTransmittedDataOrMgmtHeader = dynamicPtrCast(lastTransmittedHeader); if (dataAndMgmtRateControl) { int retryCount = lastTransmittedHeader->getRetry() ? recoveryProcedure->getRetryCount(lastTransmittedPacket, lastTransmittedDataOrMgmtHeader) : 0; - dataAndMgmtRateControl->frameTransmitted(lastTransmittedPacket, retryCount, true, false); + int totalRetryCount = recoveryProcedure->getTotalRetryCount(lastTransmittedDataOrMgmtHeader); + dataAndMgmtRateControl->frameTransmitted(lastTransmittedPacket, retryCount, totalRetryCount, true, false); } recoveryProcedure->ackFrameReceived(lastTransmittedPacket, lastTransmittedDataOrMgmtHeader, stationRetryCounters); ackHandler->processReceivedAck(dynamicPtrCast(receivedHeader), lastTransmittedDataOrMgmtHeader); @@ -364,7 +368,8 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) bool retryLimitReached = recoveryProcedure->isRetryLimitReached(failedPacket, failedHeader); if (dataAndMgmtRateControl) { int retryCount = recoveryProcedure->getRetryCount(failedPacket, failedHeader); - dataAndMgmtRateControl->frameTransmitted(failedPacket, retryCount, false, retryLimitReached); + int totalRetryCount = recoveryProcedure->getTotalRetryCount(failedHeader); + dataAndMgmtRateControl->frameTransmitted(failedPacket, retryCount, totalRetryCount, false, retryLimitReached); } ackHandler->processFailedFrame(failedHeader); if (retryLimitReached) { diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 108145af86d..b4d6a1181b7 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -417,10 +417,14 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) if (auto dataHeader = dynamicPtrCast(protectedHeader)) { edcaf->getRecoveryProcedure()->rtsFrameTransmissionFailed(dataHeader); retryLimitReached = edcaf->getRecoveryProcedure()->isRtsFrameRetryLimitReached(packet, dataHeader); + if (dataAndMgmtRateControl) + dataAndMgmtRateControl->rtsFrameTransmissionFailed(packet, edcaf->getRecoveryProcedure()->getTotalRetryCount(dataHeader), retryLimitReached); } else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) { edca->getMgmtAndNonQoSRecoveryProcedure()->rtsFrameTransmissionFailed(mgmtHeader, edcaf->getStationRetryCounters()); retryLimitReached = edca->getMgmtAndNonQoSRecoveryProcedure()->isRtsFrameRetryLimitReached(packet, mgmtHeader); + if (dataAndMgmtRateControl) + dataAndMgmtRateControl->rtsFrameTransmissionFailed(packet, edca->getMgmtAndNonQoSRecoveryProcedure()->getTotalRetryCount(mgmtHeader), retryLimitReached); } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame @@ -531,7 +535,8 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) retryLimitReached = edcaf->getRecoveryProcedure()->isRetryLimitReached(failedPacket, dataHeader); if (dataAndMgmtRateControl) { int retryCount = edcaf->getRecoveryProcedure()->getRetryCount(failedPacket, dataHeader); - dataAndMgmtRateControl->frameTransmitted(failedPacket, retryCount, false, retryLimitReached); + int totalRetryCount = edcaf->getRecoveryProcedure()->getTotalRetryCount(dataHeader); + dataAndMgmtRateControl->frameTransmitted(failedPacket, retryCount, totalRetryCount, false, retryLimitReached); } edcaf->getAckHandler()->processFailedFrame(dataHeader); } @@ -541,7 +546,8 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) retryLimitReached = edca->getMgmtAndNonQoSRecoveryProcedure()->isRetryLimitReached(failedPacket, mgmtHeader); if (dataAndMgmtRateControl) { int retryCount = edca->getMgmtAndNonQoSRecoveryProcedure()->getRetryCount(failedPacket, mgmtHeader); - dataAndMgmtRateControl->frameTransmitted(failedPacket, retryCount, false, retryLimitReached); + int totalRetryCount = edca->getMgmtAndNonQoSRecoveryProcedure()->getTotalRetryCount(mgmtHeader); + dataAndMgmtRateControl->frameTransmitted(failedPacket, retryCount, totalRetryCount, false, retryLimitReached); } edcaf->getAckHandler()->processFailedFrame(mgmtHeader); } @@ -618,14 +624,16 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetRecoveryProcedure()->getRetryCount(lastTransmittedPacket, dataHeader); else retryCount = 0; - dataAndMgmtRateControl->frameTransmitted(lastTransmittedPacket, retryCount, true, false); + int totalRetryCount = edcaf->getRecoveryProcedure()->getTotalRetryCount(dataHeader); + dataAndMgmtRateControl->frameTransmitted(lastTransmittedPacket, retryCount, totalRetryCount, true, false); } edcaf->getRecoveryProcedure()->ackFrameReceived(lastTransmittedPacket, dataHeader); } else if (auto mgmtHeader = dynamicPtrCast(lastTransmittedHeader)) { if (dataAndMgmtRateControl) { int retryCount = edca->getMgmtAndNonQoSRecoveryProcedure()->getRetryCount(lastTransmittedPacket, mgmtHeader); - dataAndMgmtRateControl->frameTransmitted(lastTransmittedPacket, retryCount, true, false); + int totalRetryCount = edca->getMgmtAndNonQoSRecoveryProcedure()->getTotalRetryCount(mgmtHeader); + dataAndMgmtRateControl->frameTransmitted(lastTransmittedPacket, retryCount, totalRetryCount, true, false); } edca->getMgmtAndNonQoSRecoveryProcedure()->ackFrameReceived(lastTransmittedPacket, mgmtHeader, edcaf->getStationRetryCounters()); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 44d9c05de37..1f5fae0c651 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -106,18 +106,13 @@ void NonQosRecoveryProcedure::ctsFrameReceived(StationRetryCounters *stationCoun void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& ackedHeader, StationRetryCounters *stationCounters) { auto id = SequenceControlField(ackedHeader->getSequenceNumber().get(), ackedHeader->getFragmentNumber()); - if (packet->getByteLength() >= rtsThreshold) { + if (packet->getByteLength() >= rtsThreshold) stationCounters->resetStationLongRetryCount(); - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { + else stationCounters->resetStationShortRetryCount(); - auto it = shortRetryCounter.find(id); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + // Completion retires both the RTS and data history for this packet identity. + shortRetryCounter.erase(id); + longRetryCounter.erase(id); // // The CW shall be reset to aCWmin after every successful attempt to transmit a frame containing @@ -134,16 +129,8 @@ void NonQosRecoveryProcedure::retryLimitReached(Packet *packet, const PtrgetSequenceNumber().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); - } + shortRetryCounter.erase(id); + longRetryCounter.erase(id); emit(retryLimitReachedSignal, packet); } @@ -204,6 +191,15 @@ int NonQosRecoveryProcedure::getShortRetryCount(Packet *packet, const Ptr& header) const +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + auto shortCounter = shortRetryCounter.find(id); + auto longCounter = longRetryCounter.find(id); + return (shortCounter == shortRetryCounter.end() ? 0 : shortCounter->second) + + (longCounter == longRetryCounter.end() ? 0 : longCounter->second); +} + int NonQosRecoveryProcedure::getLongRetryCount(Packet *packet, const Ptr& dataOrMgmtHeader) { return getRc(packet, dataOrMgmtHeader, longRetryCounter); diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h index 829680c9858..08f9c09f1f3 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h @@ -58,6 +58,8 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr virtual void rtsFrameTransmissionFailed(const Ptr& protectedHeader, StationRetryCounters *stationCounters); virtual void dataOrMgmtFrameTransmissionFailed(Packet *packet, const Ptr& failedHeader, StationRetryCounters *stationCounters); virtual int getRetryCount(Packet *packet, const Ptr& header); + // Read the completed-packet rate-control input, including RTS and data failures. + virtual int getTotalRetryCount(const Ptr& header) const; virtual int getShortRetryCount(Packet *packet, const Ptr& dataOrMgmtHeader); virtual int getLongRetryCount(Packet *packet, const Ptr& dataOrMgmtHeader); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc index 948bb6d2577..b27a9913ceb 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc @@ -115,18 +115,13 @@ void QosRecoveryProcedure::blockAckFrameReceived() void QosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& ackedHeader) { auto id = std::make_pair(ackedHeader->getTid(), SequenceControlField(ackedHeader->getSequenceNumber().get(), ackedHeader->getFragmentNumber())); - if (packet->getByteLength() >= rtsThreshold) { + if (packet->getByteLength() >= rtsThreshold) resetStationLrc(); - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { - auto it = shortRetryCounter.find(id); + else resetStationSrc(); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + // Completion retires both histories while preserving other TIDs and fragments. + shortRetryCounter.erase(id); + longRetryCounter.erase(id); // // The CW shall be reset to aCWmin after every successful attempt to transmit a frame containing // all or part of an MSDU or MMPDU @@ -142,16 +137,8 @@ void QosRecoveryProcedure::retryLimitReached(Packet *packet, const PtrgetTid(), 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); - } + shortRetryCounter.erase(id); + longRetryCounter.erase(id); emit(retryLimitReachedSignal, packet); } @@ -216,6 +203,15 @@ void QosRecoveryProcedure::incrementContentionWindow() emit(contentionWindowChangedSignal, cwCalculator->getCw()); } +int QosRecoveryProcedure::getTotalRetryCount(const Ptr& header) const +{ + auto id = std::make_pair(header->getTid(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + auto shortCounter = shortRetryCounter.find(id); + auto longCounter = longRetryCounter.find(id); + return (shortCounter == shortRetryCounter.end() ? 0 : shortCounter->second) + + (longCounter == longRetryCounter.end() ? 0 : longCounter->second); +} + void QosRecoveryProcedure::resetContentionWindow() { auto oldCw = cwCalculator->getCw(); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h index fa3f56c9d55..8c45c2a34e6 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h @@ -71,6 +71,8 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce virtual bool isRetryLimitReached(Packet *packet, const Ptr& failedHeader); virtual int getRetryCount(Packet *packet, const Ptr& header); + // Missing short or long counters contribute zero, including a clean first attempt. + virtual int getTotalRetryCount(const Ptr& header) const; virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); virtual void retryLimitReached(Packet *packet, const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h index c74b273255c..35e9e6528fa 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h @@ -48,6 +48,7 @@ class INET_API AarfRateControl : public RateControlBase virtual void resetRateControl() override { stations.clear(); } public: + using RateControlBase::frameTransmitted; virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; virtual void frameReceived(Packet *frame) override; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index 98330c58de5..992eab4ed7d 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -65,6 +65,16 @@ void OnoeRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuc computeModeIfTimerIsExpired(state); } +void OnoeRateControl::frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) +{ + frameTransmitted(frame, totalRetryCount, isSuccessful, isGivenUp); +} + +void OnoeRateControl::rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) +{ + frameTransmitted(frame, totalRetryCount, false, isGivenUp); +} + void OnoeRateControl::computeModeIfTimerIsExpired(State& state) { if (simTime() - state.timer >= interval) { diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index 158143a105d..f00b879a87b 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -49,6 +49,8 @@ class INET_API OnoeRateControl : public RateControlBase public: virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; + virtual void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) override; + virtual void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) override; virtual void frameReceived(Packet *frame) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index 898d14f468a..ca4d3bd5cf8 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -18,8 +18,10 @@ import inet.linklayer.ieee80211.mac.contract.IRateControl; // upward credit requires at least ten completions and no terminal failures. // A peer's first interval starts when it is first seen. Rate queries do not // trigger evaluation. Startup uses initialRate and retries use the stored rate; -// MadWifi's multirate retry series is not modeled. RTS/CTS and Block Ack feedback -// equivalence, and HT-or-newer adaptation fidelity, are not established. +// Failed RTS/CTS exchanges contribute to the completed packet's retry total; +// RTS exhaustion counts as one terminal error. MadWifi's multirate retry series +// is not modeled. Block Ack feedback equivalence and HT-or-newer adaptation +// fidelity are not established. // simple OnoeRateControl extends SimpleModule like IRateControl { diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index 5893d053644..007b7d62284 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -22,6 +22,16 @@ void RateControlBase::initialize(int stage) ModeSetListener::initialize(stage); } +void RateControlBase::frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) +{ + frameTransmitted(frame, retryCount, isSuccessful, isGivenUp); +} + +void RateControlBase::rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) +{ + // RTS failures do not describe an attempt at the controller's selected data rate. +} + const IIeee80211Mode *RateControlBase::increaseRateIfPossible(const IIeee80211Mode *currentMode) { const IIeee80211Mode *newMode = modeSet->getFasterMode(currentMode); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 937387b7914..2c3e6508d71 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -19,6 +19,12 @@ class INET_API RateControlBase : public ModeSetListener, public IRateControl { public: static simsignal_t datarateChangedSignal; + using IRateControl::frameTransmitted; + + // Preserve legacy data-attempt semantics for controllers such as AARF. + // Controllers that use whole-packet outcomes override these extended hooks. + virtual void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) override; + virtual void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) override; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } diff --git a/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test b/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test index 81a602e75f5..e24af15f5cc 100644 --- a/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test +++ b/tests/module/Ieee80211MgmtApHcfRtsTimeout_1.test @@ -10,6 +10,7 @@ transaction, release its reserved AID, and emit no association notification. #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/contract/IRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" namespace inet { @@ -179,6 +180,8 @@ class Ieee80211MgmtApHcfRtsTimeoutTest : public cSimpleModule, public cListener ASSERT(mgmt->terminalCallbacks == 1); ASSERT(mgmt->associationResponseCallbacks == 1); ASSERT(mgmt->lastStatus == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + auto rateControl = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf.rateControl")); + ASSERT(rateControl->getRate(expectedAddress)->getDataMode()->getNetBitrate().get() == 18e6); ASSERT(associationNotifications == 0); ASSERT(!mgmt->hasPendingAssociation(expectedAddress)); ASSERT(!mgmt->hasCommittedAssociationId(expectedAddress)); @@ -268,6 +271,9 @@ record-scalar-results = false **.wlan[*].radio.receiver.snirThreshold = 4dB *.ap.wlan[0].mac.qosStation = true *.ap.wlan[0].mac.hcf.rtsPolicy.rtsThreshold = 0B +*.ap.wlan[0].mac.hcf.rateControl.typename = "OnoeRateControl" +*.ap.wlan[0].mac.hcf.rateControl.initialRate = 24Mbps +*.ap.wlan[0].mac.hcf.rateControl.interval = 1us *.ap.wlan[0].mac.hcf.edca.mgmtAndNonQoSRecoveryProcedure.shortRetryLimit = 2 *.sta.wlan[0].mac.actions = "BB" *.sta.wlan[0].mgmt.typename = "Ieee80211MgmtStaSimplified" diff --git a/tests/module/Ieee80211RecoveryRetryTotals_1.test b/tests/module/Ieee80211RecoveryRetryTotals_1.test new file mode 100644 index 00000000000..1b6f2df37c7 --- /dev/null +++ b/tests/module/Ieee80211RecoveryRetryTotals_1.test @@ -0,0 +1,155 @@ +%description: +Check completed-packet retry totals and retirement in both recovery procedures. +RTS and protected-data counters contribute once; CTS does not retire the packet. +ACK and exhaustion retire both counters, including when the same sequence identity +is reused. Other fragments and QoS TIDs retain their own histories. Station retry +counter policy is unchanged; these checks concern per-packet accounting lifetime. + +%file: RetryTotals.cc +#include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" + +namespace inet { +namespace ieee80211 { +class RetryTotalsTest : public cSimpleModule +{ + public: + RetryTotalsTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto nonQos = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.recoveryProcedure")); + auto qos = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.hcf.edca.edcaf[1].recoveryProcedure")); + StationRetryCounters stationCounters; + auto makeFrame = [](int bytes, int tid, int fragment) { + auto packet = new Packet("retryTotals"); + auto header = makeShared(); + header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); + header->setSequenceNumber(SequenceNumberCyclic(77)); + header->setTid(tid); + header->setFragmentNumber(fragment); + packet->insertAtFront(header); + packet->insertAtBack(makeShared(B(bytes))); + return packet; + }; + auto packet = makeFrame(2000, 0, 0); + auto header = packet->peekAtFront(); + auto neighbor = makeFrame(2000, 0, 1); + auto neighborHeader = neighbor->peekAtFront(); + auto otherTid = makeFrame(2000, 6, 0); + auto otherTidHeader = otherTid->peekAtFront(); + ASSERT(nonQos->getTotalRetryCount(header) == 0); + ASSERT(qos->getTotalRetryCount(header) == 0); + // Retain independent identities while retiring/reusing the primary one. + nonQos->rtsFrameTransmissionFailed(neighborHeader, &stationCounters); + qos->rtsFrameTransmissionFailed(neighborHeader); + qos->rtsFrameTransmissionFailed(otherTidHeader); + for (int terminal = 0; terminal < 3; terminal++) { + ASSERT(nonQos->getTotalRetryCount(header) == 0); + ASSERT(qos->getTotalRetryCount(header) == 0); + nonQos->rtsFrameTransmissionFailed(header, &stationCounters); + qos->rtsFrameTransmissionFailed(header); + nonQos->ctsFrameReceived(&stationCounters); + qos->ctsFrameReceived(); + ASSERT(nonQos->getTotalRetryCount(header) == 1); + ASSERT(qos->getTotalRetryCount(header) == 1); + nonQos->dataOrMgmtFrameTransmissionFailed(packet, header, &stationCounters); + qos->dataFrameTransmissionFailed(packet, header); + ASSERT(nonQos->getRetryCount(packet, header) == 1); + ASSERT(qos->getRetryCount(packet, header) == 1); + ASSERT(nonQos->getTotalRetryCount(header) == 2); + ASSERT(qos->getTotalRetryCount(header) == 2); + if (terminal == 0) { + nonQos->ackFrameReceived(packet, header, &stationCounters); + qos->ackFrameReceived(packet, header); + } + else { + if (terminal == 1) { + nonQos->dataOrMgmtFrameTransmissionFailed(packet, header, &stationCounters); + qos->dataFrameTransmissionFailed(packet, header); + ASSERT(nonQos->isRetryLimitReached(packet, header)); + ASSERT(qos->isRetryLimitReached(packet, header)); + } + else { + for (int i = 0; i < 2; i++) { + nonQos->rtsFrameTransmissionFailed(header, &stationCounters); + qos->rtsFrameTransmissionFailed(header); + } + ASSERT(nonQos->isRtsFrameRetryLimitReached(packet, header)); + ASSERT(qos->isRtsFrameRetryLimitReached(packet, header)); + } + nonQos->retryLimitReached(packet, header); + qos->retryLimitReached(packet, header); + } + ASSERT(nonQos->getTotalRetryCount(header) == 0); + ASSERT(qos->getTotalRetryCount(header) == 0); + ASSERT(nonQos->getTotalRetryCount(neighborHeader) == 1); + ASSERT(qos->getTotalRetryCount(neighborHeader) == 1); + ASSERT(qos->getTotalRetryCount(otherTidHeader) == 1); + } + // Below the RTS threshold, data failures occupy SRC alone, not both maps. + auto small = makeFrame(100, 0, 0); + auto smallHeader = small->peekAtFront(); + nonQos->dataOrMgmtFrameTransmissionFailed(small, smallHeader, &stationCounters); + qos->dataFrameTransmissionFailed(small, smallHeader); + ASSERT(nonQos->getRetryCount(small, smallHeader) == 1); + ASSERT(qos->getRetryCount(small, smallHeader) == 1); + ASSERT(nonQos->getTotalRetryCount(smallHeader) == 1); + ASSERT(qos->getTotalRetryCount(smallHeader) == 1); + nonQos->ackFrameReceived(small, smallHeader, &stationCounters); + qos->ackFrameReceived(small, smallHeader); + ASSERT(nonQos->getTotalRetryCount(smallHeader) == 0); + ASSERT(qos->getTotalRetryCount(smallHeader) == 0); + nonQos->retryLimitReached(neighbor, neighborHeader); + qos->retryLimitReached(neighbor, neighborHeader); + qos->retryLimitReached(otherTid, otherTidHeader); + delete packet; + delete neighbor; + delete otherTid; + delete small; + std::cout << "Recovery retry totals and retirement verified.\n"; + } +}; +Define_Module(RetryTotalsTest); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple RetryTotalsTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::RetryTotalsTest); +} +network RetryTotalsNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: AdhocHost; + test: RetryTotalsTest; +} + +%inifile: omnetpp.ini +[General] +network = RetryTotalsNetwork +sim-time-limit = 1s +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta.wlan[0].mac.qosStation = true +*.sta.wlan[0].mac.*.rtsPolicy.rtsThreshold = 1000B +*.sta.wlan[0].mac.**.shortRetryLimit = 3 +*.sta.wlan[0].mac.**.longRetryLimit = 2 + +%contains: stdout +Recovery retry totals and retirement verified. diff --git a/tests/module/OnoeRateControlRtsFeedback_1.test b/tests/module/OnoeRateControlRtsFeedback_1.test new file mode 100644 index 00000000000..6c82d0b0ff1 --- /dev/null +++ b/tests/module/OnoeRateControlRtsFeedback_1.test @@ -0,0 +1,348 @@ +%description: +Real DCF/HCF normal-ACK exchanges cover clean RTS/CTS, two lost CTS responses, +RTS exhaustion, and mixed RTS/data failures ending in success, data exhaustion, +or RTS exhaustion. Repeat each scenario with AARF to verify its legacy data-only +feedback. Onoe receives the total once on completion (MadWifi ath_rate_tx_complete, +ath_rate/onoe/onoe.c revision a7531fd223a1f454d3fd74a975b4581cde5411bb). + +%file: RtsFeedback.cc +#include +#include "inet/common/ModuleAccess.h" +#include "inet/networklayer/common/NetworkInterface.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/errormodel/Ieee80211NistErrorModel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" + +namespace inet { +namespace ieee80211 { +template +class RtsFeedbackControl : public Base +{ + protected: + using cListener::finish; + int rtsFailures = 0; + int dataFailures = 0; + int completions = 0; + MacAddress receiver; + + int scenario() const { return (getContainingNode(this)->getIndex() % 12) / 2; } + + void checkCompletion(bool successful, int total) + { + completions++; + int test = scenario(); + const int totals[] = {0, 2, 3, 2, 3, 4}; + ASSERT(total == totals[test]); + ASSERT(successful == (test == 0 || test == 1 || test == 3)); + if constexpr (std::is_same_v) { + const auto& state = this->stations.at(receiver); + ASSERT(state.credit == 0); + ASSERT(state.numOfSuccTransmissions == (successful ? 1 : 0)); + ASSERT(state.numOfGivenUpTransmissions == 0); + ASSERT(state.numOfRetries == (successful ? total : 0)); + ASSERT(this->getRate(receiver)->getDataMode()->getNetBitrate().get() == (successful ? 24e6 : 18e6)); + } + } + + void finish() override + { + const int expectedRtsFailures[] = {0, 2, 3, 1, 1, 3}; + const int expectedDataFailures[] = {0, 0, 0, 1, 2, 1}; + ASSERT(rtsFailures == expectedRtsFailures[scenario()]); + ASSERT(dataFailures == expectedDataFailures[scenario()]); + ASSERT(completions == 1); + if constexpr (std::is_same_v) + ASSERT(this->getRate(receiver)->getDataMode()->getNetBitrate().get() == (scenario() == 4 ? 18e6 : 24e6)); + std::cout << this->getFullPath() << ": RTS feedback verified.\n"; + } + + public: + using Base::frameTransmitted; + void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool successful, bool givenUp) override + { + receiver = this->getReceiverAddress(frame); + if (!successful) + dataFailures++; + ASSERT(retryCount == dataFailures); + ASSERT(totalRetryCount == rtsFailures + dataFailures); + if (scenario() == 1) { + ASSERT(successful && retryCount == 0 && totalRetryCount == 2); + ASSERT(!frame->peekAtFront()->getRetry()); + } + Base::frameTransmitted(frame, retryCount, totalRetryCount, successful, givenUp); + if (successful || givenUp) + checkCompletion(successful, totalRetryCount); + std::cout << this->getFullPath() << ": data retry=" << retryCount + << " total=" << totalRetryCount << " success=" << successful + << " givenUp=" << givenUp << "\n"; + } + + void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool givenUp) override + { + receiver = this->getReceiverAddress(frame); + ASSERT(dynamicPtrCast(frame->peekAtFront())); + rtsFailures++; + ASSERT(totalRetryCount == rtsFailures + dataFailures); + Base::rtsFrameTransmissionFailed(frame, totalRetryCount, givenUp); + if (givenUp) + checkCompletion(false, totalRetryCount); + else if constexpr (std::is_same_v) { + const auto& state = this->stations.at(receiver); + ASSERT(state.numOfSuccTransmissions == 0 && state.numOfGivenUpTransmissions == 0 && state.numOfRetries == 0); + } + std::cout << this->getFullPath() << ": RTS total=" << totalRetryCount << " givenUp=" << givenUp << "\n"; + } +}; +class RtsOnoeControl : public RtsFeedbackControl {}; +class RtsAarfControl : public RtsFeedbackControl {}; +Define_Module(RtsOnoeControl); +Define_Module(RtsAarfControl); +} // namespace ieee80211 + +namespace physicallayer { +class RtsFeedbackErrorModel : public Ieee80211NistErrorModel +{ + protected: + mutable std::map, int> attempts; + mutable std::map errors; + + public: + double computePacketErrorRate(const ISnir *snir, IRadioSignal::SignalPart part) const override + { + auto reception = snir->getReception(); + auto transmission = reception->getTransmission(); + auto packet = transmission->getPacket(); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(packet); + auto header = packet->peekAt(phyHeader->getChunkLength()); + auto receiverModule = check_and_cast(reception->getReceiverRadio()); + if (header->getReceiverAddress() != getContainingNicModule(receiverModule)->getMacAddress()) + return 0; + auto found = errors.find(transmission->getId()); + if (found != errors.end()) + return found->second; + auto receiverNode = getContainingNode(receiverModule); + int type = header->getType(); + int source = -1; + bool error = false; + if (type == ieee80211::ST_CTS) { + source = receiverNode->getIndex(); + int attempt = ++attempts[{source, type}]; + error = (source % 12) / 2 == 1 && attempt <= 2; + } + else if (strcmp(receiverNode->getName(), "sink") == 0) { + source = getContainingNode(check_and_cast(transmission->getTransmitterRadio()))->getIndex(); + int test = (source % 12) / 2; + int attempt = ++attempts[{source, type}]; + if (type == ieee80211::ST_RTS) + error = test == 2 || ((test == 3 || test == 4) && attempt == 1) || (test == 5 && attempt > 1); + else if (dynamicPtrCast(header)) + error = (test == 3 && attempt == 1) || test == 4 || (test == 5 && attempt == 1); + } + errors[transmission->getId()] = error ? 1 : 0; + return error ? 1 : 0; + } +}; +Define_Module(RtsFeedbackErrorModel); +} // namespace physicallayer +} // namespace inet + +%file: test.ned +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.linklayer.ieee80211.mac.ratecontrol.AarfRateControl; +import inet.physicallayer.wireless.ieee80211.packetlevel.errormodel.Ieee80211NistErrorModel; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple RtsOnoeControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::RtsOnoeControl); +} +simple RtsAarfControl extends AarfRateControl +{ + parameters: + @class(::inet::ieee80211::RtsAarfControl); +} +module RtsFeedbackErrorModel extends Ieee80211NistErrorModel +{ + parameters: + @class(::inet::physicallayer::RtsFeedbackErrorModel); +} +network RtsFeedbackNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source[24]: AdhocHost; + sink: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +network = RtsFeedbackNetwork +sim-time-limit = 2500ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +*.source[*].mobility.initialX = 0m +*.sink.mobility.initialX = 1m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.*.ipv4.arp.typename = "GlobalArp" +*.sink.wlan[0].mac.qosStation = true +*.source[*].wlan[0].mac.*.rateControl.initialRate = 24Mbps +*.source[0..11].wlan[0].mac.*.rateControl.interval = 1us +*.source[12..23].wlan[0].mac.*.rateControl.minTimerThreshold = 100000 +*.source[*].wlan[0].mac.*.rateSelection.dataFrameBitrate = -1bps +*.source[*].wlan[0].mac.*.rtsPolicy.rtsThreshold = 0B +*.source[*].wlan[0].mac.**.shortRetryLimit = 3 +*.source[*].wlan[0].mac.**.longRetryLimit = 2 +*.*.wlan[0].radio.receiver.errorModel.typename = "RtsFeedbackErrorModel" +*.source[*].numApps = 1 +*.source[*].app[0].typename = "UdpBasicApp" +*.source[*].app[0].destAddresses = "sink" +*.source[*].app[0].destPort = 5000 +*.source[*].app[0].messageLength = 100B +*.source[*].app[0].sendInterval = 10s +*.sink.numApps = 1 +*.sink.app[0].typename = "UdpSink" +*.sink.app[0].localPort = 5000 +*.source[0].wlan[0].mac.dcf.rateControl.typename = "RtsOnoeControl" +*.source[0].app[0].startTime = 1ms +*.source[1].wlan[0].mac.qosStation = true +*.source[1].wlan[0].mac.hcf.rateControl.typename = "RtsOnoeControl" +*.source[1].app[0].startTime = 101ms +*.source[2].wlan[0].mac.dcf.rateControl.typename = "RtsOnoeControl" +*.source[2].app[0].startTime = 201ms +*.source[3].wlan[0].mac.qosStation = true +*.source[3].wlan[0].mac.hcf.rateControl.typename = "RtsOnoeControl" +*.source[3].app[0].startTime = 301ms +*.source[4].wlan[0].mac.dcf.rateControl.typename = "RtsOnoeControl" +*.source[4].app[0].startTime = 401ms +*.source[5].wlan[0].mac.qosStation = true +*.source[5].wlan[0].mac.hcf.rateControl.typename = "RtsOnoeControl" +*.source[5].app[0].startTime = 501ms +*.source[6].wlan[0].mac.dcf.rateControl.typename = "RtsOnoeControl" +*.source[6].app[0].startTime = 601ms +*.source[7].wlan[0].mac.qosStation = true +*.source[7].wlan[0].mac.hcf.rateControl.typename = "RtsOnoeControl" +*.source[7].app[0].startTime = 701ms +*.source[8].wlan[0].mac.dcf.rateControl.typename = "RtsOnoeControl" +*.source[8].app[0].startTime = 801ms +*.source[9].wlan[0].mac.qosStation = true +*.source[9].wlan[0].mac.hcf.rateControl.typename = "RtsOnoeControl" +*.source[9].app[0].startTime = 901ms +*.source[10].wlan[0].mac.dcf.rateControl.typename = "RtsOnoeControl" +*.source[10].app[0].startTime = 1001ms +*.source[11].wlan[0].mac.qosStation = true +*.source[11].wlan[0].mac.hcf.rateControl.typename = "RtsOnoeControl" +*.source[11].app[0].startTime = 1101ms +*.source[12].wlan[0].mac.dcf.rateControl.typename = "RtsAarfControl" +*.source[12].app[0].startTime = 1201ms +*.source[13].wlan[0].mac.qosStation = true +*.source[13].wlan[0].mac.hcf.rateControl.typename = "RtsAarfControl" +*.source[13].app[0].startTime = 1301ms +*.source[14].wlan[0].mac.dcf.rateControl.typename = "RtsAarfControl" +*.source[14].app[0].startTime = 1401ms +*.source[15].wlan[0].mac.qosStation = true +*.source[15].wlan[0].mac.hcf.rateControl.typename = "RtsAarfControl" +*.source[15].app[0].startTime = 1501ms +*.source[16].wlan[0].mac.dcf.rateControl.typename = "RtsAarfControl" +*.source[16].app[0].startTime = 1601ms +*.source[17].wlan[0].mac.qosStation = true +*.source[17].wlan[0].mac.hcf.rateControl.typename = "RtsAarfControl" +*.source[17].app[0].startTime = 1701ms +*.source[18].wlan[0].mac.dcf.rateControl.typename = "RtsAarfControl" +*.source[18].app[0].startTime = 1801ms +*.source[19].wlan[0].mac.qosStation = true +*.source[19].wlan[0].mac.hcf.rateControl.typename = "RtsAarfControl" +*.source[19].app[0].startTime = 1901ms +*.source[20].wlan[0].mac.dcf.rateControl.typename = "RtsAarfControl" +*.source[20].app[0].startTime = 2001ms +*.source[21].wlan[0].mac.qosStation = true +*.source[21].wlan[0].mac.hcf.rateControl.typename = "RtsAarfControl" +*.source[21].app[0].startTime = 2101ms +*.source[22].wlan[0].mac.dcf.rateControl.typename = "RtsAarfControl" +*.source[22].app[0].startTime = 2201ms +*.source[23].wlan[0].mac.qosStation = true +*.source[23].wlan[0].mac.hcf.rateControl.typename = "RtsAarfControl" +*.source[23].app[0].startTime = 2301ms + +%contains: stdout +source[0].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[1].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[2].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[3].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[4].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[5].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[6].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[7].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[8].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[9].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[10].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[11].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[12].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[13].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[14].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[15].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[16].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[17].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[18].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[19].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[20].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[21].wlan[0].mac.hcf.rateControl: RTS feedback verified. + +%contains: stdout +source[22].wlan[0].mac.dcf.rateControl: RTS feedback verified. + +%contains: stdout +source[23].wlan[0].mac.hcf.rateControl: RTS feedback verified. From 53b45d63879ba8d30fa516934f2fa0e1dda1c5c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 6/9] qosrecoveryprocedure: isolate retry histories by receiver QoS sequence numbers are allocated per receiver and TID, so different peers can have identical sequence and fragment numbers. Include the receiver in the shared retry key to prevent interleaved feedback and completion cleanup from reading or erasing another peer's counters. Cover clean reads, short and long counters, retry limits and terminal cleanup with interleaved peers in the recovery module test. --- .../mac/originator/QosRecoveryProcedure.cc | 23 ++++++---- .../mac/originator/QosRecoveryProcedure.h | 11 ++--- .../Ieee80211RecoveryRetryTotals_1.test | 43 ++++++++++++++++++- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc index b27a9913ceb..055a0fb81f3 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.cc @@ -15,7 +15,7 @@ namespace ieee80211 { Define_Module(QosRecoveryProcedure); -inline std::ostream& operator<<(std::ostream& os, const std::pair& p) { return os << p.first << "#" << p.second; } +inline std::ostream& operator<<(std::ostream& os, const std::pair>& p) { return os << p.first << "#" << p.second.first << "#" << p.second.second; } // // Contention window management @@ -65,9 +65,14 @@ void QosRecoveryProcedure::incrementStationLrc() incrementContentionWindow(); } -void QosRecoveryProcedure::incrementCounter(const Ptr& header, std::map, int>& retryCounter) +QosRecoveryProcedure::RetryKey QosRecoveryProcedure::getRetryKey(const Ptr& header) { - auto id = std::make_pair((Tid)header->getTid(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + return std::make_pair(header->getReceiverAddress(), std::make_pair((Tid)header->getTid(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()))); +} + +void QosRecoveryProcedure::incrementCounter(const Ptr& header, std::map& retryCounter) +{ + auto id = getRetryKey(header); if (containsKey(retryCounter, id)) retryCounter[id]++; else @@ -114,12 +119,12 @@ void QosRecoveryProcedure::blockAckFrameReceived() // void QosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& ackedHeader) { - auto id = std::make_pair(ackedHeader->getTid(), SequenceControlField(ackedHeader->getSequenceNumber().get(), ackedHeader->getFragmentNumber())); + auto id = getRetryKey(ackedHeader); if (packet->getByteLength() >= rtsThreshold) resetStationLrc(); else resetStationSrc(); - // Completion retires both histories while preserving other TIDs and fragments. + // Completion retires both histories while preserving other receivers, TIDs and fragments. shortRetryCounter.erase(id); longRetryCounter.erase(id); // @@ -136,7 +141,7 @@ void QosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header) { EV_WARN << "Retry limit reached for " << *packet << ".\n"; - auto id = std::make_pair(header->getTid(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + auto id = getRetryKey(header); shortRetryCounter.erase(id); longRetryCounter.erase(id); emit(retryLimitReachedSignal, packet); @@ -205,7 +210,7 @@ void QosRecoveryProcedure::incrementContentionWindow() int QosRecoveryProcedure::getTotalRetryCount(const Ptr& header) const { - auto id = std::make_pair(header->getTid(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + auto id = getRetryKey(header); auto shortCounter = shortRetryCounter.find(id); auto longCounter = longRetryCounter.find(id); return (shortCounter == shortRetryCounter.end() ? 0 : shortCounter->second) + @@ -226,9 +231,9 @@ bool QosRecoveryProcedure::isRtsFrameRetryLimitReached(Packet *packet, const Ptr return getRc(packet, protectedHeader, shortRetryCounter) >= shortRetryLimit; } -int QosRecoveryProcedure::getRc(Packet *packet, const Ptr& header, std::map, int>& retryCounter) +int QosRecoveryProcedure::getRc(Packet *packet, const Ptr& header, std::map& retryCounter) { - auto id = std::make_pair(header->getTid(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + auto id = getRetryKey(header); auto it = retryCounter.find(id); if (it != retryCounter.end()) return it->second; diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h index 8c45c2a34e6..b949783c8e2 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h @@ -32,9 +32,9 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce protected: ICwCalculator *cwCalculator = nullptr; - // TODO why do we need Tid, is this class per AC or not? we should decide - std::map, int> shortRetryCounter; // SRC - std::map, int> longRetryCounter; // LRC + using RetryKey = std::pair>; + std::map shortRetryCounter; // SRC + std::map longRetryCounter; // LRC // TODO these counters should be per AC, it's not done here but as separate recovery procedure modules int stationLongRetryCounter = 0; // QLRC @@ -48,7 +48,8 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - void incrementCounter(const Ptr& header, std::map, int>& retryCounter); + static RetryKey getRetryKey(const Ptr& header); + void incrementCounter(const Ptr& header, std::map& retryCounter); void incrementStationSrc(); void incrementStationLrc(); void resetStationSrc() { stationShortRetryCounter = 0; } @@ -56,7 +57,7 @@ class INET_API QosRecoveryProcedure : public SimpleModule, public IRecoveryProce void incrementContentionWindow(); void resetContentionWindow(); int doubleCw(int cw); - int getRc(Packet *packet, const Ptr& header, std::map, int>& retryCounter); + int getRc(Packet *packet, const Ptr& header, std::map& retryCounter); bool isMulticastFrame(Packet *packet, const Ptr& header); public: diff --git a/tests/module/Ieee80211RecoveryRetryTotals_1.test b/tests/module/Ieee80211RecoveryRetryTotals_1.test index 1b6f2df37c7..deba5165818 100644 --- a/tests/module/Ieee80211RecoveryRetryTotals_1.test +++ b/tests/module/Ieee80211RecoveryRetryTotals_1.test @@ -2,7 +2,7 @@ Check completed-packet retry totals and retirement in both recovery procedures. RTS and protected-data counters contribute once; CTS does not retire the packet. ACK and exhaustion retire both counters, including when the same sequence identity -is reused. Other fragments and QoS TIDs retain their own histories. Station retry +is reused. Other receivers, fragments and QoS TIDs retain their own histories. Station retry counter policy is unchanged; these checks concern per-packet accounting lifetime. %file: RetryTotals.cc @@ -104,6 +104,47 @@ class RetryTotalsTest : public cSimpleModule nonQos->retryLimitReached(neighbor, neighborHeader); qos->retryLimitReached(neighbor, neighborHeader); qos->retryLimitReached(otherTid, otherTidHeader); + // Interleave peers with identical TID, sequence and fragment numbers. + auto otherPeer = makeFrame(2000, 0, 0); + auto writableHeader = otherPeer->removeAtFront(); + writableHeader->setReceiverAddress(MacAddress("02:00:00:00:00:02")); + otherPeer->insertAtFront(writableHeader); + auto otherPeerHeader = otherPeer->peekAtFront(); + for (int terminal = 0; terminal < 2; terminal++) { + qos->rtsFrameTransmissionFailed(header); + qos->ctsFrameReceived(); + ASSERT(qos->getTotalRetryCount(otherPeerHeader) == 0); + // A clean completion must neither inherit nor erase the first peer's RTS failure. + qos->ackFrameReceived(otherPeer, otherPeerHeader); + ASSERT(qos->getTotalRetryCount(header) == 1); + qos->dataFrameTransmissionFailed(packet, header); + qos->rtsFrameTransmissionFailed(otherPeerHeader); + qos->rtsFrameTransmissionFailed(otherPeerHeader); + qos->dataFrameTransmissionFailed(otherPeer, otherPeerHeader); + qos->dataFrameTransmissionFailed(otherPeer, otherPeerHeader); + ASSERT(qos->getTotalRetryCount(header) == 2); + ASSERT(qos->getTotalRetryCount(otherPeerHeader) == 4); + ASSERT(qos->getRetryCount(packet, header) == 1); + ASSERT(qos->getRetryCount(otherPeer, otherPeerHeader) == 2); + ASSERT(!qos->isRetryLimitReached(packet, header)); + ASSERT(qos->isRetryLimitReached(otherPeer, otherPeerHeader)); + ASSERT(!qos->isRtsFrameRetryLimitReached(packet, header)); + ASSERT(!qos->isRtsFrameRetryLimitReached(otherPeer, otherPeerHeader)); + if (terminal == 0) + qos->ackFrameReceived(packet, header); + else { + qos->rtsFrameTransmissionFailed(header); + qos->rtsFrameTransmissionFailed(header); + ASSERT(qos->isRtsFrameRetryLimitReached(packet, header)); + ASSERT(!qos->isRtsFrameRetryLimitReached(otherPeer, otherPeerHeader)); + qos->retryLimitReached(packet, header); + } + ASSERT(qos->getTotalRetryCount(header) == 0); + ASSERT(qos->getTotalRetryCount(otherPeerHeader) == 4); + qos->retryLimitReached(otherPeer, otherPeerHeader); + ASSERT(qos->getTotalRetryCount(otherPeerHeader) == 0); + } + delete otherPeer; delete packet; delete neighbor; delete otherTid; From 2dd70f07628885370bf467d2c1e56e21bb33648f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 7/9] onoeratecontrol: apply the legacy startup rate policy Use the MadWifi startup ceiling for automatic local OFDM/ERP selection, so the built-in 11a/11g sets start at 36 Mbps. HR-DSSS starts at its fastest rate. Explicit initialRate values retain precedence, and HT/VHT retain the fastest-mandatory default. The contract supplies local modes without negotiated legacy peer rates; document that limit while preserving shared RateControlBase behavior. Cover defaults, overrides, invalid rates, peer initialization, signals and mode-set resets. --- .../mac/ratecontrol/OnoeRateControl.cc | 26 +++ .../mac/ratecontrol/OnoeRateControl.h | 1 + .../mac/ratecontrol/OnoeRateControl.ned | 8 +- tests/module/OnoeRateControlStartup_1.test | 152 ++++++++++++++++++ 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 tests/module/OnoeRateControlStartup_1.test diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index 992eab4ed7d..a33f26230b8 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -23,6 +23,32 @@ void OnoeRateControl::initialize(int stage) } } +const IIeee80211Mode *OnoeRateControl::getInitialMode() +{ + if (double(par("initialRate")) != -1) + return RateControlBase::getInitialMode(); + + // Apply the reference startup ceiling to local legacy modes. Negotiated + // legacy peer rates are not available through the rate-control contract. + switch (modeSet->getPhyType()) { + case Ieee80211ModeSet::PhyType::HR_DSSS: + return modeSet->getFastestMode(); + case Ieee80211ModeSet::PhyType::OFDM: + case Ieee80211ModeSet::PhyType::ERP: { + auto mode = modeSet->getFastestMode(); + while (mode->getDataMode()->getNetBitrate() > Mbps(36)) { + auto slowerMode = modeSet->getSlowerMode(mode); + if (slowerMode == nullptr) + break; + mode = slowerMode; + } + return mode; + } + default: + return RateControlBase::getInitialMode(); + } +} + OnoeRateControl::State& OnoeRateControl::getState(const MacAddress& receiverAddress) { auto it = stations.find(receiverAddress); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index f00b879a87b..e6d841de0dc 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -40,6 +40,7 @@ class INET_API OnoeRateControl : public RateControlBase virtual void handleMessage(cMessage *msg) override; virtual State& getState(const MacAddress& receiverAddress); + virtual const physicallayer::IIeee80211Mode *getInitialMode() override; virtual void resetRateControl() override { stations.clear(); } virtual void computeMode(State& state); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index ca4d3bd5cf8..cc2da64b41b 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -17,7 +17,11 @@ import inet.linklayer.ieee80211.mac.contract.IRateControl; // completion that reaches the deadline. Insufficient samples are retained; // upward credit requires at least ten completions and no terminal failures. // A peer's first interval starts when it is first seen. Rate queries do not -// trigger evaluation. Startup uses initialRate and retries use the stored rate; +// trigger evaluation. An explicit initialRate overrides automatic startup. +// Automatic startup uses the fastest local 11b mode, or the fastest local +// OFDM/ERP mode at or below 36 Mbps (the slowest mode if none qualifies). +// These are local modes, not negotiated peer rates. HT/VHT retain the fastest +// mandatory startup mode. Retries use the stored rate. // Failed RTS/CTS exchanges contribute to the completed packet's retry total; // RTS exhaustion counts as one terminal error. MadWifi's multirate retry series // is not modeled. Block Ack feedback equivalence and HT-or-newer adaptation @@ -27,7 +31,7 @@ simple OnoeRateControl extends SimpleModule like IRateControl { parameters: @class(OnoeRateControl); - double initialRate @unit(bps) = default(-1bps); // -1 means the fastest mandatory rate + double initialRate @unit(bps) = default(-1bps); // -1 selects automatic startup as described above double interval @unit(s) = default(1s); displayStringTextFormat = default("{numStations} stations"); @display("i=block/cogwheel"); diff --git a/tests/module/OnoeRateControlStartup_1.test b/tests/module/OnoeRateControlStartup_1.test new file mode 100644 index 00000000000..9017b21c76e --- /dev/null +++ b/tests/module/OnoeRateControlStartup_1.test @@ -0,0 +1,152 @@ +%description: +Onoe automatic startup follows the MadWifi ath_rate_ctl_start ceiling +(a7531fd223a1f454d3fd74a975b4581cde5411bb), applied to INET local legacy modes. +Check the public API, explicit overrides/errors, peer initialization signals, +and mode-set reset. HT/VHT keep the existing fastest-mandatory default. +This does not establish negotiated legacy peer-rate or on-air equivalence. + +%file: StartupTest.cc +#include "inet/common/ModuleAccess.h" +#include "inet/common/Simsignals.h" +#include "inet/networklayer/common/NetworkInterface.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" + +namespace inet { +namespace ieee80211 { +class StartupControl : public OnoeRateControl +{ + public: + void changeModeSet(const char *name) + { + Enter_Method("changeModeSet"); + auto replacement = const_cast(physicallayer::Ieee80211ModeSet::getModeSet(name)); + getContainingNicModule(this)->emit(modesetChangedSignal, replacement); + ASSERT(modeSet == replacement); + ASSERT(stations.empty()); + } +}; +Define_Module(StartupControl); + +class StartupListener : public cListener +{ + public: + int count = 0; + double lastRate = 0; + void receiveSignal(cComponent *, simsignal_t, double value, cObject *) override + { + count++; + lastRate = value; + } +}; + +class StartupTest : public cSimpleModule +{ + public: + StartupTest() : cSimpleModule(65536) {} + protected: + void activity() override + { + const MacAddress receiver("02:00:00:00:00:01"); + const MacAddress other("02:00:00:00:00:02"); + auto control = check_and_cast(getModuleByPath("^.sta[0].wlan[0].mac.dcf.rateControl")); + auto explicitControl = check_and_cast(getModuleByPath("^.sta[1].wlan[0].mac.dcf.rateControl")); + auto invalidControl = check_and_cast(getModuleByPath("^.sta[2].wlan[0].mac.dcf.rateControl")); + StartupListener listener; + control->subscribe("datarateChanged", &listener); + auto checkRate = [&](StartupControl *target, const MacAddress& address, double expected) { + ASSERT(target->getRate(address)->getDataMode()->getNetBitrate().get() == expected); + }; + // The actual initialized NED default must dispatch to Onoe's policy. + checkRate(control, receiver, 36e6); + checkRate(control, receiver, 36e6); + ASSERT(listener.count == 1 && listener.lastRate == 36e6); + checkRate(control, other, 36e6); + ASSERT(listener.count == 2); + + struct Case { const char *name; double rate; }; + const Case cases[] = {{"b", 11e6}, {"a", 36e6}, {"g(mixed)", 36e6}, + {"g(erp)", 36e6}, {"p", 27e6}}; + for (const auto& testCase : cases) { + control->changeModeSet(testCase.name); + int before = listener.count; + checkRate(control, receiver, testCase.rate); + checkRate(control, other, testCase.rate); + ASSERT(listener.count == before + 2 && listener.lastRate == testCase.rate); + } + for (const char *name : {"n(mixed-2.4Ghz)", "ac"}) { + control->changeModeSet(name); + auto expected = physicallayer::Ieee80211ModeSet::getModeSet(name)->getFastestMandatoryMode(); + ASSERT(control->getRate(receiver) == expected); + } + control->changeModeSet("a"); + // Feedback may initialize a peer before the first rate query. + auto frame = new Packet("first-feedback"); + auto header = makeShared(); + header->setReceiverAddress(receiver); + frame->insertAtFront(header); + control->frameTransmitted(frame, 0, true, false); + checkRate(control, receiver, 36e6); + delete frame; + + checkRate(explicitControl, receiver, 54e6); + explicitControl->changeModeSet("a"); + checkRate(explicitControl, other, 54e6); + bool rejected = false; + try { + invalidControl->getRate(receiver); + } + catch (const cRuntimeError& error) { + rejected = std::string(error.what()).find("Unknown bitrate") != std::string::npos; + } + ASSERT(rejected); + control->unsubscribe("datarateChanged", &listener); + std::cout << "Onoe startup policy verified.\n"; + } +}; +Define_Module(StartupTest); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple StartupControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::StartupControl); +} +simple StartupTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::StartupTest); +} +network StartupNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta[3]: AdhocHost; + test: StartupTest; +} + +%inifile: omnetpp.ini +[General] +network = StartupNetwork +sim-time-limit = 1ms +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(erp)" +*.sta[*].wlan[0].mac.dcf.rateControl.typename = "StartupControl" +*.sta[1].wlan[0].mac.dcf.rateControl.initialRate = 54Mbps +*.sta[2].wlan[0].mac.dcf.rateControl.initialRate = 7Mbps + +%contains: stdout +Onoe startup policy verified. From b0da8c1f4cd8b714b5d93fb591409efebaa9b92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 8/9] ieee80211: report internal-collision exhaustion to rate control HCF discarded packets at the internal-collision retry limit without reporting a completed sample to Onoe. Notify rate control before recovery clears the short and long counters so Onoe records the give-up and its full retry total. Use a distinct terminal-drop hook with a no-op RateControlBase default to preserve legacy AARF attempt feedback. Document internal-collision accounting as an INET modeling choice and cover QoS data and management drops with pure and mixed recovery histories. --- .../ieee80211/mac/contract/IRateControl.h | 8 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 8 + .../mac/ratecontrol/OnoeRateControl.cc | 5 + .../mac/ratecontrol/OnoeRateControl.h | 3 +- .../mac/ratecontrol/OnoeRateControl.ned | 5 +- .../mac/ratecontrol/RateControlBase.cc | 5 + .../mac/ratecontrol/RateControlBase.h | 1 + .../OnoeRateControlInternalCollision_1.test | 234 ++++++++++++++++++ 8 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 tests/module/OnoeRateControlInternalCollision_1.test diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h index f773cefccbd..4d0bd6e56dc 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h @@ -33,12 +33,16 @@ class INET_API IRateControl // Packets in all feedback methods are borrowed for the duration of the call. virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) = 0; // Extended feedback: totalRetryCount is the current per-packet SRC + LRC, - // including RTS failures and the final failed attempt on exhaustion. Report - // exactly once per data attempt, before recovery clears the completed packet. + // including RTS failures, internal collisions, and the final failure on exhaustion. + // Report exactly once per data attempt, before recovery clears the completed packet. virtual void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) = 0; // A failed RTS/CTS exchange, referring to the protected data/management packet. // Only isGivenUp marks a completed packet; successful CTS is not a completion. virtual void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) = 0; + // Terminal retry-limit drop caused by an EDCA internal collision, not an on-air + // attempt. Report once before cleanup; totalRetryCount is per-packet SRC + LRC, + // including internal collisions and any preceding RTS/data failures. + virtual void frameDroppedDueToInternalCollision(Packet *frame, int totalRetryCount) = 0; virtual void frameReceived(Packet *frame) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b4d6a1181b7..ecb2902aff7 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -272,6 +272,14 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) throw cRuntimeError("Unknown frame"); if (retryLimitReached) { EV_DETAIL << "The frame has reached its retry limit. Dropping it" << std::endl; + if (dataAndMgmtRateControl) { + int totalRetryCount; + if (auto dataHeader = dynamicPtrCast(internallyCollidedHeader)) + totalRetryCount = dataRecoveryProcedure->getTotalRetryCount(dataHeader); + else + totalRetryCount = edca->getMgmtAndNonQoSRecoveryProcedure()->getTotalRetryCount(internallyCollidedHeader); + dataAndMgmtRateControl->frameDroppedDueToInternalCollision(internallyCollidedFrame, totalRetryCount); + } if (auto dataHeader = dynamicPtrCast(internallyCollidedHeader)) dataRecoveryProcedure->retryLimitReached(internallyCollidedFrame, dataHeader); else if (auto mgmtHeader = dynamicPtrCast(internallyCollidedHeader)) diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index a33f26230b8..38e8b8c6fd1 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -101,6 +101,11 @@ void OnoeRateControl::rtsFrameTransmissionFailed(Packet *frame, int totalRetryCo frameTransmitted(frame, totalRetryCount, false, isGivenUp); } +void OnoeRateControl::frameDroppedDueToInternalCollision(Packet *frame, int totalRetryCount) +{ + frameTransmitted(frame, totalRetryCount, false, true); +} + void OnoeRateControl::computeModeIfTimerIsExpired(State& state) { if (simTime() - state.timer >= interval) { diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index e6d841de0dc..b5993d6d8d3 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -24,7 +24,7 @@ class INET_API OnoeRateControl : public RateControlBase MacAddress address; // the receiver this state belongs to (for per-station rate attribution) const physicallayer::IIeee80211Mode *mode = nullptr; simtime_t timer = SIMTIME_ZERO; - int64_t numOfRetries = 0; // failed attempts of completed frames, including terminal failures + int64_t numOfRetries = 0; // recovery counts of completed frames, including internal collisions int64_t numOfSuccTransmissions = 0; int64_t numOfGivenUpTransmissions = 0; int credit = 0; // 0 through 9 after each evaluation @@ -52,6 +52,7 @@ class INET_API OnoeRateControl : public RateControlBase virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; virtual void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) override; virtual void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) override; + virtual void frameDroppedDueToInternalCollision(Packet *frame, int totalRetryCount) override; virtual void frameReceived(Packet *frame) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index cc2da64b41b..ac46060d5b6 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -23,7 +23,10 @@ import inet.linklayer.ieee80211.mac.contract.IRateControl; // These are local modes, not negotiated peer rates. HT/VHT retain the fastest // mandatory startup mode. Retries use the stored rate. // Failed RTS/CTS exchanges contribute to the completed packet's retry total; -// RTS exhaustion counts as one terminal error. MadWifi's multirate retry series +// RTS exhaustion counts as one terminal error. As an INET modeling choice, +// internal-collision exhaustion also counts as one terminal error, with the +// packet's full recovery count (including internal collisions). +// MadWifi's multirate retry series // is not modeled. Block Ack feedback equivalence and HT-or-newer adaptation // fidelity are not established. // diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index 007b7d62284..0012254da8a 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -32,6 +32,11 @@ void RateControlBase::rtsFrameTransmissionFailed(Packet *frame, int totalRetryCo // RTS failures do not describe an attempt at the controller's selected data rate. } +void RateControlBase::frameDroppedDueToInternalCollision(Packet *frame, int totalRetryCount) +{ + // An internal collision does not attempt transmission at the selected data rate. +} + const IIeee80211Mode *RateControlBase::increaseRateIfPossible(const IIeee80211Mode *currentMode) { const IIeee80211Mode *newMode = modeSet->getFasterMode(currentMode); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 2c3e6508d71..95872032658 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -25,6 +25,7 @@ class INET_API RateControlBase : public ModeSetListener, public IRateControl // Controllers that use whole-packet outcomes override these extended hooks. virtual void frameTransmitted(Packet *frame, int retryCount, int totalRetryCount, bool isSuccessful, bool isGivenUp) override; virtual void rtsFrameTransmissionFailed(Packet *frame, int totalRetryCount, bool isGivenUp) override; + virtual void frameDroppedDueToInternalCollision(Packet *frame, int totalRetryCount) override; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } diff --git a/tests/module/OnoeRateControlInternalCollision_1.test b/tests/module/OnoeRateControlInternalCollision_1.test new file mode 100644 index 00000000000..5b2582ba705 --- /dev/null +++ b/tests/module/OnoeRateControlInternalCollision_1.test @@ -0,0 +1,234 @@ +%description: +Enter the production HCF internal-collision handler with QoS data and management +frames. Cover short retry exhaustion and long retry exhaustion after seeded RTS +and data failures. Verify terminal feedback before cleanup, exactly one Onoe +completion with the full recovery total, and no legacy AARF attempt feedback. +Collisions are injected: this does not claim natural AC_VO collision reachability. + +%file: InternalCollisionFeedback.cc +#include +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h" +#include "inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h" + +namespace inet { +namespace ieee80211 { + +template +class CollisionFeedbackControl : public Base +{ + public: + int completions = 0; + int legacyAttempts = 0; + int expectedTotal = 0; + Packet *expectedFrame = nullptr; + using Base::frameTransmitted; + + void frameTransmitted(Packet *frame, int retryCount, bool successful, bool givenUp) override + { + legacyAttempts++; + Base::frameTransmitted(frame, retryCount, successful, givenUp); + } + + void frameDroppedDueToInternalCollision(Packet *frame, int totalRetryCount) override + { + ASSERT(frame == expectedFrame); + ASSERT(totalRetryCount == expectedTotal); + completions++; + Base::frameDroppedDueToInternalCollision(frame, totalRetryCount); + } + + void verify(const MacAddress& receiver, int total) + { + auto& state = this->stations.at(receiver); + ASSERT(state.mode->getDataMode()->getNetBitrate().get() == 24e6); + if constexpr (std::is_same_v) { + ASSERT(state.numOfGivenUpTransmissions == 1); + ASSERT(state.numOfSuccTransmissions == 0); + ASSERT(state.numOfRetries == total); + ASSERT(state.credit == 0); + // Evaluate the observed failed-only sample without depending on a timer. + this->computeMode(state); + ASSERT(state.mode->getDataMode()->getNetBitrate().get() == 18e6); + ASSERT(state.numOfRetries == 0 && state.numOfGivenUpTransmissions == 0); + } + else { + ASSERT(legacyAttempts == 0); + ASSERT(state.timer == 0 && !state.probing); + ASSERT(state.numberOfConsSuccTransmissions == 0); + } + } +}; + +class CollisionOnoeControl : public CollisionFeedbackControl {}; +class CollisionAarfControl : public CollisionFeedbackControl {}; +Define_Module(CollisionOnoeControl); +Define_Module(CollisionAarfControl); + +class CollisionFeedbackHcf : public Hcf +{ + public: + template + void exercise(Control *control, bool management, bool mixed, const MacAddress& receiver) + { + Enter_Method("exercise"); + auto edcaf = edca->getEdcaf(management ? AC_VO : AC_BE); + auto frames = edcaf->getInProgressFrames(); + auto packet = new Packet("collision-feedback"); + Ptr header; + if (management) { + auto mgmtHeader = makeShared(); + mgmtHeader->setType(ST_ASSOCIATIONRESPONSE); + header = mgmtHeader; + } + else { + auto dataHeader = makeShared(); + dataHeader->setType(ST_DATA_WITH_QOS); + dataHeader->setTid(0); + header = dataHeader; + } + header->setReceiverAddress(receiver); + packet->insertAtFront(header); + if (mixed) + packet->insertAtBack(makeShared(B(1000))); + packet->insertAtBack(makeShared()); + edcaf->getPendingQueue()->enqueuePacket(packet); + // The data service may replace the pending packet and assign its sequence. + packet = frames->getFrameToTransmit(); + auto currentHeader = packet->peekAtFront(); + auto dataHeader = dynamicPtrCast(currentHeader); + auto qosRecovery = edcaf->getRecoveryProcedure(); + auto mgmtRecovery = edca->getMgmtAndNonQoSRecoveryProcedure(); + auto total = [&]() { + return dataHeader ? qosRecovery->getTotalRetryCount(dataHeader) : mgmtRecovery->getTotalRetryCount(currentHeader); + }; + ASSERT(total() == 0); + control->getRate(receiver); + int previousCompletions = control->completions; + control->expectedFrame = packet; + control->expectedTotal = mixed ? 3 : 2; + if (mixed) { + // Seed both recovery histories; the final long-count increment must + // preserve the earlier short-count RTS failure in the callback total. + if (dataHeader) { + qosRecovery->rtsFrameTransmissionFailed(dataHeader); + qosRecovery->dataFrameTransmissionFailed(packet, dataHeader); + } + else { + mgmtRecovery->rtsFrameTransmissionFailed(currentHeader, edcaf->getStationRetryCounters()); + mgmtRecovery->dataOrMgmtFrameTransmissionFailed(packet, currentHeader, edcaf->getStationRetryCounters()); + } + ASSERT(total() == 2); + } + else { + handleInternalCollision({edcaf}); + ASSERT(total() == 1); + ASSERT(control->completions == previousCompletions); + } + handleInternalCollision({edcaf}); + ASSERT(control->completions == previousCompletions + 1); + ASSERT(total() == 0); + ASSERT(!frames->hasInProgressFrames()); + control->verify(receiver, mixed ? 3 : 2); + control->expectedFrame = nullptr; + frames->clearDroppedFrames(); + } +}; +Define_Module(CollisionFeedbackHcf); + +class CollisionFeedbackTest : public cSimpleModule +{ + public: + CollisionFeedbackTest() : cSimpleModule(65536) {} + + protected: + void activity() override + { + auto onoeHcf = check_and_cast(getModuleByPath("^.ap[0].wlan[0].mac.hcf")); + auto aarfHcf = check_and_cast(getModuleByPath("^.ap[1].wlan[0].mac.hcf")); + auto onoe = check_and_cast(onoeHcf->getSubmodule("rateControl")); + auto aarf = check_and_cast(aarfHcf->getSubmodule("rateControl")); + MacAddress untouched("02:00:00:00:00:20"); + onoe->getRate(untouched); + for (int scenario = 0; scenario < 4; scenario++) { + MacAddress receiver(uint64_t(0x020000000010ULL + scenario)); + onoeHcf->exercise(onoe, scenario & 1, scenario & 2, receiver); + aarfHcf->exercise(aarf, scenario & 1, scenario & 2, receiver); + } + ASSERT(onoe->completions == 4 && aarf->completions == 4); + ASSERT(onoe->getRate(untouched)->getDataMode()->getNetBitrate().get() == 24e6); + std::cout << "HCF internal-collision completion totals and AARF compatibility verified: 8 scenarios.\n"; + endSimulation(); + } +}; +Define_Module(CollisionFeedbackTest); +} // namespace ieee80211 +} // namespace inet + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.linklayer.ieee80211.mac.ratecontrol.OnoeRateControl; +import inet.linklayer.ieee80211.mac.ratecontrol.AarfRateControl; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple CollisionOnoeControl extends OnoeRateControl +{ + parameters: + @class(::inet::ieee80211::CollisionOnoeControl); +} +simple CollisionAarfControl extends AarfRateControl +{ + parameters: + @class(::inet::ieee80211::CollisionAarfControl); +} +module CollisionFeedbackHcf extends Hcf +{ + parameters: + @class(::inet::ieee80211::CollisionFeedbackHcf); +} +simple CollisionFeedbackTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::CollisionFeedbackTest); +} +network CollisionFeedbackNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap[2]: AccessPoint; + test: CollisionFeedbackTest; +} + +%inifile: omnetpp.ini +[General] +network = CollisionFeedbackNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +sim-time-limit = 1ms +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.ap[*].wlan[0].opMode = "g(erp)" +*.ap[*].wlan[0].mgmt.beaconInterval = 10s +*.ap[*].wlan[0].mac.qosStation = true +*.ap[*].wlan[0].mac.hcf.typename = "CollisionFeedbackHcf" +*.ap[0].wlan[0].mac.hcf.rateControl.typename = "CollisionOnoeControl" +*.ap[1].wlan[0].mac.hcf.rateControl.typename = "CollisionAarfControl" +*.ap[*].wlan[0].mac.hcf.rateControl.initialRate = 24Mbps +*.ap[0].wlan[0].mac.hcf.rateControl.interval = 1s +*.ap[*].wlan[0].mac.hcf.rtsPolicy.rtsThreshold = 512B +*.ap[*].wlan[0].mac.**.shortRetryLimit = 2 +*.ap[*].wlan[0].mac.**.longRetryLimit = 2 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +HCF internal-collision completion totals and AARF compatibility verified: 8 scenarios. From 9dcd62d999a0b9e03655fbe33ece71f5e36e0b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 22:01:11 +0200 Subject: [PATCH 9/9] doc: define the Onoe multirate retry contract Record the Onoe alignment plan and its multirate retry design so attempt ownership, retry-limit precedence and PHY-rate verification requirements are reviewable before implementation. --- plan/pending/onoe-multirate-retry.md | 186 +++++++++++ plan/pending/onoe-reference-alignment.md | 390 +++++++++++++++++++++++ 2 files changed, 576 insertions(+) create mode 100644 plan/pending/onoe-multirate-retry.md create mode 100644 plan/pending/onoe-reference-alignment.md diff --git a/plan/pending/onoe-multirate-retry.md b/plan/pending/onoe-multirate-retry.md new file mode 100644 index 00000000000..9fdd7dd7137 --- /dev/null +++ b/plan/pending/onoe-multirate-retry.md @@ -0,0 +1,186 @@ +# Onoe multirate retry contract + +Status: design step completed, 2026-09-10. Production implementation pending. +Inspected source baseline: `73d5342260`. + +Follow-up 3 of [the alignment plan](onoe-reference-alignment.md). + +## Scope and reference boundary + +Add an optional legacy normal-ACK retry series without changing recovery retry +limits or Onoe's completed-sample arithmetic. Start with individually addressed +data and management frames whose rate is selected by Onoe. Fixed-rate overrides, +group frames, control responses, Block Ack, aggregation and HT/VHT remain on +their existing selection paths. Introduce `multirateRetry = false` on Onoe so +existing simulations retain their behavior until explicitly enabled. + +The pinned [MadWifi reference](https://github.com/proski/madwifi/blob/a7531fd223a1f454d3fd74a975b4581cde5411bb/ath_rate/onoe/onoe.c) +is revision `a7531fd223a1f454d3fd74a975b4581cde5411bb`. The parent assessment +records a 4/2/2/2 series, a lowest-hardware-rate final stage and disabled +redundant stages. This turn could not retrieve that source: both web URLs +failed and curl failed DNS resolution. These details are inherited evidence, +not a new reference verification. Before implementing the builder, inspect +`ath_rate_findrate`, `ath_rate_setupxtxdesc`, and `ath_rate_update`, and record +the exact low-rate predicates and total-tries interpretation in test cases. +Do not substitute four successive slower rates or redistribute disabled tries. + +INET has no negotiated legacy peer-rate list in the current controller contract. +Its local slowest legal mode is a model mapping, not automatically MadWifi's +lowest hardware rate. Resolve that mapping explicitly for each supported legacy +mode set; do not claim hardware-status or negotiated-rate equivalence. + +## Findings that determine the contract + +- `RateSelection::computeMode()` and `QosRateSelection::computeMode()` receive a + packet, but their adaptive branch calls only `IRateControl::getRate(receiver)`. + No current argument identifies the attempt or captures a packet's base rate. +- `Dcf::transmitFrame()` and `Hcf::transmitFrame()` select and tag the mode before + calculating Duration/ID and handing the packet to Tx. These are the actual + transmission paths that must consume the selected attempt mode. +- `OriginatorProtectionMechanism` and `SingleProtectionMechanism` also query + `computeMode()` for pending packets, including before RTS transmission. + Queries must be repeatable and must never consume attempts. The mode reserved + in RTS duration calculations must match the protected data transmission. +- Recovery SRC/LRC include RTS failures and internal collisions. Neither their + sum nor the length-selected legacy retry count is a data-attempt ordinal. + Reusing them would move the series without a data transmission. +- `InProgressFrames` owns the original MPDUs across exchanges, including + fragments, and retires them through `dropFrame()`/`dropFrames()` and destruction. + `FrameSequenceHandler` contexts end between exchanges; they cannot alone own + state that must survive another contention and retry. +- QoS recovery keys include receiver, TID and sequence/fragment. Non-QoS recovery + currently keys by sequence/fragment. A new series must not inherit that narrower + key or use one mutable series per receiver. + +## Proposed typed interface and ownership + +Use protocol-local value types in `mac/common/`, with no packet wire fields or +changes to the sealed packet subsystem: + +- `RateRetryStage`: borrowed mode pointer and positive data-transmission count. +- `RateRetrySeries`: bounded ordered stages, with an explicit disabled state; + an empty series means ordinary dynamic selection, never packet exhaustion. +- `RateRetryContext`: one captured series, mode-set generation, number of + completed data transmissions, and an optional prepared-attempt mode. It does + not copy SRC/LRC, own packets, or update Onoe peer statistics. + +`IRateControl` declares a pure virtual series request; `RateControlBase` returns +disabled, and Onoe constructs a series from its current peer mode when enabled. +Existing `getRate()` and feedback methods keep their meanings. AARF must keep +querying its live rate per attempt; freezing a one-stage AARF series would change +its feedback-driven behavior. Direct interface implementers need the new method; +document the migration. + +`InProgressFrames` owns one context for each original resident MPDU. Prefer a +sidecar keyed by that owned packet, erased before retirement; packet identity is +scoped to residence, not a pointer retained after deletion. Diagnostic identity +includes receiver, frame class, TID where applicable, sequence and fragment. +Transmission duplicates resolve to their original MPDU through an explicit +coordination argument, never through an assumed matching pointer or MAC sequence +number alone. Other components receive const context views or copied values. + +Rate selection owns override precedence, applicability and peer compatibility. +Extend both selection contracts to prepare/query the attempt mode with an +explicit context; keep existing ordinary queries available for unsupported +traffic. Coordination supplies the correct in-progress owner and context to +selection and protection. Protection must use the prepared pending-frame mode, +rather than making a second independent series decision. No concrete Onoe +downcast, module-path lookup from protection to recovery, or writable controller +history map is needed. + +## Attempt and terminal semantics + +1. Prepare the first attempt before its earliest duration reservation (including + RTS or preceding-fragment lookahead). Capture the series once for that MPDU. + Repeated preparation returns the same mode. Peer adaptation caused by another + packet cannot change this already captured series. +2. An actual completed data/management transmission consumes one try. RTS/CTS, + internal collisions, failed channel access, and duration queries consume none. + Use the existing transmitted-frame notification for this transition, with an + explicit prepared/in-flight/completed state to reject duplicate consumption. + ACK failure allows preparation of the next try; ACK success retires context. +3. For the full reference series, zero-based data-attempt ordinals 0–3 use stage + 0, 4–5 stage 1, 6–7 stage 2, and 8–9 stage 3. Low-rate disabled stages are + omitted using the verified reference predicates. Test every boundary. +4. Recovery and frame-sequence machinery remain the sole authority for retry + exhaustion and CW behavior. They can terminate before any series boundary. + If recovery permits attempts beyond the finite series, hold its last enabled + mode. This tail is an explicit INET policy; it is not hardware descriptor + exhaustion equivalence. A series must not silently grant extra MAC retries. +5. Existing success, data-exhaustion, RTS-exhaustion and internal-collision-drop + callbacks report one terminal completion with current recovery totals before + cleanup. They do not report one completion per series stage. Abandonment or + lifecycle cleanup must not invent a success or a transmitted data failure. +6. Erase the sidecar on every original-MPDU retirement and owner destruction. + Identity reuse starts fresh. A mode-set change invalidates borrowed modes: + discard the old series and prepare afresh against the new generation before + another exchange; keep actual recovery counters. If a protected exchange is + already committed, abort/restart it through the existing exchange machinery + before using a newly selected mode. Resolve the exact signal/abort ordering + in the implementation contract before writing that path. + +## Change surface and implementation sequence + +All named MAC targets are unsealed in the inspected registry. No new architecture +or naming exception is proposed. Follow the current +[contributor route](../../doc/project/guide/contribute-a-change.md), +[WLAN ownership rules](../../doc/project/domain/ieee80211.md), and +[test rules](../../doc/project/rule/testing.md). + +1. Verify the reference boundary table and mode mapping. Trace Tx duplication, + transmitted callbacks, mode-set signal ordering and all original-MPDU removal + paths. Resolve those details in a source-backed implementation contract. +2. Add value types, pure interface operations and base defaults, then Onoe's + opt-in builder and its deterministic boundary tests. Expected files: + `mac/common/RateRetry*.h`, `mac/contract/IRateControl.h`, + `mac/ratecontrol/{RateControlBase,OnoeRateControl}.{h,cc}`, Onoe NED and the + rate-control migration documentation. Keep interface headers free of policy. +3. Add resident-MPDU context ownership in `mac/queue/InProgressFrames.{h,cc}`; + thread it through `IRateSelection.h`, `IQosRateSelection.h`, both selection + implementations, `Dcf.{h,cc}`, `Hcf.{h,cc}` and both originator protection + implementations. Check frame-sequence/Tx paths before extending their APIs. + Recovery arithmetic requires no proposed change. +4. Add real DCF/HCF attempt evidence and cleanup tests; update Onoe documentation + with enablement, finite-series tail policy and compatibility limits. + +## Required verification + +New proposed tests: `OnoeRateControlRetrySeries_1.test` for the builder and +`OnoeRateControlMultirateRetry_1.test` for actual exchanges. They do not exist yet. + +| Case | Direct assertion | +| --- | --- | +| Full series, MAC limits permitting ten attempts | Actual PHY mode sequence follows 4/2/2/2 | +| Each low starting rate | Exact reference stage disabling and final-stage mapping | +| MAC limit before/at/after a stage boundary | No extra attempt; correct terminal callback | +| MAC limit beyond series capacity | Last enabled mode retained until MAC termination | +| RTS loss or internal collision before data | No data-stage advancement; correct completion total | +| Repeated duration queries and RTS protection | No consumed try; reserved and transmitted data modes match | +| Success at each stage | One completion and no later attempt | +| Peer, TID and fragment interleaving | Independent series and cleanup on identity reuse | +| Another packet changes the peer rate | Captured series remains stable | +| Mode-set reset and owner destruction | No stale mode or resident context | +| Disabled feature, AARF, fixed overrides and excluded traffic | Existing selection behavior retained | + +Extend the real-radio fixture pattern in `OnoeRateControlRtsFeedback_1.test` to +record the transmission object's actual PHY mode on every data attempt. A +stored-peer-rate assertion, request tag alone, or datarate-selected signal alone +does not prove that Tx emitted that rate. Check Duration/ID against authoritative +mode durations and existing response selection. Seed 0 is appropriate for +deterministically forced losses; do not claim throughput validation. + +After implementation, build fresh debug artifacts and run the explicit filter +`(OnoeRateControl|AarfRateControl|Ieee80211RecoveryRetryTotals|Ieee80211HcfInternalCollision).*\.test` +through `inet_run_module_tests -m debug -f`, plus directly mapped protection and +mode-selection cases discovered from the final diff. Run the MAC architecture +gate and naming gates for added artifacts. Record nonzero case counts, commands, +exit statuses and logs. Baseline changes require their own concrete proposal. + +## Design-step validation + +Source inspection established ownership, current callers, override precedence, +duration-query reentry and removal paths. The design intentionally separates +data-transmission progress from recovery totals. No production implementation, +behavioral test result, independent review or hardware equivalence is claimed. +Validate this plan with whitespace and local-link checks. Skill-package checks +from AGENTS.md are unavailable in this checkout and unrelated to this artifact. diff --git a/plan/pending/onoe-reference-alignment.md b/plan/pending/onoe-reference-alignment.md new file mode 100644 index 00000000000..962beb51c36 --- /dev/null +++ b/plan/pending/onoe-reference-alignment.md @@ -0,0 +1,390 @@ +# Align Onoe's decision algorithm with MadWifi + +Status: implemented and focused validation passed, 2026-09-10; pending review. + +## Implementation contract + +- Invariant and owner: Onoe owns only per-receiver completed-sample statistics, + credit, mode, and evaluation time. Recovery procedures retain ownership of + per-packet retry counters. Decision arithmetic follows the pinned MadWifi source. +- Entry and control path: DCF/HCF call `frameTransmitted()` after incrementing + failures and before clearing terminal retry state. Onoe consumes the terminal + count once, then evaluates a due sample; `getRate()` only initializes/reads state. +- Affected artifacts: Onoe `.cc/.h/.ned`, the three existing Onoe module tests, + additional Onoe boundary/feedback tests, and this plan/report. Shared callers, + interfaces, packets, and AARF behavior are unchanged. +- Siblings and terminals: exercise DCF/HCF normal-ACK success and exhaustion, + intermediate retries, peer and TID interleaving, idle queries, and the existing + mode-set signal reset. Feedback borrows packets and does not retain or delete them. +- Boundaries and units: elapsed simulation seconds from first peer observation or + last evaluation; terminal completion included at equality; integral counters, + truncated ten-percent threshold, credit 0 through 9, rate floor and ceiling. +- Verification: fresh debug build and `inet_run_module_tests -m debug -f + 'OnoeRateControl.*\.test'`; actual DCF/HCF callbacks establish retry mapping, + public API tests establish timing and decisions, scoped architecture check + establishes structural compliance. Existing INI/CSV searches find no configured + Onoe fingerprint case; report that gap rather than claim fingerprint coverage. + +The contract was self-validated against the current sources before editing. +The instruction to execute this plan authorizes the listed behavioral test +expectation changes. Deferred follow-ups remain outside this implementation. + +## Assessment and reference scope + +The supplied critique is substantially correct. PR #1176 fixes failed-attempt +double counting and failed-only fallback, but retains an approximation of Onoe's +decision algorithm. Its six statistical discrepancies are present in this checkout. +Adopt the MadWifi decision rules and completion-driven sampling together, then +describe the supported feedback scope precisely. Do not claim full MadWifi +compatibility from those changes alone. + +Evidence inspected: + +- [PR #1176](https://github.com/inet-framework/inet/pull/1176), whose retrieved head + matches local HEAD `6d7b49c1d51b5d28f0eb8d59d40bd8e3f507e07b`. +- [MadWifi onoe.c](https://github.com/proski/madwifi/blob/a7531fd223a1f454d3fd74a975b4581cde5411bb/ath_rate/onoe/onoe.c): + `ath_rate_ctl`, `ath_rate_tx_complete`, `ath_rate_update`, and `ath_rate_ctl_start`. + This is a pinned historical implementation, not proof of the earliest version. +- Local [Onoe implementation](../../src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc), + [state](../../src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h), + [parameters](../../src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned), + DCF/HCF feedback callers, recovery counters, and all three `OnoeRateControl*.test` cases. + +Onoe is an implementation-defined adaptation algorithm, not an IEEE 802.11 +normative algorithm. Use “reference implementation” in documentation. + +## Corrections and additions to the critique + +1. **Integer rounding matters.** MadWifi uses a strict comparison against an + integer-truncated percentage. With 19 successes and one retry, its threshold + is 1 and no credit is awarded, although 1/19 is below 10%. Preserve that + arithmetic; a floating ratio or cross-multiplication changes behavior. +2. **The floor changes reset behavior.** A failed-only sample below ten packets + clears credit at the lowest rate but retains statistics because the rate did + not actually change. Sufficient samples reset even at a rate limit. At the + ceiling, the tenth qualifying credit resets credit without a rate change. +3. **“About 100 packets” needs traffic assumptions.** One clean completion per + second produces ten qualifying ten-packet samples before an increase, from + zero credit and below the ceiling. Exact elapsed time depends on initialization + and boundary alignment; this is not a universal tenfold timing result. +4. **A callback is not necessarily a completed packet.** INET reports individual + failures; MadWifi commits retry totals with final success/error. Reordering + the existing callback alone can still contaminate a sample with an unfinished + packet's failures, including another HCF access category's packet. +5. **RTS accounting is not established by counting data failures.** DCF's + `originatorProcessRtsProtectionFailed()` has no rate-control callback, including + terminal RTS failure. `NonQosRecoveryProcedure::getRetryCount()` and its QoS + counterpart select a short or long counter by packet length; they do not sum + both. Therefore the current feedback cannot be asserted equivalent to + MadWifi's hardware short-plus-long retry status. +6. **MadWifi's retry chain has boundary behavior.** The inspected MadWifi chain + uses a lowest-hardware-rate final stage and disables redundant lower stages. + A future retry-series implementation must account for these details. +7. **Attribution needs care.** MadWifi names Atsushi Onoe's algorithm, while the + inspected file credits Sam Leffler as copyright holder. Correct INET's NED + description, which currently attributes development to Atheros. Implement + the behavior in INET style and retain applicable notices if source is copied. + +## Recommended scope + +Deliver exact decision arithmetic, credit transitions, sample retention, and +completion-triggered evaluation for the currently supported normal-ACK data and +management feedback. Keep the existing `initialRate` parameter, supported mode +navigation, per-receiver state, and default one-second interval. State that INET +starts a peer's interval when first seen; this is an initialization convention. + +Defer RTS/CTS feedback completeness, negotiated startup policy, and hardware-style +multirate retry to follow-up work. Aggregation/Block Ack and HT-or-newer fidelity +are not established by this legacy normal-ACK work. The resulting claim should be: + +> Onoe's decision rules follow the pinned MadWifi reference for supported +> normal-ACK completion feedback; startup and retry-series behavior use INET's +> existing policies, and RTS/Block Ack equivalence is not claimed. + +## Implementation sequence + +### 1. Establish completion feedback and timing + +Own changes in `OnoeRateControl.cc/.h` and focused module tests. Document the +existing callback contract in `IRateControl.h` only if needed; do not change +shared feedback semantics as part of this step. + +- Verify, with real DCF and HCF traces, that terminal `retryCount` contains the + accumulated data failures for the supported unprotected normal-ACK path: + clean success = 0; two failures then success = 2; immediate terminal failure + = 1; two nonterminal failures then terminal failure = 3. +- Use that terminal count once to update the completed sample. Intermediate + failures neither contribute to the completed sample nor trigger evaluation. + This avoids maintaining a second MAC retry counter in Onoe and prevents + interleaved HCF packets from sharing pending retry state. If traces invalidate + this mapping, resolve the producer contract before implementing this adapter; + do not substitute one pending counter per receiver. +- On success or give-up, record exactly one completion and its retries first, + then evaluate if the peer deadline has been reached. The completion at the + deadline belongs to the evaluated sample. Advance the deadline from that + evaluation time; do not run synthetic catch-up evaluations after idle time. +- Make `getRate()` return the stored rate after lazy peer initialization. It + must not evaluate or consume a decision deadline. Keep this local to Onoe. +- Preserve clearing all peer state on mode-set changes. Tests must demonstrate + that a reset clears counters, credit, rate references, and deadlines. + +Exit criterion: completion counts and retry totals match actual callback traces; +rate queries and unfinished retries cannot change the sampled rate or credit. + +### 2. Replace the decision block + +Use completed successes S, terminal errors E, and their retry total R: + +| Decision | Exact condition | +| --- | --- | +| Sufficient sample | S + E >= 10 | +| Down | (E > 0 and S == 0) or (sufficient and S < R) | +| Upward credit | sufficient and E == 0 and R < floor(S * 10 / 100) | +| Neutral | otherwise | + +- Down: attempt one lower mode and set credit to zero, including at the floor. +- Neutral: decrement credit only for a sufficient sample and only above zero. +- Upward credit: increment; at ten, attempt one higher mode and reset credit, + including at the ceiling. +- Clear S/E/R only after an actual rate change or a sufficient sample. Retain + insufficient samples otherwise, including failed-only samples at the floor. +- Remove `avgRetriesPerFrame` if unused after the change. Use appropriately wide + integer arithmetic for retained counts and percentage multiplication. +- Emit the existing per-peer signal on actual rate changes; preserve the initial + peer observation. Suppress false increase notifications at the ceiling, just + as the current code suppresses decreases at the floor. + +Exit criterion: all table cases below match independently derived reference +expectations; credit stays in [0, 9] after each decision. + +### 3. Repair and extend tests with the behavior change + +The existing tests encode the approximation in more than one place: + +- `OnoeRateControlRetryAccounting_1.test`: change four successes/eight failures + from a decrease to retention. Two successes/two give-ups/eight failures is + also insufficient and must retain the rate. Replace query-triggered ticks + with deliberately timed completions and assert retained state or subsequent + observable behavior. +- `OnoeRateControlFailureInterval_1.test`: replace the one-clean-packet-per-credit + expectation and the one-success/ten-retries decrease. Rework every evaluation + trigger for completion timing while retaining peer-isolation and floor checks. + Increase its simulation limit for the sparse-traffic case or split that case. +- `OnoeRateControlRetryFeedback_1.test`: preserve real DCF/HCF coverage, but make + it discriminate correct retry accounting. Four successes followed by a query + will no longer trigger a sufficient evaluation. Include ten successes with + exactly one failure each: R == S must hold the rate, while double counting + would incorrectly decrease it. Add one extra failure as the paired down case. + +Use public callback/rate APIs for the behavior tests, listeners for rate changes, +and a test subclass where direct credit/sample observation makes a boundary +assertion unambiguous. A helper-only test does not prove DCF/HCF integration. + +| Required case | Expected result at evaluation | +| --- | --- | +| S=4, E=0, R=8 | Hold rate and credit; retain sample | +| S=8, E=2, R=9 | Down; credit and sample reset | +| S=10, E=0, R=10 / R=11 | Neutral / down | +| S=10, E=0, R=0 / R=1 | Credit / neutral | +| S=19, E=0, R=1 | Neutral due to integer truncation | +| S=20, E=0, R=1 | Credit | +| S=100, E=1, R=1 | No credit; decay existing positive credit | +| Repeated sufficient neutral samples, credit initially 0 | Credit stays 0 | +| Three clean completions per second | Samples accumulate; first credit at 12 completions | +| One clean completion per second, zero initial credit | First increase after 100 qualifying completions | +| E=1, S=0 below ten completions | Down above floor; clear credit but retain sample at floor | +| Ten qualifying samples at maximum rate | Credit resets; no rate-change signal | +| Completion just before / at / after deadline | No early evaluation; boundary completion included | +| Long idle gap, repeated getRate calls | No evaluation or credit manufactured | +| Unfinished retries across a deadline and HCF AC interleaving | Only completed packets enter sample | +| Two peers and mode-set reset | No state leakage; no stale state after reset | + +For each independent table row, identify the pinned reference function in the +test description. Separate single-sample tests from accumulation tests so retained +state does not accidentally alter a later case's premise. + +### 4. Validate and document the supported behavior + +From the repository root, after implementation: + +```sh +make MODE=debug -j$(nproc) +inet_run_module_tests -m debug -f 'OnoeRateControl.*\.test' +doc/project/enforcement/check-architecture.sh src/inet/linklayer/ieee80211/mac/ratecontrol +``` + +The module runner and scoped architecture script exist in this checkout. Record +the selected case count (zero is not a pass), commands, build mode, seeds, exit +statuses, and artifacts. Run the directly affected AARF cases too if a shared +contract or caller changes. Map Onoe-using configurations to focused fingerprint +checks; explain divergences with behavioral evidence. Before push, follow the +debug/release builds and project-wide gates in +[run-the-gates.md](../../doc/project/guide/run-the-gates.md). + +Update the NED description and PR explanation with the reference revision, +completion semantics, changed low-load behavior, and explicit compatibility +limits. Suggested review units are completion accounting/timing with its tests, +then decision rules with their tests and documentation; each unit must pass its +own affected tests. No history rewrite is needed to execute this plan. + +## Follow-up work, in order + +1. **Complete RTS feedback:** design how data and RTS attempts contribute to one + terminal outcome without double counting. Cover failed CTS exchanges, RTS + exhaustion, and mixed RTS/data failures through DCF and HCF. Review all rate + controllers before changing shared feedback. This is a prerequisite for a + broader hardware-status equivalence claim. **Completed 2026-09-10:** see the + [implementation and verification report](onoe-rts-feedback.md). The original + implementation report below records the scope before this follow-up. +2. **Startup policy:** the pinned MadWifi source starts at the highest negotiated + 11b rate, otherwise the highest negotiated rate no greater than 36 Mbps. + Decide how this maps to INET's peer capabilities and explicit initial-rate + configuration. Do not alter shared `RateControlBase` defaults for Onoe alone. + **Local-mode policy completed 2026-09-10:** automatic legacy startup now + follows the reference ceiling using local modes; explicit rates take + precedence and HT/VHT defaults are retained. Negotiated legacy peer rates + are unavailable through the current contract. See the + [decision and verification report](onoe-startup-policy.md). +3. **Multirate retry:** design a typed retry-context/series contract between rate + selection, recovery, and control, respecting existing state ownership. Specify + stage truncation at low rates, MAC retry-limit precedence, and packet identity + against the pinned MadWifi reference. Validate transmitted PHY rates for + every attempt; a stored-peer-rate assertion cannot prove a 4/2/2/2 series. + **Design step completed 2026-09-10:** see the + [retry-series contract and verification plan](onoe-multirate-retry.md). + Implementation is pending reference-boundary verification and the explicit + Tx/lifecycle integration checks recorded there. + +## Project guidance and completion gates + +This plan follows [the contributor route](../../doc/project/guide/contribute-a-change.md), +[WLAN ownership and boundaries](../../doc/project/domain/ieee80211.md#ar-wlan-arch-boundaries), +[testing rules](../../doc/project/rule/testing.md), and +[the seal registry](../../doc/project/audit/seal-list.md). The proposed Onoe source +paths are unsealed; no new architecture/naming exception is proposed. The execution +report below records focused validation and self-audit, not an independent audit. + +Recorded expectation changes require a concrete scope-and-reason proposal under +[TR-BASELINE-DELIBERATE](../../doc/project/rule/testing.md#tr-baseline-deliberate) +before editing those expectations. The instruction to execute this concrete plan +authorized its listed test changes. No fingerprint baselines were changed. + +The supplied AGENTS.md describes skill-source packaging checks, but this active +checkout has no `scripts/validate_skill_suite.py`, `scripts/package_skill_suite.py`, +or `tests/skill-suite`. Those checks cannot run here; no skill/package content is +changed by this plan. Validate this artifact with `git diff --check` and verify +its local links. Implementation is complete only when the behavioral matrix, +production DCF/HCF evidence, focused regression results, and scoped documentation +claim are all satisfied. + +## Execution report + +### Result and review description + +Onoe now bases decisions on completed normal-ACK frames. Four successes with eight +retries retain their sample; ten completed frames with more retries than successes +lower the rate. Upward credit requires a sufficient error-free sample and the +integer-truncated retry threshold. Credit cannot become negative, and small +samples persist between evaluations. At one clean completion per second, the +first increase from zero credit takes 100 completions rather than ten. + +Terminal feedback contributes the recovery procedure's retry count once, before +checking the interval. Intermediate failures and rate queries do not evaluate +samples. Rate-change signals are emitted only for actual changes, with the +existing initial peer observation preserved. INET startup and retry-series +policies remain in place; RTS/Block Ack equivalence is not claimed. + +This text is prepared as the Onoe explanation for PR #1176. No remote PR edit, +commit, history rewrite, or push was performed. + +### Changed paths + +- `src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc` +- `src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h` +- `src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned` +- `tests/module/OnoeRateControlRetryAccounting_1.test` +- `tests/module/OnoeRateControlFailureInterval_1.test` +- `tests/module/OnoeRateControlRetryFeedback_1.test` +- `tests/module/OnoeRateControlCompletionFeedback_1.test` (new) +- `tests/module/OnoeRateControlSampling_1.test` (new) +- `tests/module/OnoeRateControlInterleavedFeedback_1.test` (new) +- This plan, which is ignored by the checkout's existing Git rules. + +The pre-existing AGENTS.md modification was preserved. No shared production +interface or caller changed. The division `successes / 10` implements the pinned +reference's integer-truncated ten-percent threshold without an intermediate +multiplication. Sample counters are now 64-bit. These are implementation choices +within the approved contract, not additional behavior scope. + +### Focused evidence + +All commands ran from `/home/user/omnetpp_ws/inet-agent-rate-control`. +Every module case used configuration `General`, run 0, `seed-set = 0`, debug mode. +The scenarios use deterministic outcomes; no probabilistic performance claim or +multi-seed campaign is made. + +| Command | Exit | Result / artifact | +| --- | --- | --- | +| `make MODE=debug -j$(nproc)` | 0 | Fresh debug library; `/tmp/onoe-alignment/build-after.log` | +| `inet_run_module_tests -m debug -f 'OnoeRateControl.*\.test'` | 0 | Six selected tests, six PASS; `/tmp/onoe-alignment/module-final.log` | +| `doc/project/enforcement/check-architecture.sh src/inet/linklayer/ieee80211/mac/ratecontrol` | 0 | PASS; `/tmp/onoe-alignment/architecture.log` | +| `doc/project/enforcement/check-naming.sh src/inet/linklayer/ieee80211/mac/ratecontrol` | 0 | PASS; `/tmp/onoe-alignment/naming.log` | +| `git diff --check` | 0 | No whitespace errors | + +Test-to-claim mapping: + +| Test | Direct evidence | +| --- | --- | +| RetryAccounting | Sixteen isolated reference samples: insufficient/sufficient boundary, retries versus successes, errors blocking credit, integer truncation, credit decay, and tenth-credit increase; retained counters checked explicitly | +| FailureInterval | Idle queries, unfinished retries, ten neutral periods without negative credit, 100 sparse clean completions, sufficient high-retry fallback, peer isolation, downward signal suppression at the floor | +| RetryFeedback | Real DCF/HCF paired runs: ten successful packets with ten retries hold 24 Mbps; eleven retries lower to 18 Mbps; completed statistics clear | +| CompletionFeedback | Eight real DCF/HCF scenarios establishing terminal counts 0, 2, 1, and 3 for clean success, retry success, and two retry-exhaustion limits | +| Sampling | Before/equal/after deadlines, small samples across periods, no catch-up after idle, direct interleaved TID feedback, first credit at twelve completions for three per interval, floor retention/reset, ceiling credit reset without signals, and actual mode-set signal clearing both peers | +| InterleavedFeedback | Real HCF AC_BK failure, AC_VO success, then AC_BK retry success for one receiver; completed retry totals observed as 0, 0, then 1 | + +Raw simulation output is under +`tests/module/work//test.out` and `test.err`. +The interleaving trace is: + +```text +feedback 1: tid=1 retries=1 success=0 +feedback 2: tid=6 retries=0 success=1 +feedback 3: tid=1 retries=1 success=1 +HCF interleaved completion feedback verified. +``` + +During test development, fixture failures identified an unused DCF test-controller +instance on QoS stations, a missing include, an unassigned application parameter, +a legacy sink receiving QoS frames, and an error-model cast that assumed every +frame was data. Each fixture was corrected before the final six-test run; no +production workaround or changed seed was used to obtain a pass. + +### Self-audit and remaining scope + +- Owner/control path retraced: DCF/HCF notify before terminal retry cleanup; + Onoe retains only completed statistics, owns no duplicate per-packet counter, + borrows feedback packets, and uses existing mode-navigation and signal APIs. +- C++ numeric and state checks: strict retry comparisons, truncated percentage, + zero-bounded credit, reset conditions, and rate limits match the contract. + State and evaluation time are updated before rate-change notification. +- OMNeT++ checks: the existing initialization-stage coverage is preserved; + elapsed time remains `simtime_t`; no self-messages, random draws, or new + subscriptions are introduced. The real mode-set reset path is tested. +- INET/WLAN checks: packet representation, MAC recovery, rate selection, PHY, + mode definitions, and shared AARF behavior are unchanged. The test evidence + distinguishes public callback behavior from real DCF/HCF transmission paths. + No new architecture/naming exception or protected-source change is needed. +- `rg -n -i 'onoe' --glob '*.ini' --glob '*.csv' examples showcases tests/fingerprint` + returned no matches (exit 1). There is no existing directly configured Onoe + fingerprint case in those trees; no fingerprint result or baseline update is + claimed. The six module cases supply the focused behavioral evidence. +- No release build or project-wide before-push campaign was run, because this + task did not push. Run the existing before-push gates when preparing publication. + AARF was not retested because no shared caller, interface, or implementation + changed. No independent reviewer was invoked. +- RTS/CTS completeness, negotiated startup, multirate retry, and aggregation or + newer-PHY fidelity remain the documented follow-ups. Real feedback tests cover + data frames; management callback compatibility is source-traced, without a + separate management-exchange runtime test. +- The skill-suite packaging tools listed in AGENTS.md are absent in this INET + checkout. No skill package was changed or represented as validated.