README.md is the primary documentation rendered directly by GitHub. This HTML file is provided as a standalone formatted version.

Project Overview

This is Part 1 of a university software-defined radio (SDR) project. The system performs online, energy-based monitoring of the complete 2400–2500 MHz (2.4 GHz ISM) band with an ADALM-PLUTO SDR:

Receive-only. This software performs no RF transmission. The ADALM-PLUTO is used exclusively as a receiver; no transmit functionality is configured or implemented.

Main Features

ADALM-PLUTO RXpyadi-iio over USB, fixed manual gain (30 dB)
IQ capture20 MS/s, 16384 complex samples per buffer
FFTDC removal, Hann window, relative power in dB
Average Spectrum20 captures per LO step, averaged in linear power
Max HoldPer-bin maximum over the same captures
Full 2400–2500 MHz sweep8 overlapping LO steps, 100 % coverage
Spectrum stitchingDC and edge exclusion, 5 kHz common grid
Wi-Fi energy occupancyNoise-floor based, per 20 MHz channel window
Center candidate detectionCenter score, local maxima, non-maximum suppression
Temporal stable candidateScores averaged over the last 5 sweeps
Live spectrumUpdated after every sweep
HeatmapTime-frequency activity over repeated sweeps
CSV / NPZ loggingUTC timestamps; Ctrl+C always saves completed sweeps

System Architecture

ADALM-PLUTO (USB, receive-only)          src/pluto_receiver.py
     ↓
IQ Samples
     ↓
FFT / Relative Power                     src/spectrum.py
     ↓
Overlapping Frequency Sweep              src/sweep.py
     ↓
2400–2500 MHz Spectrum                   (stitched, linear power + dB)
     ↓
 ┌───────────────┬──────────────────┐
 ↓               ↓                  ↓
Channel       Temporal           Heatmap
Analysis      Candidates         History
(src/channel_detector.py)        (src/heatmap.py)
ModuleResponsibility
main.pyMenu / command-line entry point; calls the scripts below
src/config.pyAll settings (connection, receiver, sweep, detection, plotting)
src/pluto_receiver.pyPlutoReceiver: connect, configure, receive IQ, verify gain. No DSP
src/spectrum.pyHann window, FFT, linear power, average / max hold, valid-bin mask
src/sweep.pySpectrumSweeper (LO steps) and stitch_segments() onto a 5 kHz grid
src/channel_detector.pyNoise floor, energy occupancy, center candidates, temporal tracker
src/heatmap.pySpectrumHistory, occupancy records, NPZ/CSV history files
src/utils.pyFigure saving and plot-window helpers
scripts/Stand-alone test and monitoring programs (also used by main.py)

Project Structure

WiFi_Channel_Sensing/
│
├── main.py                       Entry point (menu and --mode)
├── README.md                     Primary documentation (GitHub)
├── README.html                   This standalone documentation page
├── requirements.txt              pyadi-iio, numpy, matplotlib
├── .gitignore
│
├── src/
│   ├── __init__.py
│   ├── config.py                 All settings (including PLUTO_URI)
│   ├── pluto_receiver.py         Hardware access (receive-only)
│   ├── spectrum.py               FFT / power / average / max hold / masks
│   ├── sweep.py                  LO sweep and stitching
│   ├── channel_detector.py       Energy occupancy, center and stable candidates
│   ├── heatmap.py                Spectrum and occupancy history
│   └── utils.py                  Figure helpers
│
├── scripts/
│   ├── __init__.py               Makes `python -m scripts.<name>` reliable
│   ├── test_pluto.py
│   ├── test_spectrum.py
│   ├── test_full_sweep.py
│   ├── test_channel_detection.py
│   └── live_monitor.py
│
├── data/
│   ├── raw/
│   └── processed/                NPZ and CSV results (generated)
│
└── results/
    ├── figures/                  PNG figures (generated)
    └── logs/

Requirements

Windows

  • Windows 10 or 11 (64-bit)
  • WSL2
  • Ubuntu under WSL
  • usbipd-win

Linux / WSL (apt)

  • python3
  • python3-pip
  • python3-venv
  • libiio-utils
  • usbutils

Python (requirements.txt)

  • pyadi-iio (installs the pylibiio / iio bindings)
  • numpy
  • matplotlib

Hardware

  • ADALM-PLUTO SDR
  • USB data cable

libiio is a C library installed with apt; pip cannot install it. pyadi-iio loads it through its pylibiio bindings, so both are needed.

Setup on a New Computer

Follow steps A–H once per computer. Nothing is assumed to be installed. PowerShell (Administrator) commands run on Windows; Ubuntu commands run inside the WSL Ubuntu terminal.

A. Windows requirements

You need Windows 10/11, WSL2, Ubuntu under WSL, usbipd-win and an ADALM-PLUTO connected by USB (use a data cable).

PowerShell (Administrator) — install WSL2 and Ubuntu
wsl --install
PowerShell (Administrator) — if WSL is already installed
wsl --update

After a first-time WSL installation restart Windows. Then open Ubuntu from the Start menu and create a Linux user name and password when asked.

PowerShell (Administrator) — install usbipd-win
winget install --interactive --exact dorssel.usbipd-win

Close and reopen PowerShell afterwards so the usbipd command is found. Open PowerShell as Administrator (right-click → Run as administrator) whenever you share USB devices with WSL.

B. Connect the ADALM-PLUTO to WSL

WSL does not see USB devices plugged into Windows automatically; they must be attached with usbipd.

  1. Plug in the ADALM-PLUTO and start Ubuntu (keep its terminal open).
  2. List USB devices and find the ADALM-PLUTO. Its VID:PID is normally 0456:b673. Note the BUSID in the first column (for example 2-3).
    PowerShell (Administrator)
    usbipd list
  3. Share the device (needed once per device and USB port):
    usbipd bind --busid <BUSID>
  4. Attach it to WSL (Ubuntu must be running):
    usbipd attach --wsl --busid <BUSID>
  5. Confirm the device is visible.
    Ubuntu
    lsusb
    Expected line (bus and device numbers vary):
    Bus 001 Device 002: ID 0456:b673 Analog Devices, Inc. LibIIO based AD9363 Software Defined Radio [ADALM-PLUTO]
    If lsusb is not found yet, install the packages in step C first.

<BUSID> is machine-specific. Never copy another computer's BUSID — always read it from usbipd list. While attached, Windows itself cannot use the device.

C. Ubuntu system packages

Ubuntu
sudo apt update

sudo apt install -y \
    python3 \
    python3-pip \
    python3-venv \
    libiio-utils \
    usbutils
PackagePurpose
python3Python runtime
python3-pipPython package installer
python3-venvVirtual environment support
libiio-utilslibiio library and tools (iio_info) to discover and talk to IIO devices such as the Pluto
usbutilsProvides lsusb
Ubuntu — optional
sudo apt install -y git          # only if `git --version` fails (needed in step E)
sudo apt install -y python3-tk   # plot windows through WSLg; PNG files are saved either way

D. Verify the Pluto from WSL

Ubuntu
lsusb
iio_info -S usb

iio_info -S usb should list an ADALM-PLUTO context with its URI in square brackets, for example:

Available contexts:
	0: 0456:b673 (Analog Devices Inc. PlutoSDR (ADALM-PLUTO)), serial=... [usb:1.2.5]

Configure this URI in src/config.py — see Pluto URI. If the Pluto appears only with sudo iio_info -S usb, see Troubleshooting.

E. Clone the repository

Keep the project inside the Linux file system (not under /mnt/c).

Ubuntu
cd ~
git clone <REPOSITORY_URL>
cd WiFi_Channel_Sensing

Replace <REPOSITORY_URL> with the URL of this repository.

F. Create the Python virtual environment

Ubuntu, inside ~/WiFi_Channel_Sensing
python3 -m venv .venv

source .venv/bin/activate

python -m pip install --upgrade pip

python -m pip install -r requirements.txt

After activation the prompt normally begins with (.venv). .venv is not part of the repository, so it must be created on every new computer. requirements.txt installs pyadi-iio (which also installs the pylibiio bindings, the iio module), numpy and matplotlib. In VS Code (WSL extension) select .venv/bin/python as the interpreter.

G. Verify the Python dependencies

Ubuntu, with (.venv) active
python -c "import iio; print('iio OK')"

python -c "import adi; print('pyadi-iio OK')"

If both print OK, the Python environment is ready.

H. First hardware test

Ubuntu, with (.venv) active
python -m scripts.test_pluto

Expected type of result (sample values differ every time):

Connecting to ADALM-PLUTO...
URI: usb:...
Connected successfully.
Configured: LO 2437.000 MHz, 20.0 MS/s, buffer 16384
Samples received: 16384
First 5 IQ samples:
[...]

If this fails, do not continue to spectrum monitoring — fix the connection first (see Troubleshooting).

Pluto URI

The BUSID and the USB URI are MACHINE-SPECIFIC. usb:1.2.5 is only an example. The URI is different on other computers and can change after reconnecting the device.

Run the following command and use the URI shown in square brackets on your machine:

iio_info -S usb

The program connects to the URI configured in src/config.py:

PLUTO_URI = "usb:..."

Set it to the discovered URI, e.g. PLUTO_URI = "usb:1.2.5". (PLUTO_URI = "usb:" selects the only connected USB Pluto automatically and does not need updating after reconnects.)

Running the Project

Always run from the project root with the virtual environment active.

Main program

python main.py
============================================
 ADALM-PLUTO 2.4 GHz Wi-Fi Spectrum Monitor
============================================

1. Test Pluto connection
2. Single full-band sweep
3. Analyze saved spectrum
4. Start live monitoring
5. Exit
OptionWhat it doesNeeds the Pluto
1. Test Pluto connectionConnects and receives one IQ bufferyes
2. Single full-band sweepOne 2400–2500 MHz sweep; saves full_band_*.png and full_band_spectrum.npzyes
3. Analyze saved spectrumChannel table, full_band_channels.png, channel_occupancy.csv from the last sweep (run option 2 first)no
4. Start live monitoring30 repeated sweeps: live spectrum, heatmap, instant and stable candidates, history files. Ctrl+C stops early and savesyes
5. Exit
The same actions without the menu
python main.py --mode test
python main.py --mode sweep
python main.py --mode analyze
python main.py --mode live

During live monitoring each sweep prints a short block:

Sweep 5/30   (1.09 s, spectrum + heatmap updated)
  Noise floor:       -38.3 dB
  Energy windows:    [4, 5, 6, 7, 8, 9]
  Instant candidate: [7]
  Stable candidate:  [6]   (last 5)

The number of sweeps and all other settings are in src/config.py.

Developer commands

Stand-alone scripts (the menu uses the same code):

python -m scripts.test_pluto               # connection and IQ samples
python -m scripts.test_spectrum            # channel 6: FFT, average, max hold, masks
python -m scripts.test_full_sweep          # full-band sweep with detailed log
python -m scripts.test_channel_detection   # channel analysis of the saved sweep
python -m scripts.live_monitor             # live monitoring

Generated Output

The programs create these files at runtime. They are ignored by Git (.gitignore), so a freshly cloned repository contains only empty folders.

Figures — results/figures/

FileCreated byDescription
results/figures/live_spectrum.pngliveLatest stitched full-band spectrum, noise floor, instant (dashed orange) and stable (solid purple) candidates; overwritten after every sweep
results/figures/live_heatmap.pngliveTime-frequency activity heatmap (frequency left→right, oldest sweep at top, color = relative power in dB); overwritten after every sweep
results/figures/full_band_spectrum.pngsweepSingle stitched 2400–2500 MHz average spectrum
results/figures/full_band_average_maxhold.pngsweepStitched average and max hold
results/figures/full_band_segments.pngsweepValid part of each LO segment before stitching (diagnostic)
results/figures/full_band_channels.pnganalyzeChannel energy / candidate analysis: spectrum, noise floor, energy status per channel, center candidates
results/figures/channel6_*.pngtest_spectrumSingle-channel diagnostic figures

Processed data — data/processed/

FileCreated byDescription
data/processed/full_band_spectrum.npzsweepNumerical stitched spectrum: frequencies, average and max hold (linear and dB), capture settings
data/processed/channel_occupancy.csvanalyzePer-channel metrics: powers, excess, active fraction, energy status, center score, candidate flag
data/processed/spectrum_history.npzliveSpectrum history for repeated sweeps: power_history_db (sweeps × 20000 bins), UTC timestamps, noise floor per sweep, per-channel matrices, settings
data/processed/occupancy_history.csvliveChannel history across live monitoring: timestamp, noise floor, energy-occupied channels, instant and stable candidates per sweep

Without a plot window (normal under WSL without python3-tk), open the PNGs from Windows, e.g. explorer.exe results/figures. The live PNG and CSV files are rewritten after every sweep and can be viewed while monitoring runs.

Technical Method

IQ capture → Hann window → FFT → Power → averaging
  → overlapping LO sweep → invalid-edge / DC masking → linear-power stitching
  → channel analysis → temporal smoothing → heatmap

All thresholds are in src/config.py.

Important Scientific Limitations

  1. Relative power only. Power is shown in relative dB, not calibrated dBm. The ADALM-PLUTO is not amplitude-calibrated; only differences between frequencies and sweeps with the same settings are meaningful.
  2. No simultaneous observation. The ADALM-PLUTO does not observe the entire 100 MHz band simultaneously; one capture covers about 20 MHz.
  3. Sequential sweeps. The 2400–2500 MHz band is measured using sequential, overlapping LO sweeps (about 1.1 s per sweep in the current configuration) that are stitched together. Different frequencies within one spectrum or heatmap row were measured at slightly different times.
  4. Bursty traffic. Wi-Fi is bursty, so RF activity may change during a single sweep, and overlapping segments may show different levels.
  5. Energy-based detection. Occupancy is detected from RF energy only. Bluetooth, microwave ovens, other ISM devices or receiver spurs in the same frequency range are detected in the same way. Because 2.4 GHz channel windows overlap (5 MHz spacing, about 20 MHz width), one signal can make several neighbouring channel windows energy-occupied.
  6. No packet decoding. No IEEE 802.11 packets are decoded.
  7. Center candidates are interpretations. A center candidate means that the broadband energy is consistent with a signal centered near a standard Wi-Fi channel frequency, not that an access point or its identity has been proven. Because 2.4 GHz channels overlap, networks closer than about 15 MHz may merge into one candidate, and energy centered between two channels can make the candidate alternate; temporal averaging reduces but does not remove this.
  8. Validation pending. Controlled validation using a router fixed to known channels has not yet been performed and remains an optional future validation experiment. Detection thresholds are initial experimental values.

Troubleshooting

Work through the checks in order: lsusbiio_info -S usb → Python imports → python -m scripts.test_pluto. The first failing check shows where the problem is.

ProblemLikely causeSolution
usbipd not found (PowerShell) usbipd-win not installed, or PowerShell opened before installing it Install usbipd-win (step A) and open a new PowerShell as Administrator
lsusb does not show the Pluto The USB device is not attached to WSL usbipd list, then usbipd attach --wsl --busid <BUSID>. The device must show Attached; Ubuntu must be running. If still missing, try another USB port or cable, then bind and attach again
lsusb shows the Pluto but iio_info -S usb does not libiio not installed, or no USB permission sudo apt install libiio-utils. If it works only with sudo iio_info -S usb, add the udev rule below and re-attach the device
import iio fails Virtual environment not active or requirements not installed Check the prompt starts with (.venv); otherwise source .venv/bin/activate, then python -m pip install -r requirements.txt. If the error mentions libiio, install libiio-utils (step C)
import adi fails pyadi-iio not installed in the active environment source .venv/bin/activate, then python -m pip install -r requirements.txt
Connection to the Pluto URI fails ("Could not connect ... No device found") PLUTO_URI does not match the device Run iio_info -S usb and set PLUTO_URI in src/config.py to the URI in square brackets
Matplotlib does not open a window No GUI backend under WSL — not an error All figures are still saved as PNG files in results/figures/. Optional plot windows through WSLg: sudo apt install python3-tk
Permission or connection problems after reconnecting the Pluto Device detached, re-enumerated or URI changed usbipd detach --busid <BUSID>, then usbipd attach --wsl --busid <BUSID>; check lsusb and iio_info -S usb; update PLUTO_URI if the URI changed. "Sweep failed ... No such device" during monitoring means the Pluto was disconnected; completed sweeps are still saved
Ubuntu — udev rule for USB permission
echo 'SUBSYSTEM=="usb", ATTRS{idVendor}=="0456", ATTRS{idProduct}=="b673", MODE="0666"' \
    | sudo tee /etc/udev/rules/53-adi-plutosdr-usb.rules
sudo udevadm control --reload-rules
PowerShell (Administrator) — detach and re-attach
usbipd detach --busid <BUSID>
usbipd attach --wsl --busid <BUSID>

Starting the Project Again Later

After rebooting Windows, restarting WSL (wsl --shutdown) or unplugging the ADALM-PLUTO, the device must be attached to WSL again.

  1. Start Ubuntu.
  2. PowerShell (Administrator)
    usbipd list
    usbipd attach --wsl --busid <BUSID>
    If the device is listed as Not shared (e.g. after using a different USB port), bind it first:
    usbipd bind --busid <BUSID>
  3. Ubuntu
    lsusb
    iio_info -S usb
    If the URI in square brackets changed, update PLUTO_URI in src/config.py.
  4. Start the program:
    cd ~/WiFi_Channel_Sensing
    source .venv/bin/activate
    python main.py

Quick Start

For a computer where Setup on a New Computer has already been completed.

Windows PowerShell (Administrator)
usbipd list
usbipd bind --busid <BUSID>          # only if the Pluto is not shared yet
usbipd attach --wsl --busid <BUSID>
Ubuntu
lsusb
iio_info -S usb                      # update PLUTO_URI in src/config.py if the URI changed

cd ~/WiFi_Channel_Sensing
source .venv/bin/activate

python -m scripts.test_pluto
python main.py

Freshly cloned repository? .venv does not exist yet. Create it once before source .venv/bin/activate:

python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt