From e05a11b67c87b71f8a19828327a161d3dcc29b30 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 6 Aug 2026 15:09:59 +0200 Subject: [PATCH 1/6] ieee80211: fix TxopProcedure::getRemaining() returning the elapsed time getRemaining() computed now - start, which is the time already spent in the TXOP rather than the time left in it. The guard above it already handles the expired case, so the remaining time is start + limit - now. This is a correctness fix with no behavioural effect. The only caller, ~HcfFs, tests the result against zero, and reaches it only once a frame of the TXOP has already been transmitted -- so now is strictly greater than start, and elapsed and remaining are either both positive, while the TXOP is live, or both zero once it has expired. Verified by a TXOP-limit sweep whose recorded scalars are identical before and after. --- src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc index 0ec6898bd00..340270693a3 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/TxopProcedure.cc @@ -96,7 +96,7 @@ simtime_t TxopProcedure::getRemaining() const if (start == -1) throw cRuntimeError("Txop has not started yet"); auto now = simTime(); - return now > start + limit ? 0 : now - start; + return now > start + limit ? 0 : start + limit - now; } simtime_t TxopProcedure::getDuration() const From 359e5b690ef7af85c0d510997faf4105629eede6 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 14 Aug 2026 16:30:07 +0200 Subject: [PATCH 2/6] ieee80211: make rate control adapt per receiver ~AarfRateControl and ~OnoeRateControl kept one set of adaptive state per module: a single current mode, timer, probe flag, threshold and success counter. A station transmitting to several peers therefore blended their feedback into one rate -- a retry to a distant station pushed the rate down for a nearby one, and vice versa. This is visible on an access point serving clients at different distances, which is the normal case. IRateControl::getRate() now takes the receiver address, and both algorithms key their state on it: every variable that adapts moves into a per-receiver State, while the configured thresholds and intervals stay shared. State for a station is created on first use, seeded from initialRate (or the fastest mandatory mode), and dropped when the mode set changes. The interval timer of a new station is seeded with the current time, so the periodic rate increase measures its interval from when the station was first seen -- not from t=0, which for a station first used later than one interval would put the deadline already in the past at creation. RateControlBase no longer holds a currentMode, so the watched expression that fed the display string moves to the subclasses and reports the number of stations being tracked instead. --- .../ieee80211/mac/contract/IRateControl.h | 4 +- .../mac/ratecontrol/AarfRateControl.cc | 109 ++++++++++-------- .../mac/ratecontrol/AarfRateControl.h | 29 +++-- .../mac/ratecontrol/AarfRateControl.ned | 2 +- .../mac/ratecontrol/OnoeRateControl.cc | 94 ++++++++------- .../mac/ratecontrol/OnoeRateControl.h | 32 +++-- .../mac/ratecontrol/OnoeRateControl.ned | 2 +- .../mac/ratecontrol/RateControlBase.cc | 24 ++-- .../mac/ratecontrol/RateControlBase.h | 13 ++- .../mac/rateselection/QosRateSelection.cc | 4 +- .../mac/rateselection/RateSelection.cc | 2 +- 11 files changed, 184 insertions(+), 131 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h index d940563ab9e..85a2258ea87 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRateControl.h @@ -9,6 +9,7 @@ #define __INET_IRATECONTROL_H #include "inet/common/packet/Packet.h" +#include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo_m.h" @@ -25,7 +26,8 @@ class INET_API IRateControl public: virtual ~IRateControl() {} - virtual const physicallayer::IIeee80211Mode *getRate() = 0; + // Returns the rate to use for a unicast frame addressed to the given receiver. + virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) = 0; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) = 0; virtual void frameReceived(Packet *frame) = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc index e96da78e693..b5c4a968724 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc @@ -19,18 +19,14 @@ void AarfRateControl::initialize(int stage) RateControlBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { factor = par("increaseThresholdFactor"); - increaseThreshold = par("increaseThreshold"); maxIncreaseThreshold = par("maxIncreaseThreshold"); decreaseThreshold = par("decreaseThreshold"); interval = par("interval"); + WATCH_EXPR("numStations", (int)stations.size()); WATCH(factor); - WATCH(increaseThreshold); WATCH(maxIncreaseThreshold); WATCH(decreaseThreshold); WATCH(interval); - WATCH(timer); - WATCH(probing); - WATCH(numberOfConsSuccTransmissions); } else if (stage == INITSTAGE_LINK_LAYER) { } @@ -41,65 +37,80 @@ void AarfRateControl::handleMessage(cMessage *msg) throw cRuntimeError("This module doesn't handle self messages"); } + +AarfRateControl::State& AarfRateControl::stateFor(const MacAddress& receiverAddress) +{ + auto it = stations.find(receiverAddress); + if (it == stations.end()) { + State state; + state.mode = getInitialMode(); + state.increaseThreshold = par("increaseThreshold"); + state.timer = simTime(); // the interval starts when the station is first seen, not at t=0 + it = stations.insert({receiverAddress, state}).first; + emitDatarateChangedSignal(state.mode); + } + return it->second; +} + void AarfRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) { - increaseRateIfTimerIsExpired(); - - if (!isSuccessful && probing) { // probing packet failed - numberOfConsSuccTransmissions = 0; - currentMode = decreaseRateIfPossible(currentMode); - emitDatarateChangedSignal(); - EV_DETAIL << "Decreased rate to " << *currentMode << endl; - multiplyIncreaseThreshold(factor); - resetTimer(); + State& state = stateFor(getReceiverAddress(frame)); + increaseRateIfTimerIsExpired(state); + + if (!isSuccessful && state.probing) { // probing packet failed + state.numberOfConsSuccTransmissions = 0; + state.mode = decreaseRateIfPossible(state.mode); + emitDatarateChangedSignal(state.mode); + EV_DETAIL << "Decreased rate to " << *state.mode << endl; + multiplyIncreaseThreshold(state, factor); + resetTimer(state); } else if (!isSuccessful && retryCount >= decreaseThreshold - 1) { // decreaseThreshold consecutive failed transmissions - numberOfConsSuccTransmissions = 0; - currentMode = decreaseRateIfPossible(currentMode); - emitDatarateChangedSignal(); - EV_DETAIL << "Decreased rate to " << *currentMode << endl; - resetIncreaseThreshdold(); - resetTimer(); + state.numberOfConsSuccTransmissions = 0; + state.mode = decreaseRateIfPossible(state.mode); + emitDatarateChangedSignal(state.mode); + EV_DETAIL << "Decreased rate to " << *state.mode << endl; + resetIncreaseThreshdold(state); + resetTimer(state); } else if (isSuccessful && retryCount == 0) - numberOfConsSuccTransmissions++; - - if (numberOfConsSuccTransmissions == increaseThreshold) { - numberOfConsSuccTransmissions = 0; - currentMode = increaseRateIfPossible(currentMode); - emitDatarateChangedSignal(); - EV_DETAIL << "Increased rate to " << *currentMode << endl; - resetTimer(); - probing = true; + state.numberOfConsSuccTransmissions++; + + if (state.numberOfConsSuccTransmissions == state.increaseThreshold) { + state.numberOfConsSuccTransmissions = 0; + state.mode = increaseRateIfPossible(state.mode); + emitDatarateChangedSignal(state.mode); + EV_DETAIL << "Increased rate to " << *state.mode << endl; + resetTimer(state); + state.probing = true; } else - probing = false; - + state.probing = false; } -void AarfRateControl::multiplyIncreaseThreshold(double factor) +void AarfRateControl::multiplyIncreaseThreshold(State& state, double factor) { - if (increaseThreshold * factor <= maxIncreaseThreshold) - increaseThreshold *= factor; + if (state.increaseThreshold * factor <= maxIncreaseThreshold) + state.increaseThreshold *= factor; } -void AarfRateControl::resetIncreaseThreshdold() +void AarfRateControl::resetIncreaseThreshdold(State& state) { - increaseThreshold = par("increaseThreshold"); + state.increaseThreshold = par("increaseThreshold"); } -void AarfRateControl::resetTimer() +void AarfRateControl::resetTimer(State& state) { - timer = simTime(); + state.timer = simTime(); } -void AarfRateControl::increaseRateIfTimerIsExpired() +void AarfRateControl::increaseRateIfTimerIsExpired(State& state) { - if (simTime() - timer >= interval) { - currentMode = increaseRateIfPossible(currentMode); - emitDatarateChangedSignal(); - EV_DETAIL << "Increased rate to " << *currentMode << endl; - resetTimer(); + if (simTime() - state.timer >= interval) { + state.mode = increaseRateIfPossible(state.mode); + emitDatarateChangedSignal(state.mode); + EV_DETAIL << "Increased rate to " << *state.mode << endl; + resetTimer(state); } } @@ -107,14 +118,14 @@ void AarfRateControl::frameReceived(Packet *frame) { } -const IIeee80211Mode *AarfRateControl::getRate() +const IIeee80211Mode *AarfRateControl::getRate(const MacAddress& receiverAddress) { Enter_Method("getRate"); - increaseRateIfTimerIsExpired(); - EV_INFO << "The current mode is " << currentMode << " the net bitrate is " << currentMode->getDataMode()->getNetBitrate() << std::endl; - return currentMode; + State& state = stateFor(receiverAddress); + increaseRateIfTimerIsExpired(state); + EV_INFO << "The current mode is " << state.mode << " the net bitrate is " << state.mode->getDataMode()->getNetBitrate() << std::endl; + return state.mode; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h index 96c35ea93a6..66618e489cf 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h @@ -19,28 +19,37 @@ namespace ieee80211 { class INET_API AarfRateControl : public RateControlBase { protected: - simtime_t timer = SIMTIME_ZERO; + // Per-receiver adaptive state (formerly single-instance module members). + struct State { + const physicallayer::IIeee80211Mode *mode = nullptr; + simtime_t timer = SIMTIME_ZERO; + bool probing = false; + int increaseThreshold = -1; + int numberOfConsSuccTransmissions = 0; + }; + std::map stations; + + // configuration, shared across stations simtime_t interval = SIMTIME_ZERO; - bool probing = false; - int increaseThreshold = -1; int maxIncreaseThreshold = -1; int decreaseThreshold = -1; double factor = -1; - int numberOfConsSuccTransmissions = 0; - protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void handleMessage(cMessage *msg) override; - virtual void multiplyIncreaseThreshold(double factor); - virtual void resetIncreaseThreshdold(); - virtual void resetTimer(); - virtual void increaseRateIfTimerIsExpired(); + virtual State& stateFor(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() override; + 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/AarfRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned index 01d48d21d7d..17121012f64 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned @@ -26,7 +26,7 @@ simple AarfRateControl extends SimpleModule like IRateControl // needed to increase the rate 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("{currentMode}"); + displayStringTextFormat = default("{numStations} stations"); @display("i=block/cogwheel"); @signal[datarateChanged]; @statistic[datarateChanged](title="datarate"; record=vector; interpolationmode=sample-hold); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index 4fbc0110fa7..9b954f5b2cb 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -19,20 +19,29 @@ void OnoeRateControl::initialize(int stage) RateControlBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { interval = par("interval"); - WATCH(timer); - WATCH(numOfRetries); - WATCH(credit); - WATCH(numOfSuccTransmissions); - WATCH(numOfGivenUpTransmissions); - WATCH(avgRetriesPerFrame); + WATCH_EXPR("numStations", (int)stations.size()); } } -void OnoeRateControl::resetStatisticalVariables() + +OnoeRateControl::State& OnoeRateControl::stateFor(const MacAddress& receiverAddress) +{ + auto it = stations.find(receiverAddress); + if (it == stations.end()) { + State state; + state.mode = getInitialMode(); + state.timer = simTime(); // the interval starts when the station is first seen, not at t=0 + it = stations.insert({receiverAddress, state}).first; + emitDatarateChangedSignal(state.mode); + } + return it->second; +} + +void OnoeRateControl::resetStatisticalVariables(State& state) { - numOfRetries = 0; - numOfSuccTransmissions = 0; - numOfGivenUpTransmissions = 0; + state.numOfRetries = 0; + state.numOfSuccTransmissions = 0; + state.numOfGivenUpTransmissions = 0; } void OnoeRateControl::handleMessage(cMessage *msg) @@ -42,20 +51,21 @@ void OnoeRateControl::handleMessage(cMessage *msg) void OnoeRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) { - computeModeIfTimerIsExpired(); + State& state = stateFor(getReceiverAddress(frame)); + computeModeIfTimerIsExpired(state); if (isSuccessful) - numOfSuccTransmissions++; + state.numOfSuccTransmissions++; else if (isGivenUp) - numOfGivenUpTransmissions++; + state.numOfGivenUpTransmissions++; if (retryCount > 0) - numOfRetries++; + state.numOfRetries++; } -void OnoeRateControl::computeModeIfTimerIsExpired() +void OnoeRateControl::computeModeIfTimerIsExpired(State& state) { - if (simTime() - timer >= interval) { - computeMode(); - timer = simTime(); + if (simTime() - state.timer >= interval) { + computeMode(state); + state.timer = simTime(); } } @@ -63,42 +73,42 @@ void OnoeRateControl::frameReceived(Packet *frame) { } -void OnoeRateControl::computeMode() +void OnoeRateControl::computeMode(State& state) { - int numOfFrameTransmitted = numOfSuccTransmissions + numOfGivenUpTransmissions + numOfRetries; - avgRetriesPerFrame = double(numOfRetries) / (numOfSuccTransmissions + numOfGivenUpTransmissions); - - if (numOfSuccTransmissions > 0) { - if (numOfFrameTransmitted >= 10 && avgRetriesPerFrame > 1) { - currentMode = decreaseRateIfPossible(currentMode); - emitDatarateChangedSignal(); - EV_DETAIL << "Decreased rate to " << *currentMode << endl; - credit = 0; + 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.mode); + EV_DETAIL << "Decreased rate to " << *state.mode << endl; + state.credit = 0; } - else if (avgRetriesPerFrame >= 0.1) - credit--; + else if (state.avgRetriesPerFrame >= 0.1) + state.credit--; else - credit++; + state.credit++; - if (credit >= 10) { - currentMode = increaseRateIfPossible(currentMode); - emitDatarateChangedSignal(); - EV_DETAIL << "Increased rate to " << *currentMode << endl; - credit = 0; + if (state.credit >= 10) { + state.mode = increaseRateIfPossible(state.mode); + emitDatarateChangedSignal(state.mode); + EV_DETAIL << "Increased rate to " << *state.mode << endl; + state.credit = 0; } - resetStatisticalVariables(); + resetStatisticalVariables(state); } } -const IIeee80211Mode *OnoeRateControl::getRate() +const IIeee80211Mode *OnoeRateControl::getRate(const MacAddress& receiverAddress) { Enter_Method("getRate"); - computeModeIfTimerIsExpired(); - EV_INFO << "The current mode is " << currentMode << " the net bitrate is " << currentMode->getDataMode()->getNetBitrate() << std::endl; - return currentMode; + State& state = stateFor(receiverAddress); + computeModeIfTimerIsExpired(state); + EV_INFO << "The current mode is " << state.mode << " the net bitrate is " << state.mode->getDataMode()->getNetBitrate() << std::endl; + return state.mode; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index 6e978d36851..797a6cfd402 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -19,27 +19,35 @@ namespace ieee80211 { class INET_API OnoeRateControl : public RateControlBase { protected: - simtime_t timer = SIMTIME_ZERO; + // Per-receiver adaptive state (formerly single-instance module members). + struct State { + 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; + }; + std::map stations; + + // configuration, shared across stations simtime_t interval = SIMTIME_ZERO; - int numOfRetries = 0; - int numOfSuccTransmissions = 0; - int numOfGivenUpTransmissions = 0; - - double avgRetriesPerFrame = 0; - int credit = 0; - protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void handleMessage(cMessage *msg) override; - virtual void computeMode(); - virtual void resetStatisticalVariables(); - virtual void computeModeIfTimerIsExpired(); + virtual State& stateFor(const MacAddress& receiverAddress); + virtual void resetRateControl() override { stations.clear(); } + + virtual void computeMode(State& state); + virtual void resetStatisticalVariables(State& state); + virtual void computeModeIfTimerIsExpired(State& state); public: - virtual const physicallayer::IIeee80211Mode *getRate() override; + 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.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index dcecaf3c474..62304f03ee8 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -20,7 +20,7 @@ simple OnoeRateControl extends SimpleModule like IRateControl @class(OnoeRateControl); double initialRate @unit(bps) = default(-1bps); // -1 means the fastest mandatory rate double interval @unit(s) = default(1s); - displayStringTextFormat = default("{currentMode}"); + displayStringTextFormat = default("{numStations} stations"); @display("i=block/cogwheel"); @signal[datarateChanged]; @statistic[datarateChanged](title="datarate"; record=vector; interpolationmode=sample-hold); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index 0101c90e18d..c7fb4fcf0c3 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h" +#include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" namespace inet { @@ -19,9 +20,6 @@ simsignal_t RateControlBase::datarateChangedSignal = cComponent::registerSignal( void RateControlBase::initialize(int stage) { ModeSetListener::initialize(stage); - - if (stage == INITSTAGE_LOCAL) - WATCH_EXPR("currentMode", currentMode ? currentMode->getName() : "none"); } const IIeee80211Mode *RateControlBase::increaseRateIfPossible(const IIeee80211Mode *currentMode) @@ -36,9 +34,21 @@ const IIeee80211Mode *RateControlBase::decreaseRateIfPossible(const IIeee80211Mo return newMode == nullptr ? currentMode : newMode; } -void RateControlBase::emitDatarateChangedSignal() +MacAddress RateControlBase::getReceiverAddress(Packet *frame) const +{ + const auto& header = frame->peekAtFront(); + return header->getReceiverAddress(); +} + +const IIeee80211Mode *RateControlBase::getInitialMode() +{ + double initialRate = par("initialRate"); + return initialRate == -1 ? modeSet->getFastestMandatoryMode() : modeSet->getMode(bps(initialRate)); +} + +void RateControlBase::emitDatarateChangedSignal(const IIeee80211Mode *mode) { - bps rate = currentMode->getDataMode()->getNetBitrate(); + bps rate = mode->getDataMode()->getNetBitrate(); emit(datarateChangedSignal, rate.get()); } @@ -48,9 +58,7 @@ void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cO if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); - double initRate = par("initialRate"); - currentMode = initRate == -1 ? modeSet->getFastestMandatoryMode() : modeSet->getMode(bps(initRate)); - emitDatarateChangedSignal(); + resetRateControl(); } } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index c0dff2f6ac2..743076141b1 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -8,6 +8,7 @@ #ifndef __INET_RATECONTROLBASE_H #define __INET_RATECONTROLBASE_H +#include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" @@ -19,15 +20,19 @@ class INET_API RateControlBase : public ModeSetListener, public IRateControl public: static simsignal_t datarateChangedSignal; - protected: - const physicallayer::IIeee80211Mode *currentMode = nullptr; - protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; - virtual void emitDatarateChangedSignal(); + // The receiver MAC address of a transmitted (or received) frame, which keys the per-station state. + virtual MacAddress getReceiverAddress(Packet *frame) const; + // The mode a newly seen station starts from: the initialRate parameter, or the fastest mandatory mode. + virtual const physicallayer::IIeee80211Mode *getInitialMode(); + // Emits datarateChanged with the rate of the given mode. + virtual void emitDatarateChangedSignal(const physicallayer::IIeee80211Mode *mode); + // Drops all per-station state; called by subclasses' override when the mode set changes. + virtual void resetRateControl() {} const physicallayer::IIeee80211Mode *increaseRateIfPossible(const physicallayer::IIeee80211Mode *currentMode); const physicallayer::IIeee80211Mode *decreaseRateIfPossible(const physicallayer::IIeee80211Mode *currentMode); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index c8579ac4424..961937a447c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -141,7 +141,7 @@ const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const PtrgetRate(); + return dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress()); else return fastestMandatoryMode; } @@ -159,7 +159,7 @@ const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const PtrgetRate(); + return dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress()); else return fastestMandatoryMode; } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 726b52c3f09..8a7cc6a5dcd 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -121,7 +121,7 @@ const IIeee80211Mode *RateSelection::computeDataOrMgmtFrameMode(const Ptr(dataOrMgmtHeader) && mgmtFrameMode) return mgmtFrameMode; if (dataOrMgmtRateControl) - return dataOrMgmtRateControl->getRate(); + return dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress()); else return fastestMandatoryMode; } From 1baf65123b1ece67838cbb446e74dc646e6be2f0 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 14 Aug 2026 16:46:07 +0200 Subject: [PATCH 3/6] networklayer: name the host owning a MAC address Naming a peer by the address a model knows it by -- to label the per-station series of a demultiplexed statistic, for example -- takes both a lookup and a naming convention, and neither belongs to the model that happens to need it first. Add L3AddressResolver::getHostNameWithMacAddress(), next to the findHostWithMacAddress() it builds on. It names the host by its path relative to the network, so that hosts of the same name in different subnetworks are distinguishable, and falls back to the MAC address string when no host owns the address. --- src/inet/networklayer/common/L3AddressResolver.cc | 15 +++++++++++++++ src/inet/networklayer/common/L3AddressResolver.h | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/src/inet/networklayer/common/L3AddressResolver.cc b/src/inet/networklayer/common/L3AddressResolver.cc index e6f5efc506f..17e116ea2b5 100644 --- a/src/inet/networklayer/common/L3AddressResolver.cc +++ b/src/inet/networklayer/common/L3AddressResolver.cc @@ -574,5 +574,20 @@ cModule *L3AddressResolver::findHostWithMacAddress(const MacAddress& addr) return entry ? entry->getInterfaceTable()->getHostModule() : nullptr; } +std::string L3AddressResolver::getHostNameWithMacAddress(const MacAddress& addr) +{ + cModule *host = findHostWithMacAddress(addr); + if (host == nullptr) + return addr.str(); + // the path relative to the network, so that hosts of the same name in different + // subnetworks get different names; for a host directly under the network this is + // just its name + std::string name = host->getFullPath(); + std::string networkPrefix = std::string(host->getSimulation()->getSystemModule()->getFullName()) + "."; + if (name.compare(0, networkPrefix.length(), networkPrefix) == 0) + name.erase(0, networkPrefix.length()); + return name; +} + } // namespace inet diff --git a/src/inet/networklayer/common/L3AddressResolver.h b/src/inet/networklayer/common/L3AddressResolver.h index 57ba2756728..44d8b989733 100644 --- a/src/inet/networklayer/common/L3AddressResolver.h +++ b/src/inet/networklayer/common/L3AddressResolver.h @@ -205,6 +205,15 @@ class INET_API L3AddressResolver * Find the host with the specified MAC address. Returns nullptr if not found. */ virtual cModule *findHostWithMacAddress(const MacAddress& addr); + + /** + * Returns a name identifying the host with the specified MAC address: its path + * relative to the network, which is just the host name unless the host is nested + * in a subnetwork. Falls back to the MAC address string if no such host is found. + * Useful wherever a peer has to be named by the address a model knows it by, e.g. + * to label the per-peer series of a demultiplexed statistic. + */ + virtual std::string getHostNameWithMacAddress(const MacAddress& addr); //@} }; From 0d76de8dc9c59dfd5321088bc99485f48247ef27 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 14 Aug 2026 16:31:27 +0200 Subject: [PATCH 4/6] ieee80211: record the data rate per station The datarateChanged statistic recorded one interleaved vector per rate control module, in which the rates chosen for different peers are indistinguishable. Now that rate control adapts per receiver, the rate of an individual station is what the statistic should be able to show. datarateChanged is emitted with the receiver's station name as a named details object, and a new dataratePerStation statistic uses demux(datarateChanged) to record one data rate vector per station. The aggregate datarateChanged statistic ignores details and is unchanged. Group-addressed frames carry no per-station rate and are emitted without details, so they land only in the aggregate. This is also the observable that makes the per-receiver adaptation of the previous commit testable, so the module test for it comes with it. One source saturates a near and a far ad-hoc peer at once; the rate towards the near peer stays at 54 Mbps while the far peer's retries pull its own rate down to 24.7 Mbps. With the single set of adaptive state the two peers previously shared, both are served at a blended 29.6 Mbps instead, and the far peer receives 380 packets rather than 1350. --- .../mac/ratecontrol/AarfRateControl.cc | 11 +- .../mac/ratecontrol/AarfRateControl.h | 1 + .../mac/ratecontrol/AarfRateControl.ned | 3 +- .../mac/ratecontrol/OnoeRateControl.cc | 7 +- .../mac/ratecontrol/OnoeRateControl.h | 1 + .../mac/ratecontrol/OnoeRateControl.ned | 3 +- .../mac/ratecontrol/RateControlBase.cc | 13 ++- .../mac/ratecontrol/RateControlBase.h | 7 +- .../module/AarfRateControlPerReceiver_1.test | 103 ++++++++++++++++++ 9 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 tests/module/AarfRateControlPerReceiver_1.test diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc index b5c4a968724..9392a205cc7 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc @@ -43,11 +43,12 @@ AarfRateControl::State& AarfRateControl::stateFor(const MacAddress& receiverAddr auto it = stations.find(receiverAddress); if (it == stations.end()) { 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 it = stations.insert({receiverAddress, state}).first; - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); } return it->second; } @@ -60,7 +61,7 @@ void AarfRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuc if (!isSuccessful && state.probing) { // probing packet failed state.numberOfConsSuccTransmissions = 0; state.mode = decreaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); EV_DETAIL << "Decreased rate to " << *state.mode << endl; multiplyIncreaseThreshold(state, factor); resetTimer(state); @@ -68,7 +69,7 @@ void AarfRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuc else if (!isSuccessful && retryCount >= decreaseThreshold - 1) { // decreaseThreshold consecutive failed transmissions state.numberOfConsSuccTransmissions = 0; state.mode = decreaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); EV_DETAIL << "Decreased rate to " << *state.mode << endl; resetIncreaseThreshdold(state); resetTimer(state); @@ -79,7 +80,7 @@ void AarfRateControl::frameTransmitted(Packet *frame, int retryCount, bool isSuc if (state.numberOfConsSuccTransmissions == state.increaseThreshold) { state.numberOfConsSuccTransmissions = 0; state.mode = increaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); EV_DETAIL << "Increased rate to " << *state.mode << endl; resetTimer(state); state.probing = true; @@ -108,7 +109,7 @@ void AarfRateControl::increaseRateIfTimerIsExpired(State& state) { if (simTime() - state.timer >= interval) { state.mode = increaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); EV_DETAIL << "Increased rate to " << *state.mode << endl; resetTimer(state); } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h index 66618e489cf..56bb114b9e4 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h @@ -21,6 +21,7 @@ class INET_API AarfRateControl : public RateControlBase protected: // Per-receiver adaptive state (formerly single-instance module members). 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; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned index 17121012f64..9ec36a21ebc 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.ned @@ -29,6 +29,7 @@ simple AarfRateControl extends SimpleModule like IRateControl displayStringTextFormat = default("{numStations} stations"); @display("i=block/cogwheel"); @signal[datarateChanged]; - @statistic[datarateChanged](title="datarate"; record=vector; interpolationmode=sample-hold); + @statistic[datarateChanged](title="datarate"; record=vector; interpolationmode=sample-hold); // aggregate rate over all stations (interleaved) + @statistic[dataratePerStation](title="data rate per station"; source=demux(datarateChanged); unit=bps; record=vector; interpolationmode=sample-hold); // one vector per receiver, demultiplexed by station } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index 9b954f5b2cb..d1c9789c5ea 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -29,10 +29,11 @@ OnoeRateControl::State& OnoeRateControl::stateFor(const MacAddress& receiverAddr auto it = stations.find(receiverAddress); if (it == stations.end()) { State state; + state.address = receiverAddress; state.mode = getInitialMode(); state.timer = simTime(); // the interval starts when the station is first seen, not at t=0 it = stations.insert({receiverAddress, state}).first; - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); } return it->second; } @@ -81,7 +82,7 @@ void OnoeRateControl::computeMode(State& state) if (state.numOfSuccTransmissions > 0) { if (numOfFrameTransmitted >= 10 && state.avgRetriesPerFrame > 1) { state.mode = decreaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); EV_DETAIL << "Decreased rate to " << *state.mode << endl; state.credit = 0; } @@ -92,7 +93,7 @@ void OnoeRateControl::computeMode(State& state) if (state.credit >= 10) { state.mode = increaseRateIfPossible(state.mode); - emitDatarateChangedSignal(state.mode); + emitDatarateChangedSignal(state.address, state.mode); EV_DETAIL << "Increased rate to " << *state.mode << endl; state.credit = 0; } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index 797a6cfd402..d7558b002d4 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -21,6 +21,7 @@ class INET_API OnoeRateControl : public RateControlBase protected: // Per-receiver adaptive state (formerly single-instance module members). 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; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned index 62304f03ee8..966753c790f 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.ned @@ -23,6 +23,7 @@ simple OnoeRateControl extends SimpleModule like IRateControl displayStringTextFormat = default("{numStations} stations"); @display("i=block/cogwheel"); @signal[datarateChanged]; - @statistic[datarateChanged](title="datarate"; record=vector; interpolationmode=sample-hold); + @statistic[datarateChanged](title="datarate"; record=vector; interpolationmode=sample-hold); // aggregate rate over all stations (interleaved) + @statistic[dataratePerStation](title="data rate per station"; source=demux(datarateChanged); unit=bps; record=vector; interpolationmode=sample-hold); // one vector per receiver, demultiplexed by station } diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index c7fb4fcf0c3..352479df59a 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -9,6 +9,7 @@ #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" +#include "inet/networklayer/common/L3AddressResolver.h" namespace inet { namespace ieee80211 { @@ -46,10 +47,18 @@ const IIeee80211Mode *RateControlBase::getInitialMode() return initialRate == -1 ? modeSet->getFastestMandatoryMode() : modeSet->getMode(bps(initialRate)); } -void RateControlBase::emitDatarateChangedSignal(const IIeee80211Mode *mode) +void RateControlBase::emitDatarateChangedSignal(const MacAddress& receiver, const IIeee80211Mode *mode) { bps rate = mode->getDataMode()->getNetBitrate(); - emit(datarateChangedSignal, rate.get()); + // Emit once, tagging the value with the receiver as a named details object. The aggregate + // datarateChanged statistic ignores the details (so it is unchanged), while a demux(datarateChanged) + // statistic uses the details name to record a separate data-rate vector per station. + if (receiver.isBroadcast() || receiver.isMulticast()) + emit(datarateChangedSignal, rate.get()); + else { + cNamedObject details(L3AddressResolver().getHostNameWithMacAddress(receiver).c_str()); + emit(datarateChangedSignal, rate.get(), &details); + } } void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 743076141b1..2a61b5028fa 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -29,8 +29,11 @@ class INET_API RateControlBase : public ModeSetListener, public IRateControl virtual MacAddress getReceiverAddress(Packet *frame) const; // The mode a newly seen station starts from: the initialRate parameter, or the fastest mandatory mode. virtual const physicallayer::IIeee80211Mode *getInitialMode(); - // Emits datarateChanged with the rate of the given mode. - virtual void emitDatarateChangedSignal(const physicallayer::IIeee80211Mode *mode); + // Emits datarateChanged with the receiver as a named details object, so a demux(datarateChanged) + // result filter can record a separate data-rate vector per station. The aggregate datarateChanged + // statistic ignores the details and is therefore unchanged. Group-addressed receivers are emitted + // without details (aggregate only). + virtual void emitDatarateChangedSignal(const MacAddress& receiver, const physicallayer::IIeee80211Mode *mode); // Drops all per-station state; called by subclasses' override when the mode set changes. virtual void resetRateControl() {} diff --git a/tests/module/AarfRateControlPerReceiver_1.test b/tests/module/AarfRateControlPerReceiver_1.test new file mode 100644 index 00000000000..51f1c1c1c7a --- /dev/null +++ b/tests/module/AarfRateControlPerReceiver_1.test @@ -0,0 +1,103 @@ +%description: +Tests that AarfRateControl adapts its rate separately for each receiver, so that +feedback from one peer does not move the rate used towards another. + +One source saturates two ad-hoc peers at once: nearSink 20 m away, farSink 1200 m +away on a link poor enough that frames to it are lost and retried. The rate towards +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; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +network AarfRateControlPerReceiverTest +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + source: AdhocHost; + nearSink: AdhocHost; + farSink: AdhocHost; +} + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +network = AarfRateControlPerReceiverTest +ned-path = .;../../../../src +sim-time-limit = 2s +cmdenv-express-mode = true +**.vector-recording = false + +*.radioMedium.sameTransmissionStartTimeCheck = "ignore" +*.radioMedium.backgroundNoise.power = -100dBm +*.*.ipv4.arp.typename = "GlobalArp" +*.configurator.addStaticRoutes = false + +# ad-hoc, so no association is needed and a distant peer simply has a worse link +*.*.wlan[*].opMode = "g(erp)" +*.*.wlan[*].mgmt.typename = "Ieee80211MgmtAdhoc" +*.*.wlan[*].agent.typename = "" +*.*.wlan[*].radio.transmitter.power = 50mW + +*.*.mobility.typename = "StationaryMobility" +**.mobility.initFromDisplayString = false +*.source.mobility.initialX = 0m +*.source.mobility.initialY = 0m +*.source.mobility.initialZ = 0m +*.nearSink.mobility.initialX = 20m +*.nearSink.mobility.initialY = 0m +*.nearSink.mobility.initialZ = 0m +*.farSink.mobility.initialX = 1200m +*.farSink.mobility.initialY = 0m +*.farSink.mobility.initialZ = 0m + +# the rate control under test, adapting from the same starting rate for every peer +*.source.wlan[*].mac.dcf.rateControl.typename = "AarfRateControl" +*.source.wlan[*].mac.dcf.rateControl.initialRate = 54Mbps +*.source.wlan[*].mac.dcf.rateControl.increaseThreshold = 20 +*.source.wlan[*].mac.dcf.rateControl.decreaseThreshold = 5 +*.source.wlan[*].mac.dcf.rateSelection.dataFrameBitrate = -1bps + +# the same saturating stream to each peer +*.source.numApps = 2 +*.source.app[0].typename = "UdpBasicApp" +*.source.app[0].destAddresses = "nearSink" +*.source.app[0].destPort = 5000 +*.source.app[1].typename = "UdpBasicApp" +*.source.app[1].destAddresses = "farSink" +*.source.app[1].destPort = 5001 +*.source.app[*].messageLength = 1000B +*.source.app[*].sendInterval = 0.5ms +*.nearSink.numApps = 1 +*.nearSink.app[0].typename = "UdpSink" +*.nearSink.app[0].localPort = 5000 +*.farSink.numApps = 1 +*.farSink.app[0].typename = "UdpSink" +*.farSink.app[0].localPort = 5001 + +# the per-station rates the verdict is computed from, as a scalar per peer +*.source.wlan[*].mac.dcf.rateControl.dataratePerStation.result-recording-modes = timeavg + +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: gawk '/(near|far)Sink:dataratePerStation:timeavg/ { match($3, /^([a-zA-Z]+)Sink:/, m); r[m[1]] = $4 } /(near|far)Sink\.app\[0\] packetReceived:count/ { match($2, /\.([a-zA-Z]+)Sink\./, m); c[m[1]] = $4 } END { printf "bothServed=%s\n", (c["near"] > 0 && c["far"] > 0) ? "YES" : "NO"; printf "nearRateUndragged=%s\n", (r["near"] > 50e6) ? "YES" : "NO"; printf "farRateReduced=%s\n", (r["far"] > 0 && r["far"] < 30e6) ? "YES" : "NO"; printf "values near=%.1fMbps far=%.1fMbps nearPk=%d farPk=%d\n", r["near"]/1e6, r["far"]/1e6, c["near"], c["far"] }' results/*.sca > verdict.out +%contains: verdict.out +bothServed=YES +nearRateUndragged=YES +farRateReduced=YES +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: From 9d2ea1f4e036db78f5ec57bd5ac3e973e27b8d4e Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 6 Aug 2026 15:05:21 +0200 Subject: [PATCH 5/6] ieee80211: add per-receiver configured unicast data-frame rates dataFrameBitrate fixes one rate for the whole interface, so a scenario that needs different peers reached at different rates -- the usual setup for studying rate diversity -- had to fall back on an adaptive rate control and let the channel produce the spread indirectly, which is slow to set up and hard to hold steady. ~RateSelection and ~QosRateSelection now accept dataFrameBitratePerReceiver, a map from peer interface module path to bitrate. A unicast data frame whose receiver matches an entry is sent at that rate; everything else falls back to the existing rules. The parameter is empty by default, so the capability is inert unless configured. Resolution of the paths to MAC addresses is deferred to the first transmitted data frame rather than done in initialize(): peer MAC addresses are assigned during INITSTAGE_LINK_LAYER and the order of modules within a stage is undefined, so reading them from initialize() would be a race. By the time any data frame is sent, all init stages have completed. Unresolvable paths and rates the mode set does not support fail with an error naming the offending entry. --- .../mac/rateselection/QosRateSelection.cc | 31 +++++++++++++++++++ .../mac/rateselection/QosRateSelection.h | 13 ++++++++ .../mac/rateselection/QosRateSelection.ned | 8 +++++ .../mac/rateselection/RateSelection.cc | 30 ++++++++++++++++++ .../mac/rateselection/RateSelection.h | 13 ++++++++ .../mac/rateselection/RateSelection.ned | 8 +++++ 6 files changed, 103 insertions(+) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 961937a447c..423eb138cb3 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -9,6 +9,7 @@ #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" +#include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" namespace inet { @@ -40,6 +41,27 @@ void QosRateSelection::initialize(int stage) } } +void QosRateSelection::ensurePerReceiverModesResolved() +{ + if (perReceiverResolved) + return; + perReceiverResolved = true; + auto perReceiverBitrate = check_and_cast(par("dataFrameBitratePerReceiver").objectValue()); + for (auto& [path, value] : perReceiverBitrate->getFields()) { + auto module = findModuleByPath(path.c_str()); + if (module == nullptr) + throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); + auto networkInterface = check_and_cast(module); + try { + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; + } + catch (const cRuntimeError& e) { + throw cRuntimeError("dataFrameBitratePerReceiver: cannot use rate '%s' for receiver '%s': %s", value.str().c_str(), path.c_str(), e.what()); + } + } +} + const IIeee80211Mode *QosRateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -119,6 +141,15 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). + // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management + // frames are left to the existing rules below. + if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + ensurePerReceiverModesResolved(); + auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); + if (it != perReceiverDataFrameMode.end()) + return it->second; + } if (dynamicPtrCast(dataOrMgmtHeader) && dataFrameMode) return dataFrameMode; if (dynamicPtrCast(dataOrMgmtHeader) && mgmtFrameMode) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 167acc72b63..86a58bb95eb 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -48,11 +48,20 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen const physicallayer::IIeee80211Mode *responseCtsFrameMode = nullptr; const physicallayer::IIeee80211Mode *responseBlockAckFrameMode = nullptr; + // per-receiver unicast data-frame modes, resolved lazily from dataFrameBitratePerReceiver + std::map perReceiverDataFrameMode; + bool perReceiverResolved = false; + protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer + // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module + // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. + virtual void ensurePerReceiverModesResolved(); + virtual const physicallayer::IIeee80211Mode *getMode(Packet *packet, const Ptr& header); virtual const physicallayer::IIeee80211Mode *computeControlFrameMode(const Ptr& header, TxopProcedure *txopProcedure); virtual const physicallayer::IIeee80211Mode *computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader); @@ -60,6 +69,10 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual bool isControlResponseFrame(const Ptr& header, TxopProcedure *txopProcedure); public: + // Per-receiver configured data-frame modes (resolved lazily from dataFrameBitratePerReceiver); + // used by the IEEE 802.11 rate visualizer to show configured rates before any traffic. + const std::map& getPerReceiverDataFrameModes() { ensurePerReceiverModesResolved(); return perReceiverDataFrameMode; } + // A control response frame is a control frame that is transmitted as a response to the reception of a frame a SIFS // time after the PPDU containing the frame that elicited the response, e.g. a CTS in response to an RTS // reception, an ACK in response to a DATA reception, a BlockAck in response to a BlockAckReq reception. In diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index b3b96441781..bd1638ff827 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -34,6 +34,14 @@ simple QosRateSelection extends SimpleModule double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Unspecified by default int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. + // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). + // A unicast data frame whose receiver matches a key is transmitted at that rate; + // unmatched receivers fall back to dataFrameBitrate / the rate-control module. The + // bandwidth and spatial-stream count are taken from dataFrameBandwidth / + // dataFrameNumSpatialStreams. Empty by default (capability inert). + object dataFrameBitratePerReceiver = default({}); + double mgmtFrameBitrate @unit(bps) = default(-1bps); // Fastest double controlFrameBitrate @unit(bps) = default(-1bps); @display("i=block/cogwheel"); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 8a7cc6a5dcd..e2ef6ab06c0 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -60,6 +60,27 @@ void RateSelection::initialize(int stage) } } +void RateSelection::ensurePerReceiverModesResolved() +{ + if (perReceiverResolved) + return; + perReceiverResolved = true; + auto perReceiverBitrate = check_and_cast(par("dataFrameBitratePerReceiver").objectValue()); + for (auto& [path, value] : perReceiverBitrate->getFields()) { + auto module = findModuleByPath(path.c_str()); + if (module == nullptr) + throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); + auto networkInterface = check_and_cast(module); + try { + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; + } + catch (const cRuntimeError& e) { + throw cRuntimeError("dataFrameBitratePerReceiver: cannot use rate '%s' for receiver '%s': %s", value.str().c_str(), path.c_str(), e.what()); + } + } +} + const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -114,6 +135,15 @@ const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, // const IIeee80211Mode *RateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). + // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management + // frames are left to the existing rules below. + if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + ensurePerReceiverModesResolved(); + auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); + if (it != perReceiverDataFrameMode.end()) + return it->second; + } if (dataOrMgmtHeader->getReceiverAddress().isMulticast() && multicastFrameMode) return multicastFrameMode; if (dynamicPtrCast(dataOrMgmtHeader) && dataFrameMode) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 98d525812f6..2146b0b9960 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -48,16 +48,29 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi const physicallayer::IIeee80211Mode *responseAckFrameMode = nullptr; const physicallayer::IIeee80211Mode *responseCtsFrameMode = nullptr; + // per-receiver unicast data-frame modes, resolved lazily from dataFrameBitratePerReceiver + std::map perReceiverDataFrameMode; + bool perReceiverResolved = false; + protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer + // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module + // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. + virtual void ensurePerReceiverModesResolved(); + virtual const physicallayer::IIeee80211Mode *getMode(Packet *packet, const Ptr& header); virtual const physicallayer::IIeee80211Mode *computeControlFrameMode(const Ptr& header); virtual const physicallayer::IIeee80211Mode *computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader); public: + // Per-receiver configured data-frame modes (resolved lazily from dataFrameBitratePerReceiver); + // used by the IEEE 802.11 rate visualizer to show configured rates before any traffic. + const std::map& getPerReceiverDataFrameModes() { ensurePerReceiverModesResolved(); return perReceiverDataFrameMode; } + static void setFrameMode(Packet *packet, const Ptr& header, const physicallayer::IIeee80211Mode *mode); // A control response frame is a control frame that is transmitted as a response to the reception of a frame a SIFS diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index b60bba95a51..99d92b535ec 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -29,6 +29,14 @@ simple RateSelection extends SimpleModule like IRateSelection double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Unspecified by default int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. + // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). + // A unicast data frame whose receiver matches a key is transmitted at that rate; + // unmatched receivers fall back to dataFrameBitrate / the rate-control module. The + // bandwidth and spatial-stream count are taken from dataFrameBandwidth / + // dataFrameNumSpatialStreams. Empty by default (capability inert). + object dataFrameBitratePerReceiver = default({}); + double mgmtFrameBitrate @unit(bps) = default(-1bps); // Fastest double controlFrameBitrate @unit(bps) = default(-1bps); @display("i=block/cogwheel"); From 42d01743bff85bcddb99622d5090b78d2d1a56d3 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 12 Aug 2026 17:22:06 +0200 Subject: [PATCH 6/6] ieee80211: tag datarateSelected with the receiving station datarateSelected reports the rate chosen for every transmitted frame, so unlike datarateChanged it also covers interface-wide fixed rates and per-receiver configured rates, not just adaptive rate control. It was emitted with the packet as details, which identifies the frame but not the peer, so the values of several stations arrived interleaved with no way to separate them. ~IRateSelection::emitDatarateSelected() now emits unicast data frames with the receiver's station label as a named details object, so a demux(datarateSelected) result filter can key a per-station series on it. The condition matches the one under which ~RateSelection applies a per-receiver configured rate, so the label always names the station whose rate is being reported. Control, management and group-addressed frames have no per-station rate and are emitted without details: they still reach the aggregate statistic. Both coordination functions emit through the shared helper rather than each formatting the signal themselves. Unlike datarateChanged, this signal fires for every transmitted frame, and resolving a station label sweeps the network's interface tables -- so the label is only computed when the signal has listeners: with result recording off, transmitting costs nothing extra. --- .../ieee80211/mac/contract/IRateSelection.cc | 18 +++++++++++++++++- .../ieee80211/mac/contract/IRateSelection.h | 10 ++++++++++ .../ieee80211/mac/coordinationfunction/Dcf.cc | 4 ++-- .../ieee80211/mac/coordinationfunction/Hcf.cc | 4 ++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.cc b/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.cc index 8675a9f31d1..a5401b9bef8 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.cc @@ -7,11 +7,27 @@ #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" +#include "inet/networklayer/common/L3AddressResolver.h" + namespace inet { namespace ieee80211 { +using namespace inet::physicallayer; + simsignal_t IRateSelection::datarateSelectedSignal = cComponent::registerSignal("datarateSelected"); +void IRateSelection::emitDatarateSelected(cComponent *emitter, const Ptr& header, const IIeee80211Mode *mode) +{ + double rate = mode->getDataMode()->getNetBitrate().get(); + auto dataHeader = dynamicPtrCast(header); + // naming the station sweeps the network, so skip it if nothing listens anyway + if (dataHeader != nullptr && !dataHeader->getReceiverAddress().isMulticast() && emitter->mayHaveListeners(datarateSelectedSignal)) { + cNamedObject details(L3AddressResolver().getHostNameWithMacAddress(dataHeader->getReceiverAddress()).c_str()); + emitter->emit(datarateSelectedSignal, rate, &details); + } + else + emitter->emit(datarateSelectedSignal, rate); +} + } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.h b/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.h index 716e45948e6..54e605a652d 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRateSelection.h @@ -26,6 +26,16 @@ class INET_API IRateSelection public: static simsignal_t datarateSelectedSignal; + // Emits datarateSelected on behalf of a coordination function. Unicast data frames are tagged + // with the name of the receiving station as a named details object, so that a + // demux(datarateSelected) result filter or a statistic visualizer can key a separate + // per-station series on it; this mirrors the condition under which ~RateSelection applies a + // per-receiver configured rate, so the details always name the station whose rate is + // reported. Control, management and group-addressed frames carry no per-station data rate and + // are emitted without details: the aggregate datarateSelected statistic still records them, a + // bar chart ignores them. + static void emitDatarateSelected(cComponent *emitter, const Ptr& header, const physicallayer::IIeee80211Mode *mode); + public: virtual ~IRateSelection() {} diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 0da41104ace..0fcbe490543 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -102,7 +102,7 @@ void Dcf::transmitControlResponseFrame(Packet *responsePacket, const PtrgetDataMode()->getNetBitrate().get(), responsePacket); + IRateSelection::emitDatarateSelected(this, responseHeader, responseMode); EV_DEBUG << "Datarate for " << responsePacket->getName() << " is set to " << responseMode->getDataMode()->getNetBitrate() << ".\n"; tx->transmitFrame(responsePacket, responseHeader, modeSet->getSifsTime(), this); delete responsePacket; @@ -166,7 +166,7 @@ void Dcf::transmitFrame(Packet *packet, simtime_t ifs) const auto& header = packet->peekAtFront(); auto mode = rateSelection->computeMode(packet, header); RateSelection::setFrameMode(packet, header, mode); - emit(IRateSelection::datarateSelectedSignal, mode->getDataMode()->getNetBitrate().get(), packet); + IRateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; auto pendingPacket = channelAccess->getInProgressFrames()->getPendingFrameFor(packet); auto duration = originatorProtectionMechanism->computeDurationField(packet, header, pendingPacket, pendingPacket == nullptr ? nullptr : pendingPacket->peekAtFront()); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b3d11467343..c320b00cc88 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -668,7 +668,7 @@ void Hcf::transmitFrame(Packet *packet, simtime_t ifs) } auto mode = rateSelection->computeMode(packet, header, txop); setFrameMode(packet, header, mode); - emit(IRateSelection::datarateSelectedSignal, mode->getDataMode()->getNetBitrate().get(), packet); + IRateSelection::emitDatarateSelected(this, header, mode); EV_DEBUG << "Datarate for " << packet->getName() << " is set to " << mode->getDataMode()->getNetBitrate() << ".\n"; if (txop->getProtectionMechanism() == TxopProcedure::ProtectionMechanism::SINGLE_PROTECTION) { auto pendingPacket = channelOwner->getInProgressFrames()->getPendingFrameFor(packet); @@ -703,7 +703,7 @@ void Hcf::transmitControlResponseFrame(Packet *responsePacket, const PtrgetDataMode()->getNetBitrate().get(), responsePacket); + IRateSelection::emitDatarateSelected(this, responseHeader, responseMode); EV_DEBUG << "Datarate for " << responsePacket->getName() << " is set to " << responseMode->getDataMode()->getNetBitrate() << ".\n"; tx->transmitFrame(responsePacket, responseHeader, modeSet->getSifsTime(), this); delete responsePacket;