Main Content

OFDM WiFi Scanner Using SDR Preamble Detection

This example shows how to retrieve information about WiFi networks using a software-defined radio (SDR) and preamble detection. The example scans over the 2.4 GHz and 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.


This example scans through a set of WiFi 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 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 function. The function returns all available radio setup configurations that you saved using the Radio Setup wizard. For more information, see Connect and Set Up NI USRP Radios.

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. To speed up the execution time of this example upon subsequent runs, reuse the workspace object from the first run of the example.

clear pd
pd = preambleDetector(radio);

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 WiFi 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 function. For more information on tuning these values, see Triggered Capture Using Preamble Detection.

For tuning the preamble detector, specify a frequency band and channel with a known OFDM packet.

These are the valid channel numbers.

  • 1–14 for the 2.4 GHz band.

  • 1–200 for the 5 GHz band. However, the valid 20 MHz control channels for APs using 5 GHz 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 = 52;

pd.CenterFrequency = wlanChannelFrequency(channel,band);

Adjust these values for tuning the preamble detector.

pd.AdaptiveThresholdGain = 0.37;
pd.AdaptiveThresholdOffset = 0.00004;
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);

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 WiFi Channels

Specify Scanning Region

Specify the frequency band and the channels for the SDR to scan.

band = 5;
channels = [52 56 157];

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 ms, set captureLength to milliseconds(100) and timeout to milliseconds(200).

captureLength = milliseconds(100);
timeout = milliseconds(200);

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

  • SSID


  • 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",[], ...

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);
        fprintf("Non-HT packet detected on channel %d in band %.1f.\n",channels(i),band)
    % 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")

        % 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), ...

        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;
                    cfg.MCS = mod(rate,6);

                % 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))> ...
                    % Exit while loop as full packet not captured.

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

                % 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";
                        APs(counter).SSID = string(cfgMAC.ManagementConfig.SSID);

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

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

                    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;
                % Shift packet search offset for next iteration of while loop.
                searchOffset = res.PacketOffset + double(ind.NonHTData(2));
                % 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;
            % 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;
Scanning channel 52 on band 5.0.
Non-HT packet detected on channel 52 in band 5.0.
Beacon detected on channel 52 in band 5.0.
Downloading oui.csv from IEEE Registration Authority...
Scanning channel 56 on band 5.0.
No non-HT packet detected on channel 56 in band 5.0.
Scanning channel 157 on band 5.0.
No non-HT packet detected on channel 157 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=1×9 table
      SSID           BSSID                 Vendor             SNR (dB)    Primary 20 MHz Channel    Current Channel Center Frequency Index    Channel Width (MHz)    Band       Mode   
    _________    ______________    _______________________    ________    ______________________    ______________________________________    ___________________    ____    __________

    "WLAN_5G"    "04D4C451C584"    "ASUSTek COMPUTER INC."     18.796               52                                58                             "80"             5      "802.11ax"

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);
    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);
        ouis = readtable("oui.csv",VariableNamingRule="preserve");

    % 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.",[]);

if isempty(vendor)
    vendor = "Unknown";


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";
        mode = "Unknown";
    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);
    % Non-HT Packet Format
    % Exclude b as only DSSS is supported
    mode ="802.11a/g/j/p";
    bw = "Unknown";
    operatingChannel = [];


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 and Section

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;
        % 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";

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;
            warning("Could not determine operating channel.")

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


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);


See Also



Related Topics