Main Content

OFDM Wi-Fi Scanner Using SDR Preamble Detection

This example shows how to retrieve information about Wi-Fi® networks using a software-defined radio (SDR) and preamble detection. The example scans over the 5 GHz channels and uses an SDR preamble detector to detect and capture orthogonal frequency-division multiplexing (OFDM) packets from the air. The example then decodes the OFDM packets to determine which packets are access point (AP) beacons. The AP beacon information includes the service set identifier (SSID), media access control (MAC) address (also known as the basic SSID, or BSSID), AP channel bandwidth, and 802.11 standard used by the AP.

Introduction

This example scans through a set of Wi-Fi channels to detect AP beacons that are transmitted on 20 MHz subchannels. The scanning procedure uses a preamble detector on an NI™ USRP™ radio.

The scanning procedure comprises of these steps.

  • Configure the preambleDetector (Wireless Testbench) object with a preamble that is generated from the legacy long training field (L-LTF).

  • Set the frequency band and channels for the preamble detector to scan.

  • Scan each specified channel and with each successful detection of an OFDM packet, capture a waveform for a set duration.

  • Process the waveform in MATLAB® by searching for beacon frames in the captured waveform and extracting relevant information from each successfully decoded beacon frame.

  • Display key information about the detected APs.

Set Up Radio

Call the radioConfigurations (Wireless Testbench) function. The function returns all available radio setup configurations that you saved using the Radio Setup (Wireless Testbench) wizard.

savedRadioConfigurations = radioConfigurations;

To update the dropdown menu with your saved radio setup configuration names, click Update. Then select the radio to use with this example.

savedRadioConfigurationNames = [string({savedRadioConfigurations.Name})];
radio = savedRadioConfigurationNames(1) ;

Configure Preamble Detector

Create a preamble detector object with the specified radio. Because the object requires exclusive access to radio hardware resources, before running this example for the first time, clear any other object associated with the specified radio. In subsequent runs, to speed up the execution time of the example, reuse your new workspace object.

if ~exist("pd","var")
    pd = preambleDetector(radio);
end

To update the dropdown menu with the antennas available for capture on your radio, call the hCaptureAntennas helper function. Then select the antenna to use with this example.

captureAntennaSelection = hCaptureAntennas(radio);
pd.Antennas = captureAntennaSelection(1);

To increase the capture sample rate to 40 MHz, specify an oversampling factor of 2.

osf = 2;
pd.SampleRate = 20e6*osf;
pd.CaptureDataType = "double";
pd.ThresholdMethod = "adaptive";

Configure Preamble For Radio

The 802.11 standard requires that all Wi-Fi APs must transmit OFDM beacons using non-high throughput (non-HT) packets over a 20 MHz bandwidth. Therefore, generate a 20 MHz L-LTF waveform and use one long training symbol from the generated waveform as the preamble to detect WLAN OFDM packets.

cbw = "CBW20";
cfg = wlanNonHTConfig(ChannelBandwidth=cbw);
lltf = wlanLLTF(cfg,OversamplingFactor=osf);

Extract the first long training symbol from the L-LTF waveform.

cyclicPrefixLength = 1.6e-6*pd.SampleRate;
trainingSymbolLength = 3.2e-6*pd.SampleRate;
preamble = lltf(cyclicPrefixLength+1:cyclicPrefixLength+trainingSymbolLength);

Because the preamble detector requires the preamble to be between –1 and 1, normalize and set the preamble.

preamble = preamble/sqrt(sum(abs(preamble).^2));
pd.Preamble = preamble;

To capture the entire first non-HT packet, you must set the trigger offset to a negative value. Since you created a matched filter based on the long training symbol in the L-LTF waveform, the offset is at least one legacy short training field (L-STF), one L-LTF cyclic prefix, and one long training symbol.

lstfLength = 8e-6*pd.SampleRate;
pd.TriggerOffset = -(lstfLength + cyclicPrefixLength + trainingSymbolLength + 5);

Tune Preamble Detector

Configure the adaptive threshold gain, the adaptive threshold offset, and the radio gain values of the preamble detector for the local environment. Configuring these values requires manual tuning by exploring the trigger points provided by the plotThreshold (Wireless Testbench) function. For more information on tuning these values, see Triggered Capture Using Preamble Detection (Wireless Testbench).

For tuning the preamble detector, specify a channel in the 5 GHz band with a known OFDM packet.

In the 5 GHz band, the valid channel numbers are 1–200. However, the valid 20 MHz control channels for APs that use the 5 GHz band are 32, 36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165, 169, 173, 177.

band = 5;
channel = 100;

pd.CenterFrequency = wlanChannelFrequency(channel,band);

Adjust these values for tuning the preamble detector.

pd.AdaptiveThresholdGain = 0.3;
pd.AdaptiveThresholdOffset = 0.00003;
pd.RadioGain = 30;

Plot the filter output power, adaptive threshold, and trigger points of the reconfigured preamble detector. The generated figure contains two trigger points for each OFDM packet. Each trigger point corresponds to a long training symbol.

When you generate a plotThreshold figure, if you do not have at least two trigger points for each OFDM packet, readjust the adaptive threshold gain, the adaptive threshold offset, and the radio gain until there are at least two trigger points per OFDM packet.

captureDuration = milliseconds(120);
plotThreshold(pd,captureDuration);

Figure contains an axes object. The axes object with xlabel Samples, ylabel Power contains 4 objects of type line. One or more of the lines displays its values using only markers These objects represent Correlator Output Power, Adaptive Threshold, Adaptive Threshold Offset, Trigger Point.

Inspect the trigger points by zooming in along the x-axis of the plot. For example, this figure shows a zoomed-in view of an OFDM packet with the trigger points on the correlation peaks.

Scan Wi-Fi Channels

Specify Scanning Region

Specify the channels in the 5 GHz band for the SDR to scan.

band = 5;
channels = [32 36 40 44 48 52 56 60 64 100 104 108 112 116 120 124 128 132 136 140 144 149 153 157 161 165 169 173 177];

Generate the center frequencies associated with the selected channels and band values.

centerFrequencies = wlanChannelFrequency(channels,band);

Receiver Design

This diagram shows an overview of the receiver for scanning the selected channels and frequency band.

These steps provide further information on the diagram.

  1. Set the center frequency of the preamble detector, then initialize the detection and capture of a waveform for a set duration.

  2. Check if the preamble detector detects an OFDM packet.

  3. Determine and apply frequency and timing corrections on the waveform, then attempt to recover the legacy signal (L-SIG) field bits.

  4. Check that the packet format is non-HT.

  5. From the recovered L-SIG, extract the modulation and coding scheme (MCS) and the length of the PLCP service data unit (PSDU). Then recover the non-HT data and subsequently decode the MAC protocol data unit (MPDU).

  6. Using the recovered MAC frame configuration, check if the non-HT packet is a beacon.

  7. Recover the SSID, BSSID, vendor of the AP, SNR, primary 20 MHz channel, current channel center frequency index, supported channel width, frequency band, and wireless standard used by the AP.

  8. Check if the waveform contains another packet that you can decode.

Initialize Variables

When you call the capture function to detect and capture a signal, you must specify the length of the capture and the signal detection timeout. Since beacons transmit every 100 time units (TU), set the capture length and the signal detection timeout to 102.4 ms.

captureLength = milliseconds(102.4);
timeout = milliseconds(102.4);

Create a structure (APs) for storing this information for each successfully decoded beacon.

  • SSID

  • BSSID

  • Vendor of AP

  • Signal-to-noise ratio (SNR)

  • Primary 20 MHz channel

  • Current channel center frequency

  • Channel width

  • Frequency band

  • Operating mode supported by the AP

  • MAC frame configuration

  • Waveform in which the beacon exists

  • Index value at which the non-HT beacon packet begins in the captured waveform

APs = struct(...
    "SSID",[],"BSSID",[],"Vendor",[],"SNR_dB",[],"Beacon_Channel",[], ...
    "Operating_Channel",[],"Channel_Width_MHz",[],"Band",[],"Mode",[], ...
    "MAC_Config",wlanMACFrameConfig,"Waveform",[],"Offset",[]);

To determine the hardware manufacturer of the AP, select the retrieveVendorInfo box. Selecting the retrieveVendorInfo box downloads the organizationally unique identifier (OUI) CSV file from the IEEE® Registration Authority website for vendor AP identification.

retrieveVendorInfo = true;
counter = 1;
ind = wlanFieldIndices(cfg);

% Begin scanning and decoding for specified channels.
for i = 1:length(centerFrequencies)

    pd.CenterFrequency = centerFrequencies(i);

    fprintf("Scanning channel %d on band %.1f.\n",channels(i),band);
    [capturedData, ~, ~, status] = capture(pd, captureLength, timeout);

    if ~status
        % If no non-HT packet is decoded, go to next channel.
        fprintf("No non-HT packet detected on channel %d in band %.1f.\n",channels(i),band);
        continue;
    else
        fprintf("<strong>Non-HT packet detected on channel %d in band %.1f.</strong>\n",channels(i),band)
    end
    % Resample the captured data to 20 MHz for beacon processing.
    capturedData = resample(capturedData,1,osf);
    searchOffset = 0;
    while searchOffset<length(capturedData)

        % recoverPreamble detects a packet and performs analysis of the non-HT preamble.
        [preambleStatus,res] = recoverPreamble(capturedData,cbw,searchOffset);

        if matches(preambleStatus,"No packet detected")
            break;
        end

        % Retrieve synchronized data and scale it with LSTF power as done
        % in the recoverPreamble function.
        syncData = capturedData(res.PacketOffset+1:end)./sqrt(res.LSTFPower);
        syncData = frequencyOffset(syncData,pd.SampleRate/osf,-res.CFOEstimate);

        % Need only 4 OFDM symbols (LSIG + 3 more symbols) following LLTF
        % for format detection
        fmtDetect = syncData(ind.LSIG(1):(ind.LSIG(2)+4e-6*pd.SampleRate/osf*3));

        [LSIGBits, failcheck] = wlanLSIGRecover(fmtDetect(1:4e-6*pd.SampleRate/osf*1), ...
            res.ChanEstNonHT,res.NoiseEstNonHT,cbw);

        if ~failcheck
            format = wlanFormatDetect(fmtDetect,res.ChanEstNonHT,res.NoiseEstNonHT,cbw);
            if matches(format,"Non-HT")

                % Extract MCS from first 3 bits of L-SIG.
                rate = double(bit2int(LSIGBits(1:3),3));
                if rate <= 1
                    cfg.MCS = rate + 6;
                else
                    cfg.MCS = mod(rate,6);
                end

                % Determine PSDU length from L-SIG.
                cfg.PSDULength = double(bit2int(LSIGBits(6:17),12,0));
                ind.NonHTData = wlanFieldIndices(cfg,"NonHT-Data");

                if double(ind.NonHTData(2)-ind.NonHTData(1))> ...
                        length(syncData(ind.NonHTData(1):end))
                    % Exit while loop as full packet not captured.
                    break;
                end

                nonHTData = syncData(ind.NonHTData(1):ind.NonHTData(2));
                bitsData = wlanNonHTDataRecover(nonHTData,res.ChanEstNonHT, ...
                    res.NoiseEstNonHT,cfg);
                [cfgMAC, ~, decodeStatus] = wlanMPDUDecode(bitsData,cfg, ...
                    SuppressWarnings=true);

                % Extract information about channel from the beacon.
                if ~decodeStatus && matches(cfgMAC.FrameType,"Beacon")
                    fprintf("Beacon detected on channel %d in band %.1f.\n",channels(i),band);

                    % Populate the table with information about the beacon.
                    if isempty(cfgMAC.ManagementConfig.SSID)
                        APs(counter).SSID = "Hidden";
                    else
                        APs(counter).SSID = string(cfgMAC.ManagementConfig.SSID);
                    end

                    APs(counter).BSSID = string(cfgMAC.Address3);
                    if retrieveVendorInfo
                        APs(counter).Vendor = determineVendor(cfgMAC.Address3);
                    else
                        APs(counter).Vendor = "Skipped"; %#ok<UNRCH>
                    end
                    [APs(counter).Mode, APs(counter).Channel_Width_MHz, operatingChannel] = ...
                        determineMode(cfgMAC.ManagementConfig.InformationElements);

                    if isempty(operatingChannel)
                        % Default to scanning channel if operating channel
                        % cannot be determined.
                        operatingChannel = channels(i);
                    end

                    APs(counter).Beacon_Channel = channels(i);
                    APs(counter).Operating_Channel = operatingChannel;
                    APs(counter).SNR_dB = res.LLTFSNR;
                    APs(counter).MAC_Config = cfgMAC;
                    APs(counter).Offset = res.PacketOffset;
                    APs(counter).Waveform = capturedData;
                    counter = counter + 1;
                end
                % Shift packet search offset for next iteration of while loop.
                searchOffset = res.PacketOffset + double(ind.NonHTData(2));
            else
                % Packet is NOT non-HT; shift packet search offset by 10 OFDM symbols (minimum
                % packet length of non-HT) for next iteration of while loop.
                searchOffset = res.PacketOffset + 4e-6*pd.SampleRate/osf*10;
            end
        else
            % L-SIG recovery failed; shift packet search offset by 10 OFDM symbols (minimum
            % packet length of non-HT) for next iteration of while loop.
            searchOffset = res.PacketOffset + 4e-6*pd.SampleRate/osf*10;
        end
    end
end
Scanning channel 32 on band 5.0.
No non-HT packet detected on channel 32 in band 5.0.
Scanning channel 36 on band 5.0.
No non-HT packet detected on channel 36 in band 5.0.
Scanning channel 40 on band 5.0.
No non-HT packet detected on channel 40 in band 5.0.
Scanning channel 44 on band 5.0.
No non-HT packet detected on channel 44 in band 5.0.
Scanning channel 48 on band 5.0.
No non-HT packet detected on channel 48 in band 5.0.
Scanning channel 52 on band 5.0.
No non-HT packet detected on channel 52 in band 5.0.
Scanning channel 56 on band 5.0.
Non-HT packet detected on channel 56 in band 5.0.
Scanning channel 60 on band 5.0.
No non-HT packet detected on channel 60 in band 5.0.
Scanning channel 64 on band 5.0.
No non-HT packet detected on channel 64 in band 5.0.
Scanning channel 100 on band 5.0.
Non-HT packet detected on channel 100 in band 5.0.
Beacon detected on channel 100 in band 5.0.
Beacon detected on channel 100 in band 5.0.
Beacon detected on channel 100 in band 5.0.
Beacon detected on channel 100 in band 5.0.
Beacon detected on channel 100 in band 5.0.
Scanning channel 104 on band 5.0.
No non-HT packet detected on channel 104 in band 5.0.
Scanning channel 108 on band 5.0.
No non-HT packet detected on channel 108 in band 5.0.
Scanning channel 112 on band 5.0.
No non-HT packet detected on channel 112 in band 5.0.
Scanning channel 116 on band 5.0.
No non-HT packet detected on channel 116 in band 5.0.
Scanning channel 120 on band 5.0.
No non-HT packet detected on channel 120 in band 5.0.
Scanning channel 124 on band 5.0.
Non-HT packet detected on channel 124 in band 5.0.
Beacon detected on channel 124 in band 5.0.
Beacon detected on channel 124 in band 5.0.
Beacon detected on channel 124 in band 5.0.
Beacon detected on channel 124 in band 5.0.
Beacon detected on channel 124 in band 5.0.
Scanning channel 128 on band 5.0.
No non-HT packet detected on channel 128 in band 5.0.
Scanning channel 132 on band 5.0.
No non-HT packet detected on channel 132 in band 5.0.
Scanning channel 136 on band 5.0.
No non-HT packet detected on channel 136 in band 5.0.
Scanning channel 140 on band 5.0.
No non-HT packet detected on channel 140 in band 5.0.
Scanning channel 144 on band 5.0.
No non-HT packet detected on channel 144 in band 5.0.
Scanning channel 149 on band 5.0.
No non-HT packet detected on channel 149 in band 5.0.
Scanning channel 153 on band 5.0.
No non-HT packet detected on channel 153 in band 5.0.
Scanning channel 157 on band 5.0.
No non-HT packet detected on channel 157 in band 5.0.
Scanning channel 161 on band 5.0.
No non-HT packet detected on channel 161 in band 5.0.
Scanning channel 165 on band 5.0.
No non-HT packet detected on channel 165 in band 5.0.
Scanning channel 169 on band 5.0.
No non-HT packet detected on channel 169 in band 5.0.
Scanning channel 173 on band 5.0.
No non-HT packet detected on channel 173 in band 5.0.
Scanning channel 177 on band 5.0.
No non-HT packet detected on channel 177 in band 5.0.

Convert the APs structure to a table and display the information specified in step 7 by using the local function generateBeaconTable.

detectedBeaconsInfo = generateBeaconTable(APs,band)
detectedBeaconsInfo=10×9 table
        SSID            BSSID                    Vendor               SNR (dB)    Primary 20 MHz Channel    Current Channel Center Frequency Index    Channel Width (MHz)    Band       Mode   
    ____________    ______________    ____________________________    ________    ______________________    ______________________________________    ___________________    ____    __________

    "wlan1234_5"    "04D4C451C584"    "ASUSTek COMPUTER INC."          15.298              100                               106                             "80"             5      "802.11ax"
    "wlan1234_5"    "04D4C451C584"    "ASUSTek COMPUTER INC."          15.235              100                               106                             "80"             5      "802.11ax"
    "wlan1234_5"    "04D4C451C584"    "ASUSTek COMPUTER INC."          15.114              100                               106                             "80"             5      "802.11ax"
    "wlan1234_5"    "04D4C451C584"    "ASUSTek COMPUTER INC."          14.931              100                               106                             "80"             5      "802.11ax"
    "wlan1234_5"    "04D4C451C584"    "ASUSTek COMPUTER INC."          15.809              100                               106                             "80"             5      "802.11ax"
    "w-inside"      "B0B867F3D9B0"    "Hewlett Packard Enterprise"     24.668              124                               122                             "80"             5      "802.11ac"
    "w-mobile"      "B0B867F3D9B1"    "Hewlett Packard Enterprise"     24.217              124                               122                             "80"             5      "802.11ac"
    "w-guest"       "B0B867F3D9B2"    "Hewlett Packard Enterprise"     23.676              124                               122                             "80"             5      "802.11ac"
    "w-iot"         "B0B867F3D9B3"    "Hewlett Packard Enterprise"     24.636              124                               122                             "80"             5      "802.11ac"
    "w-training"    "B0B867F3D9B4"    "Hewlett Packard Enterprise"     23.559              124                               122                             "80"             5      "802.11ac"

Further Exploration

  • The detectedBeaconsInfo table shows only key information about the APs. To get further information about the beacons, such as data rates supported by the AP, explore the MAC frame configuration in the APs structure.

  • If you have access to a configurable AP, change the channel width of your AP and rerun the example to confirm the channel width.

Local Functions

These functions assist in processing the incoming beacons.

function vendor = determineVendor(mac)
% DETERMINEVENDOR returns the vendor name of the AP by extracting the
% organizationally unique identifier (OUI) from the specified MAC address.

persistent ouis

vendor = strings(0);
try
    if isempty(ouis)
        if ~exist("oui.csv","file")
            disp("Downloading oui.csv from IEEE Registration Authority...")
            % Increase websave timeout if necessary
            options = weboptions("Timeout",5);
            websave("oui.csv","http://standards-oui.ieee.org/oui/oui.csv",options);
        end
        ouis = readtable("oui.csv",VariableNamingRule="preserve");
    end

    % Extract OUI from MAC Address.
    oui = mac(1:6);

    % Extract vendors name based on OUI.
    vendor = string(cell2mat(ouis.("Organization Name")(matches(ouis.Assignment,oui))));

catch ME
    % Rethrow caught error as warning.
    warning(ME.message+"\nTo skip the determineVendor function call, set retrieveVendorInfo to false.",[]);
end

if isempty(vendor)
    vendor = "Unknown";
end

end

function [mode,bw,operatingChannel] = determineMode(informationElements)
% DETERMINEMODE determines the 802.11 standard that the AP uses.
% The function checks for the presence of HT, VHT, and HE capability
% elements and determines the 802.11 standard that the AP uses. The element
% IDs are defined in IEEE Std 802.11-2020 and IEEE Std 802.11ax-2021.


elementIDs = cell2mat(informationElements(:,1));
IDs = elementIDs(:,1);

if any(IDs==255)
    if any(elementIDs(IDs==255,2)==35)
        % HE Packet Format
        mode = "802.11ax";
    else
        mode = "Unknown";
    end
    vhtElement = informationElements{IDs==192,2};
    htElement = informationElements{IDs==61,2};
    [bw,operatingChannel] = determineChannelWidth(htElement,vhtElement);
elseif any(IDs==191)
    % VHT Packet Format
    mode = "802.11ac";
    vhtElement = informationElements{IDs==192,2};
    htElement = informationElements{IDs==61,2};
    [bw,operatingChannel] = determineChannelWidth(htElement,vhtElement);
elseif any(IDs==45)
    % HT Packet Format
    mode = "802.11n";
    htElement = informationElements{IDs==61,2};
    [bw,operatingChannel] = determineChannelWidth(htElement);
else
    % Non-HT Packet Format
    % Exclude b as only DSSS is supported
    mode ="802.11a/g/j/p";
    bw = "Unknown";
    operatingChannel = [];
end

end

function [bw,operatingChannel] = determineChannelWidth(htElement,varargin)
% DETERMINECHANNELWIDTH returns the bandwidth of the channel from the
% beacons HT/VHT operation information elements as defined in IEEE Std 802.11-2020
% Section 9.4.2.56 and Section 9.4.2.158.

msbFirst = false;

% IEEE Std 802.11-2020 Figure 9-382 and Table 9-190 define each bit in
% htOperationInfoBits
% Convert to bits to get STA channel width value in 3rd bit.
htOperationInfoBits = int2bit(htElement(2),5*8,msbFirst);
operatingChannel = 0;

if nargin == 2
    % IEEE Std 802.11-2020 Figure 9-163 and Table 9-274 define each octet
    % in vhtElement
    vhtElement = varargin{1};

    % VHT Operation Channel Width Field
    CW = vhtElement(1);
    % Channel Center Frequency Segment 0
    CCFS0 = vhtElement(2);
    % Channel Center Frequency Segment 1
    CCFS1 = vhtElement(3);

    % IEEE Std 802.11-2020 Table 11-23 defines the logic below
    if htOperationInfoBits(3) == 0
        bw = "20";
        operatingChannel = CCFS0;
    elseif CW == 0
        % HT Operation Channel Width Field is 1
        bw = "40";
        operatingChannel = CCFS0;
    elseif CCFS1 == 0
        % HT Operation Channel Width Field is 1 and
        % VHT Operation Channel Width Field is 1
        bw = "80";
        operatingChannel = CCFS0;
    elseif abs(CCFS1 - CCFS0) == 8
        % HT Operation Channel Width Field is 1 and
        % VHT Operation Channel Width Field is 1 and
        % CCFS1 is greater than 0
        bw = "160";
        operatingChannel = CCFS1;
    else
        % HT Operation Channel Width Field is 1 and
        % VHT Operation Channel Width Field is 1 and
        % CCFS1 is greater than 0 and
        % |CCFS1 - CCFS0| is greater than 16
        bw = "80+80";
    end
end

if operatingChannel == 0
    if htOperationInfoBits(3) == 1
        bw = "40";
        secondaryChannelOffset = bit2int(htOperationInfoBits(1:2),2,false);
        if secondaryChannelOffset == 1
            % Secondary Channel is above the primary channel.
            operatingChannel = htElement(1) + 2;
        elseif secondaryChannelOffset == 3
            % Secondary Channel is below the primary channel.
            operatingChannel = htElement(1) - 2;
        else
            warning("Could not determine operating channel.")
        end

    else
        bw = "20";
        operatingChannel = htElement(1);
    end
end

end

function tbl = generateBeaconTable(APs,band)
% GENERATEBEACONTABLE converts the access point structure to a table and
% cleans up the variable names.

tbl = struct2table(APs,"AsArray",true);
tbl.Band = repmat(band,length(tbl.SSID),1);
tbl = renamevars(tbl,["SNR_dB","Beacon_Channel","Operating_Channel","Channel_Width_MHz"], ...
    ["SNR (dB)","Primary 20 MHz Channel","Current Channel Center Frequency Index", ...
    "Channel Width (MHz)"]);
tbl = tbl(:,1:9);

end