Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Copyright (C) 2026 OpenSim Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//


import inet.common.INETDefs;
import inet.linklayer.common.MacAddress;

namespace inet::ieee80211;

//
// Carries the on-air time actually consumed by a transmitted frame, together with
// the receiver it was sent to. Emitted by the coordination function (~Dcf, ~Hcf)
// as the object value of the `frameTransmittedAirtime` signal, once per on-air
// transmission (so retries are counted), and consumed by ~AirtimeFairnessQueue to
// charge the receiver's airtime deficit a-posteriori.
//
class Ieee80211TransmittedAirtime extends cObject
{
MacAddress receiverAddress;
simtime_t airtime;
}
17 changes: 17 additions & 0 deletions src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "inet/common/ModuleAccess.h"
#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h"
#include "inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime_m.h"
#include "inet/linklayer/ieee80211/mac/framesequence/DcfFs.h"
#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h"
#include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h"
Expand All @@ -18,6 +19,8 @@ namespace ieee80211 {

using namespace inet::physicallayer;

simsignal_t Dcf::frameTransmittedAirtimeSignal = cComponent::registerSignal("frameTransmittedAirtime");

Define_Module(Dcf);

void Dcf::initialize(int stage)
Expand Down Expand Up @@ -239,6 +242,20 @@ void Dcf::transmissionComplete(Packet *packet, const Ptr<const Ieee80211MacHeade
{
Enter_Method("transmissionComplete");
if (frameSequenceHandler->isSequenceRunning()) {
// Account the on-air time of the just-transmitted unicast data/mgmt frame to its
// receiver, a-posteriori (so each retransmission is charged), for airtime-fair
// transmit scheduling in AirtimeFairnessQueue. Control frames and multicast are
// excluded; the duration is exact (computed from the selected mode and length).
if (auto dataOrMgmtHeader = dynamicPtrCast<const Ieee80211DataOrMgmtHeader>(header)) {
auto receiver = dataOrMgmtHeader->getReceiverAddress();
if (!receiver.isMulticast()) {
auto mode = rateSelection->computeMode(packet, header);
Ieee80211TransmittedAirtime info;
info.setReceiverAddress(receiver);
info.setAirtime(mode->getDuration(packet->getDataLength()));
emit(frameTransmittedAirtimeSignal, &info);
}
}
frameSequenceHandler->transmissionComplete();
}
else
Expand Down
3 changes: 3 additions & 0 deletions src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class Ieee80211Mac;
*/
class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler::ICallback, public IChannelAccess::ICallback, public ITx::ICallback, public IProcedureCallback, public ModeSetListener
{
public:
static simsignal_t frameTransmittedAirtimeSignal;

protected:
Ieee80211Mac *mac = nullptr;
IRateControl *dataAndMgmtRateControl = nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module Dcf extends Module like IDcf
@signal[frameSequenceStarted];
@signal[frameSequenceFinished];
@signal[datarateSelected](type=double);
@signal[frameTransmittedAirtime](type=inet::ieee80211::Ieee80211TransmittedAirtime); // receiver + on-air time, once per transmitted frame
@statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none);
@statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none);
@statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none);
Expand Down
16 changes: 16 additions & 0 deletions src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h"
#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h"
#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h"
#include "inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime_m.h"
#include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h"
#include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h"
#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h"
Expand All @@ -24,6 +25,7 @@ using namespace inet::physicallayer;
simsignal_t Hcf::edcaCollisionDetectedSignal = cComponent::registerSignal("edcaCollisionDetected");
simsignal_t Hcf::blockAckAgreementAddedSignal = cComponent::registerSignal("blockAckAgreementAdded");
simsignal_t Hcf::blockAckAgreementDeletedSignal = cComponent::registerSignal("blockAckAgreementDeleted");
simsignal_t Hcf::frameTransmittedAirtimeSignal = cComponent::registerSignal("frameTransmittedAirtime");

Define_Module(Hcf);

Expand Down Expand Up @@ -369,6 +371,20 @@ void Hcf::transmissionComplete(Packet *packet, const Ptr<const Ieee80211MacHeade
Enter_Method("transmissionComplete");
auto edcaf = edca->getChannelOwner();
if (edcaf) {
// Account the on-air time of the just-transmitted unicast data/mgmt frame to its
// receiver, a-posteriori (so each retransmission is charged), for airtime-fair
// transmit scheduling in AirtimeFairnessQueue. Control frames and multicast are
// excluded; the duration is exact (computed from the selected mode and length).
if (auto dataOrMgmtHeader = dynamicPtrCast<const Ieee80211DataOrMgmtHeader>(header)) {
auto receiver = dataOrMgmtHeader->getReceiverAddress();
if (!receiver.isMulticast()) {
auto mode = rateSelection->computeMode(packet, header, edcaf->getTxopProcedure());
Ieee80211TransmittedAirtime info;
info.setReceiverAddress(receiver);
info.setAirtime(mode->getDuration(packet->getDataLength()));
emit(frameTransmittedAirtimeSignal, &info);
}
}
frameSequenceHandler->transmissionComplete();
}
else if (hcca->isOwning())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler:
static simsignal_t edcaCollisionDetectedSignal;
static simsignal_t blockAckAgreementAddedSignal;
static simsignal_t blockAckAgreementDeletedSignal;
static simsignal_t frameTransmittedAirtimeSignal;

protected:
Ieee80211Mac *mac = nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module Hcf extends Module like IHcf
@signal[datarateSelected](type=double);
@signal[blockAckAgreementAdded];
@signal[blockAckAgreementDeleted];
@signal[frameTransmittedAirtime](type=inet::ieee80211::Ieee80211TransmittedAirtime); // receiver + on-air time, once per transmitted frame
@statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none);
@statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none);
@statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Copyright (C) 2026 OpenSim Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//


#include "inet/linklayer/ieee80211/mac/queue/AirtimeFairnessCompoundQueue.h"

#include "inet/common/InitStages.h"

namespace inet {
namespace ieee80211 {

Define_Module(AirtimeFairnessCompoundQueue);

void AirtimeFairnessCompoundQueue::initialize(int stage)
{
CompoundPacketQueueBase::initialize(stage);
if (stage == INITSTAGE_LOCAL) {
// Number of per-station branches created so far (= stations served). Watched on this
// module so the display string can use {numStations}; a cross-submodule reference like
// {classifier.numStations} would make ModuleMixin call getModuleByPath(), which throws
// in OMNeT++ 6 and breaks every Qtenv refreshDisplay (Cmdenv never evaluates it).
WATCH_EXPR("numStations", (int)getSubmoduleVectorSize("branch"));
}
}

} // namespace ieee80211
} // namespace inet
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Copyright (C) 2026 OpenSim Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//


#ifndef __INET_AIRTIMEFAIRNESSCOMPOUNDQUEUE_H
#define __INET_AIRTIMEFAIRNESSCOMPOUNDQUEUE_H

#include "inet/queueing/queue/CompoundPacketQueueBase.h"

namespace inet {
namespace ieee80211 {

/**
* The compound-module class behind the airtime-fairness compound queue. It only publishes the
* number of per-station branches created so far as a `numStations` watch, so the queue's
* display string can refer to it as {numStations}.
*
* See the corresponding NED file for the submodule structure.
*
* @see DynamicClassifier, AirtimeFairnessGate, AirtimeFairnessScheduler,
* Ieee80211LongestFlowDropper
*/
class INET_API AirtimeFairnessCompoundQueue : public queueing::CompoundPacketQueueBase
{
protected:
virtual void initialize(int stage) override;
};

} // namespace ieee80211
} // namespace inet

#endif
145 changes: 145 additions & 0 deletions src/inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//
// Copyright (C) 2026 OpenSim Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
//


#include "inet/linklayer/ieee80211/mac/queue/AirtimeFairnessGate.h"

#include "inet/common/ModuleAccess.h"
#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h"
#include "inet/linklayer/ieee80211/mac/common/Ieee80211TransmittedAirtime_m.h"
#include "inet/networklayer/common/NetworkInterface.h"

namespace inet {
namespace ieee80211 {

Define_Module(AirtimeFairnessGate);

simsignal_t AirtimeFairnessGate::deficitChangedSignal = cComponent::registerSignal("deficitChanged");

void AirtimeFairnessGate::initialize(int stage)
{
PacketGateBase::initialize(stage);
if (stage == INITSTAGE_LOCAL) {
quantum = par("quantum");
weight = par("weight");
fairnessEnabled = par("fairnessEnabled");
if (quantum <= SIMTIME_ZERO)
throw cRuntimeError("The quantum parameter must be positive");
if (weight <= 0)
throw cRuntimeError("The weight parameter must be positive");
frameTransmittedAirtimeSignal = registerSignal("frameTransmittedAirtime");
isOpen_ = isEligible(); // deficit starts at zero -> eligible -> open
WATCH(stationAddress);
WATCH(deficit);
}
else if (stage == INITSTAGE_LINK_LAYER)
// The coordination function (Dcf/Hcf) emits frameTransmittedAirtime from within the
// containing network interface; subscribing there scopes the accounting to this
// interface's transmissions (the frame's receiver disambiguates the stations).
getContainingNicModule(this)->subscribe(frameTransmittedAirtimeSignal, this);
}

void AirtimeFairnessGate::processPacket(Packet *packet)
{
PacketGateBase::processPacket(packet);
if (stationAddress.isUnspecified()) {
const auto& header = packet->peekAtFront<Ieee80211MacHeader>();
stationAddress = header->getReceiverAddress();
}
}

void AirtimeFairnessGate::setDeficit(simtime_t value)
{
if (deficit != value) {
deficit = value;
emit(deficitChangedSignal, deficit.dbl());
}
}

void AirtimeFairnessGate::addQuantum()
{
Enter_Method("addQuantum");
setDeficit(deficit + quantum * weight);
updateGateState();
}

void AirtimeFairnessGate::updateGateState()
{
bool eligible = isEligible();
if (eligible && isClosed())
open();
else if (!eligible && isOpen())
close();
}

bool AirtimeFairnessGate::isBacklogged() const
{
return provider != nullptr && provider.canPullSomePacket();
}

Packet *AirtimeFairnessGate::peekPacket() const
{
return provider != nullptr ? provider.canPullPacket() : nullptr;
}

void AirtimeFairnessGate::handleCanPullPacketChanged(const cGate *gate)
{
Enter_Method("handleCanPullPacketChanged");
// Forward even while the gate is closed: a station can become backlogged while its
// deficit is negative (gate shut), and the scheduler must still learn about it so it
// can top the station up and eventually serve it. PacketGateBase would swallow this
// notification while the gate is closed.
if (collector != nullptr)
collector.handleCanPullPacketChanged();
}

int AirtimeFairnessGate::getNumPackets() const
{
return queueing::PacketFlowBase::getNumPackets();
}

b AirtimeFairnessGate::getTotalLength() const
{
return queueing::PacketFlowBase::getTotalLength();
}

Packet *AirtimeFairnessGate::getPacket(int index) const
{
return queueing::PacketFlowBase::getPacket(index);
}

bool AirtimeFairnessGate::isEmpty() const
{
return queueing::PacketFlowBase::isEmpty();
}

void AirtimeFairnessGate::removePacket(Packet *packet)
{
queueing::PacketFlowBase::removePacket(packet);
}

void AirtimeFairnessGate::removeAllPackets()
{
queueing::PacketFlowBase::removeAllPackets();
}

void AirtimeFairnessGate::receiveSignal(cComponent *source, simsignal_t signalID, cObject *object, cObject *details)
{
if (signalID == frameTransmittedAirtimeSignal) {
Enter_Method("%s", cComponent::getSignalName(signalID));
auto info = check_and_cast<Ieee80211TransmittedAirtime *>(object);
// charge only frames sent to the station this gate serves
if (!stationAddress.isUnspecified() && info->getReceiverAddress() == stationAddress) {
setDeficit(deficit - info->getAirtime());
EV_DEBUG << "Charged " << info->getAirtime() << " airtime to " << stationAddress
<< ", deficit now " << deficit << EV_ENDL;
updateGateState(); // may close the gate if the deficit went negative
}
}
}

} // namespace ieee80211
} // namespace inet
Loading