From 03ab6dd664e8873eed0bb37880360beab92567b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 16 Sep 2026 00:23:07 +0200 Subject: [PATCH 1/3] ieee80211: fix: carry the target BSSID into STA management headers STA management frames built before association omitted Address3. Carry the procedure-selected BSSID in a typed request tag and honor it at MAC header construction, preserving the AP fallback and keeping receiver address distinct from BSSID. A real STA scan/authentication/association test checks management Address3 at the MAC boundary, including wildcard probes. Change: src.ieee80211.management | behavior.change.fix | test | wifi-audit --- .../ieee80211/mac/Ieee80211BssidReq.msg | 16 +++++ .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 6 +- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 3 + tests/module/Ieee80211StaMgmtBssid_1.test | 69 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/inet/linklayer/ieee80211/mac/Ieee80211BssidReq.msg create mode 100644 tests/module/Ieee80211StaMgmtBssid_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211BssidReq.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211BssidReq.msg new file mode 100644 index 00000000000..ee4094c13ee --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211BssidReq.msg @@ -0,0 +1,16 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; +import inet.linklayer.common.MacAddress; + +namespace inet; + +// The management procedure's BSSID for Address3 of an outgoing management frame. +// It is independent of the receiver address and survives queuing/state changes. +class Ieee80211BssidReq extends TagBase +{ + MacAddress bssid; +} diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..8528a05aeef 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -6,6 +6,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211BssidReq_m.h" #include @@ -164,7 +165,10 @@ void Ieee80211Mac::handleMgmtPacket(Packet *packet) const auto& header = makeShared(); header->setType((Ieee80211FrameType)packet->getTag()->getSubtype()); header->setReceiverAddress(packet->getTag()->getDestAddress()); - if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) + // IEEE Std 802.11-2024, 9.3.3.1: management supplies its intended BSSID. + if (auto bssid = packet->findTag()) + header->setAddress3(bssid->getBssid()); + else if (mib->mode == Ieee80211Mib::INFRASTRUCTURE && mib->bssStationData.stationType == Ieee80211Mib::ACCESS_POINT) header->setAddress3(mib->bssData.bssid); packet->insertAtFront(header); packet->insertAtBack(makeShared()); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 859a5c108ea..c102893474d 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -14,6 +14,7 @@ #include "inet/common/packet/Message.h" #include "inet/linklayer/common/MacAddressTag_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211SubtypeTag_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211BssidReq_m.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/RadioControlInfo_m.h" @@ -319,6 +320,8 @@ void Ieee80211MgmtSta::sendManagementFrame(const char *name, const PtraddTag()->setDestAddress(address); + // IEEE Std 802.11-2024, 9.3.3.1: use the target AP, including before association. + packet->addTag()->setBssid(address); packet->addTag()->setSubtype(subtype); packet->insertAtBack(body); sendDown(packet); diff --git a/tests/module/Ieee80211StaMgmtBssid_1.test b/tests/module/Ieee80211StaMgmtBssid_1.test new file mode 100644 index 00000000000..0b9e418cb1c --- /dev/null +++ b/tests/module/Ieee80211StaMgmtBssid_1.test @@ -0,0 +1,69 @@ +%description: +Infrastructure STA management packets retain the target BSSID at the real MAC +header construction boundary, including wildcard scans before association. + +%file: TestStaMgmtBssid.cc +#include +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +namespace inet { namespace ieee80211 { +class TestStaMgmtBssid : public Ieee80211Mac +{ + protected: + int probes = 0, authentications = 0, associations = 0; + virtual void processUpperFrame(Packet *packet, const Ptr& header) override { + if (dynamicPtrCast(header)) { + ASSERT(header->getAddress3() == header->getReceiverAddress()); + if (header->getType() == ST_PROBEREQUEST) probes++; + if (header->getType() == ST_AUTHENTICATION) authentications++; + if (header->getType() == ST_ASSOCIATIONREQUEST) associations++; + } + Ieee80211Mac::processUpperFrame(packet, header); + } + virtual void finish() override { + ASSERT(probes > 0 && authentications > 0 && associations > 0); + std::cout << "STA scan, authentication and association headers carry their target BSSID.\n"; + } +}; +Define_Module(TestStaMgmtBssid); +}} + +%file: test.ned +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.node.wireless.AccessPoint; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +module TestStaMgmtBssid extends Ieee80211Mac { + parameters: + @class(::inet::ieee80211::TestStaMgmtBssid); +} +network StaMgmtBssidNetwork { + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + sta: WirelessHost; +} + +%inifile: omnetpp.ini +[General] +network = StaMgmtBssidNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1s +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +*.sta.wlan[0].mac.typename = "TestStaMgmtBssid" +*.ap.wlan[0].mgmt.ssid = "test" +*.sta.wlan[0].agent.defaultSsid = "test" +*.sta.wlan[0].agent.channelsToScan = "0" +**.opMode = "g(erp)" +**.bitrate = 6Mbps +**.mobility.initFromDisplayString = false +*.ap.mobility.initialX = 0m +*.sta.mobility.initialX = 10m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m + +%contains: stdout +STA scan, authentication and association headers carry their target BSSID. From 9ef958f2177b60e43db77245b367928efae24227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 16 Sep 2026 00:23:07 +0200 Subject: [PATCH 2/3] ieee80211: fix: start scan dwell after radio and probe completion Active dwell could expire before the queued Probe Request finished, and a passive retune parked during reception could remain pending after the medium became idle. Wait for the matching radio channel event, correlate probe completion through its management transaction, and retry deferred configuration only outside active exchanges and pending transmissions. Observe busy indications on the scanning radio instead of the entire host. Clear scan listeners and retry state on teardown. DCF and HCF regressions verify probe completion before minimum dwell and exact passive dwell after the deferred retune applies. The separately proposed old-channel active-probe ordering was not reproduced. Change: src.ieee80211.scanning | behavior.change.fix | test | wifi-audit --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 40 ++- .../linklayer/ieee80211/mac/Ieee80211Mac.h | 3 + src/inet/linklayer/ieee80211/mac/Tx.h | 1 + .../ieee80211/mac/contention/Contention.cc | 3 + .../linklayer/ieee80211/mac/contract/ITx.h | 3 + .../ieee80211/mac/coordinationfunction/Dcf.h | 1 + .../ieee80211/mac/coordinationfunction/Hcf.h | 1 + .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 137 +++++++--- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 11 +- .../ieee80211/mgmt/Ieee80211MgmtSta.ned | 1 + .../module/Ieee80211ScanCompletionQos_1.test | 240 ++++++++++++++++++ tests/module/Ieee80211ScanCompletion_1.test | 239 +++++++++++++++++ 12 files changed, 640 insertions(+), 40 deletions(-) create mode 100644 tests/module/Ieee80211ScanCompletionQos_1.test create mode 100644 tests/module/Ieee80211ScanCompletion_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index 8528a05aeef..ec288f5587a 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -47,8 +47,8 @@ Ieee80211Mac::Ieee80211Mac() Ieee80211Mac::~Ieee80211Mac() { - if (pendingRadioConfigMsg) - delete pendingRadioConfigMsg; + cancelAndDelete(radioConfigRetry); + delete pendingRadioConfigMsg; } void Ieee80211Mac::initialize(int stage) @@ -157,9 +157,37 @@ void Ieee80211Mac::handleMessageWhenUp(cMessage *message) void Ieee80211Mac::handleSelfMessage(cMessage *msg) { - ASSERT(false); + if (msg == radioConfigRetry) { + // Retry after delivered receptions and exchange callbacks have reserved any SIFS response. + if (pendingRadioConfigMsg && rx->isMediumFree() && !tx->isTransmissionPending() && + !(mib->qos ? hcf->isFrameSequenceRunning() : dcf->isFrameSequenceRunning())) + sendDownPendingRadioConfigMsg(); + } + else + ASSERT(false); +} + +void Ieee80211Mac::scheduleRadioConfigRetry() +{ + Enter_Method("scheduleRadioConfigRetry"); + if (pendingRadioConfigMsg) { + if (!radioConfigRetry) + radioConfigRetry = new cMessage("radioConfigRetry"); + if (!radioConfigRetry->isScheduled()) + scheduleAt(simTime(), radioConfigRetry); + } } + +void Ieee80211Mac::clearPendingRadioConfig() +{ + if (radioConfigRetry) + cancelEvent(radioConfigRetry); + delete pendingRadioConfigMsg; + pendingRadioConfigMsg = nullptr; +} + + void Ieee80211Mac::handleMgmtPacket(Packet *packet) { const auto& header = makeShared(); @@ -243,9 +271,9 @@ void Ieee80211Mac::handleUpperCommand(cMessage *msg) sendDown(msg); } else { - // TODO waiting potentially indefinitely?! wtf?! EV_DEBUG << "Delaying " << msg->getName() << " until next IDLE or DEFER state\n"; pendingRadioConfigMsg = msg; + scheduleRadioConfigRetry(); } } else { @@ -344,6 +372,8 @@ void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, intva configureRadioMode(IRadio::RADIO_MODE_RECEIVER); // FIXME this is in a very wrong place!!! should be done explicitly from coordination function! } rx->transmissionStateChanged(transmissionState); + if (transmissionFinished) + scheduleRadioConfigRetry(); } else if (signalID == IRadio::receivedSignalPartChangedSignal) { rx->receivedSignalPartChanged(static_cast(value)); @@ -432,11 +462,13 @@ void Ieee80211Mac::handleStartOperation(LifecycleOperation *operation) // FIXME void Ieee80211Mac::handleStopOperation(LifecycleOperation *operation) { + clearPendingRadioConfig(); } // FIXME void Ieee80211Mac::handleCrashOperation(LifecycleOperation *operation) { + clearPendingRadioConfig(); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 1114deaed00..39958f6494c 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -60,6 +60,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase // The last change channel message received and not yet sent to the physical layer, or nullptr. cMessage *pendingRadioConfigMsg = nullptr; + cMessage *radioConfigRetry = nullptr; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } @@ -79,6 +80,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase /** @brief Handle timer self messages */ virtual void handleSelfMessage(cMessage *msg) override; + void clearPendingRadioConfig(); /** @brief Handle packets from management */ virtual void handleMgmtPacket(Packet *packet); @@ -106,6 +108,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual void sendUpFrame(Packet *frame); virtual void sendDownFrame(Packet *frame); virtual void sendDownPendingRadioConfigMsg(); + virtual void scheduleRadioConfigRetry(); virtual void processUpperFrame(Packet *packet, const Ptr& header); virtual void processLowerFrame(Packet *packet, const Ptr& header); diff --git a/src/inet/linklayer/ieee80211/mac/Tx.h b/src/inet/linklayer/ieee80211/mac/Tx.h index ae4c37308b6..a5f1cf0a063 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.h +++ b/src/inet/linklayer/ieee80211/mac/Tx.h @@ -42,6 +42,7 @@ class INET_API Tx : public SimpleModule, public ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ITx::ICallback *txCallback) override; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *txCallback) override; virtual void radioTransmissionFinished() override; + virtual bool isTransmissionPending() const override { return frame != nullptr; } }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contention/Contention.cc b/src/inet/linklayer/ieee80211/mac/contention/Contention.cc index 4a1471e28f1..e205e2118db 100644 --- a/src/inet/linklayer/ieee80211/mac/contention/Contention.cc +++ b/src/inet/linklayer/ieee80211/mac/contention/Contention.cc @@ -160,6 +160,9 @@ void Contention::mediumStateChanged(bool mediumFree) this->mediumFree = mediumFree; lastChannelBusyTime = simTime(); handleWithFSM(MEDIUM_STATE_CHANGED); + // An idle contender otherwise has no transition that applies a deferred channel change. + if (mediumFree) + mac->scheduleRadioConfigRetry(); } void Contention::handleMessage(cMessage *msg) diff --git a/src/inet/linklayer/ieee80211/mac/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index 090969da65c..d0adb83061b 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/ITx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/ITx.h @@ -34,6 +34,9 @@ class INET_API ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ICallback *callback) = 0; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ICallback *callback) = 0; virtual void radioTransmissionFinished() = 0; + + // Includes a frame waiting for its inter-frame space, as well as on-air transmission. + virtual bool isTransmissionPending() const = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 51ce7172954..6d7de852cb8 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -123,6 +123,7 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Dcf(); + virtual bool isFrameSequenceRunning() const { return frameSequenceHandler->isSequenceRunning(); } // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 72f7af70fc4..f34ea1277f0 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -164,6 +164,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Hcf(); + virtual bool isFrameSequenceRunning() const { return frameSequenceHandler->isSequenceRunning(); } // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index c102893474d..1f1b4bf4d65 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -14,6 +14,9 @@ #include "inet/common/packet/Message.h" #include "inet/linklayer/common/MacAddressTag_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211SubtypeTag_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211BssidReq_m.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadioMedium.h" @@ -37,6 +40,7 @@ Define_Module(Ieee80211MgmtSta); Register_Class(Ieee80211MgmtSta::HtNegotiationFailure); simsignal_t Ieee80211MgmtSta::htNegotiationFailedSignal = cComponent::registerSignal("htNegotiationFailed"); +static const simsignal_t scanRadioChannelChangedSignal = cComponent::registerSignal("radioChannelChanged"); // message kind values for timers #define MK_AUTH_TIMEOUT 1 @@ -45,6 +49,7 @@ simsignal_t Ieee80211MgmtSta::htNegotiationFailedSignal = cComponent::registerSi #define MK_SCAN_MINCHANNELTIME 4 #define MK_SCAN_MAXCHANNELTIME 5 #define MK_BEACON_TIMEOUT 6 +#define MK_SCAN_NEXTCHANNEL 7 #define MAX_BEACONS_MISSED 3.5 // beacon lost timeout, in beacon intervals (doesn't need to be integer) @@ -160,7 +165,7 @@ void Ieee80211MgmtSta::initialize(int stage) assocTimeoutMsg = nullptr; numChannels = par("numChannels"); - host = getContainingNode(this); + scanningRadio = getModuleFromPar(par("radioModule"), this); WATCH(isScanning); @@ -199,7 +204,7 @@ void Ieee80211MgmtSta::handleTimer(cMessage *msg) else sendAssociationConfirm(ap, PRC_TIMEOUT); } - else if (msg->getKind() == MK_SCAN_MAXCHANNELTIME) { + else if (msg->getKind() == MK_SCAN_MAXCHANNELTIME || msg->getKind() == MK_SCAN_NEXTCHANNEL) { ASSERT(msg == scanTimer); scanTimer = nullptr; // go to next channel during scanning @@ -213,10 +218,11 @@ void Ieee80211MgmtSta::handleTimer(cMessage *msg) scanTimer = nullptr; // Active Scan: send a probe request, then wait for minChannelTime (11.1.3.2.2) delete msg; - sendProbeRequest(); - ASSERT(scanTimer == nullptr); - scanTimer = new cMessage("minChannelTime", MK_SCAN_MINCHANNELTIME); - scheduleAfter(scanning.minChannelTime, scanTimer); // TODO actually, we should start waiting after ProbeReq actually got transmitted + scanPhase = SCAN_WAIT_PROBE; + if (++nextProbeTransactionId == 0) + throw cRuntimeError("Probe transaction identifier exhausted"); + pendingProbeTransactionId = nextProbeTransactionId; + sendProbeRequest(); // Dwell begins on completion of this probe's final fragment. } else if (msg->getKind() == MK_SCAN_MINCHANNELTIME) { ASSERT(msg == scanTimer); @@ -226,6 +232,7 @@ void Ieee80211MgmtSta::handleTimer(cMessage *msg) if (scanning.busyChannelDetected) { EV << "Busy channel detected during minChannelTime, continuing listening until maxChannelTime elapses\n"; ASSERT(scanTimer == nullptr); + scanPhase = SCAN_MAX_DWELL; scanTimer = new cMessage("maxChannelTime", MK_SCAN_MAXCHANNELTIME); scheduleAfter(scanning.maxChannelTime - scanning.minChannelTime, scanTimer); } @@ -323,6 +330,8 @@ void Ieee80211MgmtSta::sendManagementFrame(const char *name, const PtraddTag()->setBssid(address); packet->addTag()->setSubtype(subtype); + if (subtype == ST_PROBEREQUEST && isScanning && scanPhase == SCAN_WAIT_PROBE) + packet->addTag()->setTransactionId(pendingProbeTransactionId); packet->insertAtBack(body); sendDown(packet); } @@ -402,17 +411,89 @@ void Ieee80211MgmtSta::startReassociation(ApInfo *ap, simtime_t timeout) void Ieee80211MgmtSta::receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); - - // Note that we are only subscribed during scanning! - if (signalID == IRadio::receptionStateChangedSignal) { - IRadio::ReceptionState newReceptionState = static_cast(value); - if (newReceptionState != IRadio::RECEPTION_STATE_UNDEFINED && newReceptionState != IRadio::RECEPTION_STATE_IDLE) { - EV << "busy radio channel detected during scanning\n"; + if (!isScanning || source != scanningRadio) + return; + if (signalID == scanRadioChannelChangedSignal && scanPhase == SCAN_WAIT_CHANNEL && + value == scanning.channelList[scanning.currentChannelIndex]) { + ASSERT(scanTimer == nullptr); + if (scanning.activeScan) { + scanPhase = SCAN_PROBE_DELAY; + scanTimer = new cMessage("sendProbe", MK_SCAN_SENDPROBE); + scheduleAfter(scanning.probeDelay, scanTimer); + } + else { + scanPhase = SCAN_MAX_DWELL; + scanTimer = new cMessage("maxChannelTime", MK_SCAN_MAXCHANNELTIME); + scheduleAfter(scanning.maxChannelTime, scanTimer); + } + } + else if (signalID == IRadio::receptionStateChangedSignal && scanPhase == SCAN_MIN_DWELL) { + auto state = static_cast(value); + if (state != IRadio::RECEPTION_STATE_UNDEFINED && state != IRadio::RECEPTION_STATE_IDLE) scanning.busyChannelDetected = true; + } +} + +void Ieee80211MgmtSta::startScanDwell() +{ + // IEEE Std 802.11-2024, 11.1.4.3.2: send the probe before starting ActiveScanningTimer. + // Matching transmission completion implements that ordering after contention. + ASSERT(scanTimer == nullptr); + pendingProbeTransactionId = 0; + scanPhase = SCAN_MIN_DWELL; + auto state = check_and_cast(scanningRadio.get())->getReceptionState(); + scanning.busyChannelDetected = state != IRadio::RECEPTION_STATE_UNDEFINED && state != IRadio::RECEPTION_STATE_IDLE; + scanTimer = new cMessage("minChannelTime", MK_SCAN_MINCHANNELTIME); + scheduleAfter(scanning.minChannelTime, scanTimer); +} + +void Ieee80211MgmtSta::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +{ + if (signalID != packetSentToPeerSignal && signalID != Ieee80211Mac::frameTransmissionOutcomeSignal) { + Ieee80211MgmtBase::receiveSignal(source, signalID, obj, details); + return; + } + Enter_Method("%s", cComponent::getSignalName(signalID)); + if (!isScanning || scanPhase != SCAN_WAIT_PROBE) + return; + auto packet = dynamic_cast(obj); + if (!packet) + return; + auto tag = packet->findTag(); + if (!tag || tag->getTransactionId() != pendingProbeTransactionId) + return; + auto header = dynamicPtrCast(packet->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR)); + if (!header || header->getType() != ST_PROBEREQUEST) + return; + if (signalID == packetSentToPeerSignal) { + if (!header->getMoreFragments()) + startScanDwell(); + } + else { + auto outcome = check_and_cast(details); + if (outcome->getStatus() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION || + outcome->getStatus() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED) { + pendingProbeTransactionId = 0; + scanPhase = SCAN_IDLE; + scanTimer = new cMessage("nextScanChannel", MK_SCAN_NEXTCHANNEL); + scheduleAt(simTime(), scanTimer); } } } +void Ieee80211MgmtSta::stopScanListening() +{ + if (isScanning) { + scanningRadio->unsubscribe(IRadio::receptionStateChangedSignal, this); + scanningRadio->unsubscribe(scanRadioChannelChangedSignal, this); + myIface->unsubscribe(packetSentToPeerSignal, this); + myIface->unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + } + isScanning = false; + scanPhase = SCAN_IDLE; + pendingProbeTransactionId = 0; +} + void Ieee80211MgmtSta::processScanCommand(Ieee80211Prim_ScanRequest *ctrl) { EV << "Received Scan Request from agent, clearing AP list and starting scanning...\n"; @@ -444,8 +525,10 @@ void Ieee80211MgmtSta::processScanCommand(Ieee80211Prim_ScanRequest *ctrl) scanning.channelList.push_back(i); // start scanning - if (scanning.activeScan) - host->subscribe(IRadio::receptionStateChangedSignal, this); + scanningRadio->subscribe(IRadio::receptionStateChangedSignal, this); + scanningRadio->subscribe(scanRadioChannelChangedSignal, this); + myIface->subscribe(packetSentToPeerSignal, this); + myIface->subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); scanning.currentChannelIndex = -1; // so we'll start with index==0 isScanning = true; scanNextChannel(); @@ -456,29 +539,17 @@ bool Ieee80211MgmtSta::scanNextChannel() // if we're already at the last channel, we're through if (scanning.currentChannelIndex == (int)scanning.channelList.size() - 1) { EV << "Finished scanning last channel\n"; - if (scanning.activeScan) - host->unsubscribe(IRadio::receptionStateChangedSignal, this); - isScanning = false; + stopScanListening(); return true; // we're done } - // tune to next channel + // Start timing only after the own radio confirms that the requested channel is applied. int newChannel = scanning.channelList[++scanning.currentChannelIndex]; - changeChannel(newChannel); scanning.busyChannelDetected = false; - + pendingProbeTransactionId = 0; + scanPhase = SCAN_WAIT_CHANNEL; ASSERT(scanTimer == nullptr); - if (scanning.activeScan) { - // Active Scan: first wait probeDelay, then send a probe. Listening - // for minChannelTime or maxChannelTime takes place after that. (11.1.3.2) - scanTimer = new cMessage("sendProbe", MK_SCAN_SENDPROBE); - scheduleAfter(scanning.probeDelay, scanTimer); - } - else { - // Passive Scan: spend maxChannelTime on the channel (11.1.3.1) - scanTimer = new cMessage("maxChannelTime", MK_SCAN_MAXCHANNELTIME); - scheduleAfter(scanning.maxChannelTime, scanTimer); - } + changeChannel(newChannel); return false; } @@ -660,9 +731,7 @@ bool Ieee80211MgmtSta::terminateCurrentAssociationFromPeer(const MacAddress& add void Ieee80211MgmtSta::stop() { - if (host != nullptr && isScanning && scanning.activeScan) - host->unsubscribe(IRadio::receptionStateChangedSignal, this); - isScanning = false; + stopScanListening(); cancelScanTimer(); scanning = ScanningInfo(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h index 326c7638644..eb05b5d9217 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h @@ -109,7 +109,13 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase }; protected: - cModule *host; + opp_component_ptr scanningRadio; + enum ScanPhase { SCAN_IDLE, SCAN_WAIT_CHANNEL, SCAN_PROBE_DELAY, SCAN_WAIT_PROBE, SCAN_MIN_DWELL, SCAN_MAX_DWELL }; + ScanPhase scanPhase = SCAN_IDLE; + uint64_t nextProbeTransactionId = 0; + uint64_t pendingProbeTransactionId = 0; + void stopScanListening(); + void startScanDwell(); // number of channels in RadioMedium -- used if we're told to scan "all" channels int numChannels; @@ -130,7 +136,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase AssociatedApInfo assocAP; public: - Ieee80211MgmtSta() : host(nullptr), numChannels(-1), isScanning(false), scanTimer(nullptr), assocTimeoutMsg(nullptr) {} + Ieee80211MgmtSta() : numChannels(-1), isScanning(false), scanTimer(nullptr), assocTimeoutMsg(nullptr) {} virtual ~Ieee80211MgmtSta(); virtual const ApInfo *getAssociatedAp() { return &assocAP; } @@ -227,6 +233,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase /** Called by the signal handler whenever a change occurs we're interested in */ virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; /** lifecycle support */ virtual void stop() override; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.ned b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.ned index 8ca5f6e70d5..f5b5f289aa2 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.ned +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.ned @@ -31,6 +31,7 @@ simple Ieee80211MgmtSta extends SimpleModule like IIeee80211Mgmt string interfaceTableModule; // The path to the InterfaceTable module int numChannels = default(1); // Number of channels to scan string mibModule; + string radioModule = default("^.radio"); // Own radio for scan channel completion and busy detection string macModule; // The path to the MAC module @display("i=block/cogwheel"); @signal[l2Associated](type=inet::NetworkInterface); diff --git a/tests/module/Ieee80211ScanCompletionQos_1.test b/tests/module/Ieee80211ScanCompletionQos_1.test new file mode 100644 index 00000000000..8c9e97369f7 --- /dev/null +++ b/tests/module/Ieee80211ScanCompletionQos_1.test @@ -0,0 +1,240 @@ +%description: +Verify that active scan minimum dwell begins after the Probe Request has completed +transmission, and that passive scan dwell confirmation is preceded by application +of the requested radio channel when the scan starts while the radio is busy. + +%file: Ieee80211ScanCompletionQos.cc + +#include + +#include "inet/common/InitStages.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211Primitives_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" + +namespace inet { +namespace ieee80211 { + +class ScanCompletionQosObserver : public cSimpleModule, public cListener +{ + protected: + cMessage *trigger = nullptr; + cModule *radio = nullptr; + bool activeScan = true; + bool waitForBusyReception = false; + int targetChannel = 1; + bool scanRequested = false; + int scanConfirms = 0; + bool probeTransmissionCompleted = false; + simtime_t probeTransmissionCompletionTime = SIMTIME_ZERO; + bool requestedChannelApplied = false; + simtime_t requestedChannelApplicationTime = SIMTIME_ZERO; + simtime_t scanConfirmTime = SIMTIME_ZERO; + + public: + int getScanConfirms() const { return scanConfirms; } + bool hasProbeTransmissionCompleted() const { return probeTransmissionCompleted; } + simtime_t getProbeTransmissionCompletionTime() const { return probeTransmissionCompletionTime; } + bool hasRequestedChannelApplied() const { return requestedChannelApplied; } + simtime_t getRequestedChannelApplicationTime() const { return requestedChannelApplicationTime; } + simtime_t getScanConfirmTime() const { return scanConfirmTime; } + + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) { + activeScan = par("active"); + waitForBusyReception = par("waitBusy"); + targetChannel = par("targetChannel"); + trigger = new cMessage("scanTrigger"); + radio = getParentModule()->getSubmodule("radio"); + radio->subscribe(physicallayer::IRadio::receptionStateChangedSignal, this); + radio->subscribe(physicallayer::Ieee80211Radio::radioChannelChangedSignal, this); + getParentModule()->subscribe(packetSentToPeerSignal, this); + } + if (stage == INITSTAGE_APPLICATION_LAYER && !waitForBusyReception) + scheduleAfter(SimTime(1, SIMTIME_MS), trigger); + } + + virtual void handleMessage(cMessage *message) override + { + if (message == trigger) { + ASSERT(!scanRequested); + scanRequested = true; + auto request = new Ieee80211Prim_ScanRequest(); + request->setBSSType(BSSTYPE_INFRASTRUCTURE); + request->setActiveScan(activeScan); + request->setProbeDelay(SIMTIME_ZERO); + request->setMinChannelTime(SimTime(1, SIMTIME_US)); + request->setMaxChannelTime(SimTime(2, SIMTIME_MS)); + request->setChannelListArraySize(1); + request->setChannelList(0, targetChannel); + auto command = new cMessage("scanRequest"); + command->setControlInfo(request); + send(command, "mgmtOut"); + } + else { + auto confirm = dynamic_cast(message->getControlInfo()); + ASSERT(confirm != nullptr); + ASSERT(confirm->getResultCode() == PRC_SUCCESS); + scanConfirms++; + scanConfirmTime = simTime(); + delete message; + } + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, intval_t value, cObject *) override + { + if (signalID == physicallayer::IRadio::receptionStateChangedSignal && waitForBusyReception && !scanRequested && + !trigger->isScheduled() && value == physicallayer::IRadio::RECEPTION_STATE_RECEIVING) + scheduleAfter(SIMTIME_ZERO, trigger); + else if (signalID == physicallayer::Ieee80211Radio::radioChannelChangedSignal && scanRequested && value == targetChannel) { + requestedChannelApplied = true; + requestedChannelApplicationTime = simTime(); + } + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *object, cObject *) override + { + if (signalID != packetSentToPeerSignal || !scanRequested || object == nullptr) + return; + auto packet = dynamic_cast(object); + if (packet == nullptr) + return; + const auto& header = packet->peekAtFront(); + if (header->getType() == ST_PROBEREQUEST) { + ASSERT(!probeTransmissionCompleted); + ASSERT(requestedChannelApplied && requestedChannelApplicationTime <= simTime()); + probeTransmissionCompleted = true; + probeTransmissionCompletionTime = simTime(); + } + } + + using cListener::finish; + + virtual void finish() override + { + if (radio != nullptr) { + radio->unsubscribe(physicallayer::IRadio::receptionStateChangedSignal, this); + radio->unsubscribe(physicallayer::Ieee80211Radio::radioChannelChangedSignal, this); + } + if (getParentModule() != nullptr) + getParentModule()->unsubscribe(packetSentToPeerSignal, this); + } + + public: + virtual ~ScanCompletionQosObserver() + { + cancelAndDelete(trigger); + } +}; + +Define_Module(ScanCompletionQosObserver); + +class Ieee80211ScanCompletionQosTest : public cSimpleModule +{ + protected: + virtual void finish() override + { + auto active = check_and_cast(getModuleByPath("^.activeSta.wlan[0].agent")); + auto passive = check_and_cast(getModuleByPath("^.passiveSta.wlan[0].agent")); + + ASSERT(active->getScanConfirms() == 1); + ASSERT(active->hasProbeTransmissionCompleted()); + ASSERT(active->getScanConfirmTime() >= active->getProbeTransmissionCompletionTime() + SimTime(1, SIMTIME_US)); + + ASSERT(passive->getScanConfirms() == 1); + ASSERT(passive->hasRequestedChannelApplied()); + ASSERT(passive->getRequestedChannelApplicationTime() + SimTime(2, SIMTIME_MS) == passive->getScanConfirmTime()); + + std::cout << "IEEE 802.11 scan dwell and retune completion ordering verified.\n"; + } +}; + +Define_Module(Ieee80211ScanCompletionQosTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.IIeee80211Agent; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple ScanCompletionQosObserver like IIeee80211Agent +{ + parameters: + bool active = default(true); + bool waitBusy = default(false); + int targetChannel = default(1); + @class(::inet::ieee80211::ScanCompletionQosObserver); + gates: + input mgmtIn; + output mgmtOut; +} + +simple Ieee80211ScanCompletionQosTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211ScanCompletionQosTest); +} + +network Ieee80211ScanCompletionQosTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + activeSta: WirelessHost { + parameters: + wlan[*].agent.typename = "ScanCompletionQosObserver"; + wlan[*].agent.active = true; + wlan[*].agent.waitBusy = false; + } + passiveSta: WirelessHost { + parameters: + wlan[*].agent.typename = "ScanCompletionQosObserver"; + wlan[*].agent.active = false; + wlan[*].agent.waitBusy = true; + } + test: Ieee80211ScanCompletionQosTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211ScanCompletionQosTestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +**.wlan[*].mac.qosStation = true +sim-time-limit = 5ms +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.analogModel.ignorePartialInterference = true +**.mobility.typename = "StationaryMobility" +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = 2Mbps +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB +*.ap.wlan[*].mgmt.beaconInterval = 1024us +*.activeSta.wlan[*].mgmt.numChannels = 2 +*.passiveSta.wlan[*].mgmt.numChannels = 2 +*.activeSta.wlan[*].mac.dcf.channelAccess.cwMin = 0 +*.passiveSta.wlan[*].mac.dcf.channelAccess.cwMin = 0 + +%contains: stdout +IEEE 802.11 scan dwell and retune completion ordering verified. diff --git a/tests/module/Ieee80211ScanCompletion_1.test b/tests/module/Ieee80211ScanCompletion_1.test new file mode 100644 index 00000000000..c7334513bb3 --- /dev/null +++ b/tests/module/Ieee80211ScanCompletion_1.test @@ -0,0 +1,239 @@ +%description: +Verify that active scan minimum dwell begins after the Probe Request has completed +transmission, and that passive scan dwell confirmation is preceded by application +of the requested radio channel when the scan starts while the radio is busy. + +%file: Ieee80211ScanCompletion.cc + +#include + +#include "inet/common/InitStages.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211Primitives_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" + +namespace inet { +namespace ieee80211 { + +class ScanCompletionObserver : public cSimpleModule, public cListener +{ + protected: + cMessage *trigger = nullptr; + cModule *radio = nullptr; + bool activeScan = true; + bool waitForBusyReception = false; + int targetChannel = 1; + bool scanRequested = false; + int scanConfirms = 0; + bool probeTransmissionCompleted = false; + simtime_t probeTransmissionCompletionTime = SIMTIME_ZERO; + bool requestedChannelApplied = false; + simtime_t requestedChannelApplicationTime = SIMTIME_ZERO; + simtime_t scanConfirmTime = SIMTIME_ZERO; + + public: + int getScanConfirms() const { return scanConfirms; } + bool hasProbeTransmissionCompleted() const { return probeTransmissionCompleted; } + simtime_t getProbeTransmissionCompletionTime() const { return probeTransmissionCompletionTime; } + bool hasRequestedChannelApplied() const { return requestedChannelApplied; } + simtime_t getRequestedChannelApplicationTime() const { return requestedChannelApplicationTime; } + simtime_t getScanConfirmTime() const { return scanConfirmTime; } + + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) { + activeScan = par("active"); + waitForBusyReception = par("waitBusy"); + targetChannel = par("targetChannel"); + trigger = new cMessage("scanTrigger"); + radio = getParentModule()->getSubmodule("radio"); + radio->subscribe(physicallayer::IRadio::receptionStateChangedSignal, this); + radio->subscribe(physicallayer::Ieee80211Radio::radioChannelChangedSignal, this); + getParentModule()->subscribe(packetSentToPeerSignal, this); + } + if (stage == INITSTAGE_APPLICATION_LAYER && !waitForBusyReception) + scheduleAfter(SimTime(1, SIMTIME_MS), trigger); + } + + virtual void handleMessage(cMessage *message) override + { + if (message == trigger) { + ASSERT(!scanRequested); + scanRequested = true; + auto request = new Ieee80211Prim_ScanRequest(); + request->setBSSType(BSSTYPE_INFRASTRUCTURE); + request->setActiveScan(activeScan); + request->setProbeDelay(SIMTIME_ZERO); + request->setMinChannelTime(SimTime(1, SIMTIME_US)); + request->setMaxChannelTime(SimTime(2, SIMTIME_MS)); + request->setChannelListArraySize(1); + request->setChannelList(0, targetChannel); + auto command = new cMessage("scanRequest"); + command->setControlInfo(request); + send(command, "mgmtOut"); + } + else { + auto confirm = dynamic_cast(message->getControlInfo()); + ASSERT(confirm != nullptr); + ASSERT(confirm->getResultCode() == PRC_SUCCESS); + scanConfirms++; + scanConfirmTime = simTime(); + delete message; + } + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, intval_t value, cObject *) override + { + if (signalID == physicallayer::IRadio::receptionStateChangedSignal && waitForBusyReception && !scanRequested && + !trigger->isScheduled() && value == physicallayer::IRadio::RECEPTION_STATE_RECEIVING) + scheduleAfter(SIMTIME_ZERO, trigger); + else if (signalID == physicallayer::Ieee80211Radio::radioChannelChangedSignal && scanRequested && value == targetChannel) { + requestedChannelApplied = true; + requestedChannelApplicationTime = simTime(); + } + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *object, cObject *) override + { + if (signalID != packetSentToPeerSignal || !scanRequested || object == nullptr) + return; + auto packet = dynamic_cast(object); + if (packet == nullptr) + return; + const auto& header = packet->peekAtFront(); + if (header->getType() == ST_PROBEREQUEST) { + ASSERT(!probeTransmissionCompleted); + ASSERT(requestedChannelApplied && requestedChannelApplicationTime <= simTime()); + probeTransmissionCompleted = true; + probeTransmissionCompletionTime = simTime(); + } + } + + using cListener::finish; + + virtual void finish() override + { + if (radio != nullptr) { + radio->unsubscribe(physicallayer::IRadio::receptionStateChangedSignal, this); + radio->unsubscribe(physicallayer::Ieee80211Radio::radioChannelChangedSignal, this); + } + if (getParentModule() != nullptr) + getParentModule()->unsubscribe(packetSentToPeerSignal, this); + } + + public: + virtual ~ScanCompletionObserver() + { + cancelAndDelete(trigger); + } +}; + +Define_Module(ScanCompletionObserver); + +class Ieee80211ScanCompletionTest : public cSimpleModule +{ + protected: + virtual void finish() override + { + auto active = check_and_cast(getModuleByPath("^.activeSta.wlan[0].agent")); + auto passive = check_and_cast(getModuleByPath("^.passiveSta.wlan[0].agent")); + + ASSERT(active->getScanConfirms() == 1); + ASSERT(active->hasProbeTransmissionCompleted()); + ASSERT(active->getScanConfirmTime() >= active->getProbeTransmissionCompletionTime() + SimTime(1, SIMTIME_US)); + + ASSERT(passive->getScanConfirms() == 1); + ASSERT(passive->hasRequestedChannelApplied()); + ASSERT(passive->getRequestedChannelApplicationTime() + SimTime(2, SIMTIME_MS) == passive->getScanConfirmTime()); + + std::cout << "IEEE 802.11 scan dwell and retune completion ordering verified.\n"; + } +}; + +Define_Module(Ieee80211ScanCompletionTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.IIeee80211Agent; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple ScanCompletionObserver like IIeee80211Agent +{ + parameters: + bool active = default(true); + bool waitBusy = default(false); + int targetChannel = default(1); + @class(::inet::ieee80211::ScanCompletionObserver); + gates: + input mgmtIn; + output mgmtOut; +} + +simple Ieee80211ScanCompletionTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211ScanCompletionTest); +} + +network Ieee80211ScanCompletionTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint; + activeSta: WirelessHost { + parameters: + wlan[*].agent.typename = "ScanCompletionObserver"; + wlan[*].agent.active = true; + wlan[*].agent.waitBusy = false; + } + passiveSta: WirelessHost { + parameters: + wlan[*].agent.typename = "ScanCompletionObserver"; + wlan[*].agent.active = false; + wlan[*].agent.waitBusy = true; + } + test: Ieee80211ScanCompletionTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211ScanCompletionTestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +sim-time-limit = 5ms +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.analogModel.ignorePartialInterference = true +**.mobility.typename = "StationaryMobility" +**.mobility.initFromDisplayString = false +**.mobility.initialX = 0m +**.mobility.initialY = 0m +**.mobility.initialZ = 0m +**.wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = 2Mbps +**.wlan[*].radio.channelNumber = 0 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB +*.ap.wlan[*].mgmt.beaconInterval = 1024us +*.activeSta.wlan[*].mgmt.numChannels = 2 +*.passiveSta.wlan[*].mgmt.numChannels = 2 +*.activeSta.wlan[*].mac.dcf.channelAccess.cwMin = 0 +*.passiveSta.wlan[*].mac.dcf.channelAccess.cwMin = 0 + +%contains: stdout +IEEE 802.11 scan dwell and retune completion ordering verified. From 4456bdbc78f38e125e1e5e974a93a8ff219898c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 16 Sep 2026 00:23:07 +0200 Subject: [PATCH 3/3] tests: add: check scan event source and transaction isolation A busy indication from another component must not extend the scanning radio dwell, and completion of an older probe must not advance a new scan. Exercise these handlers directly alongside the DCF/HCF exchange timing tests. Change: tests | behavior.add | test | wifi-audit --- tests/unit/Ieee80211ScanEventScope_1.test | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/unit/Ieee80211ScanEventScope_1.test diff --git a/tests/unit/Ieee80211ScanEventScope_1.test b/tests/unit/Ieee80211ScanEventScope_1.test new file mode 100644 index 00000000000..a9d3d613474 --- /dev/null +++ b/tests/unit/Ieee80211ScanEventScope_1.test @@ -0,0 +1,47 @@ +%description: +Scan busy detection accepts only the scanning radio's indication, and a completed +probe from an older transaction cannot advance a new scan's state machine. + +%includes: +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +%global: +class ScopedScanStation : public Ieee80211MgmtSta { + public: + void checkScope(cModule *own, cModule *other) { + ASSERT(own != other); + isScanning = true; + scanPhase = SCAN_MIN_DWELL; + scanningRadio = own; + scanning.busyChannelDetected = false; + receiveSignal(other, IRadio::receptionStateChangedSignal, intval_t(IRadio::RECEPTION_STATE_RECEIVING), nullptr); + ASSERT(!scanning.busyChannelDetected); + receiveSignal(own, IRadio::receptionStateChangedSignal, intval_t(IRadio::RECEPTION_STATE_RECEIVING), nullptr); + ASSERT(scanning.busyChannelDetected); + scanPhase = SCAN_WAIT_PROBE; + pendingProbeTransactionId = 8; + auto header = makeShared(); + header->setType(ST_PROBEREQUEST); + Packet stale("old-probe", header); + stale.addTag()->setTransactionId(7); + receiveSignal(own, packetSentToPeerSignal, &stale, nullptr); + ASSERT(scanPhase == SCAN_WAIT_PROBE && pendingProbeTransactionId == 8 && scanTimer == nullptr); + // This harness calls the handler directly; it never installs subscriptions. + isScanning = false; + scanningRadio = nullptr; + } +}; + +%activity: +ScopedScanStation station; +station.checkScope(getSimulation()->getSystemModule(), &station); +EV << "Scan events are scoped to the radio and probe transaction.\n"; + +%contains: stdout +Scan events are scoped to the radio and probe transaction.