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:
- live monitoring of 2400–2500 MHz
- full-band spectrum sensing
- Wi-Fi channel energy analysis (channels 1–13)
- channel-center candidate detection
- temporal candidate stabilization
- time-frequency heatmap
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
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)
| Module | Responsibility |
|---|---|
main.py | Menu / command-line entry point; calls the scripts below |
src/config.py | All settings (connection, receiver, sweep, detection, plotting) |
src/pluto_receiver.py | PlutoReceiver: connect, configure, receive IQ, verify gain. No DSP |
src/spectrum.py | Hann window, FFT, linear power, average / max hold, valid-bin mask |
src/sweep.py | SpectrumSweeper (LO steps) and stitch_segments() onto a 5 kHz grid |
src/channel_detector.py | Noise floor, energy occupancy, center candidates, temporal tracker |
src/heatmap.py | SpectrumHistory, occupancy records, NPZ/CSV history files |
src/utils.py | Figure 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)
python3python3-pippython3-venvlibiio-utilsusbutils
Python (requirements.txt)
pyadi-iio(installs thepylibiio/iiobindings)numpymatplotlib
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 Ubuntuwsl --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.
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.
- Plug in the ADALM-PLUTO and start Ubuntu (keep its terminal open).
- List USB devices and find the ADALM-PLUTO. Its VID:PID is normally
0456:b673. Note the BUSID in the first column (for example2-3).
PowerShell (Administrator)usbipd list - Share the device (needed once per device and USB port):
usbipd bind --busid <BUSID> - Attach it to WSL (Ubuntu must be running):
usbipd attach --wsl --busid <BUSID> - Confirm the device is visible.
Ubuntu
Expected line (bus and device numbers vary):lsusb
IfBus 001 Device 002: ID 0456:b673 Analog Devices, Inc. LibIIO based AD9363 Software Defined Radio [ADALM-PLUTO]lsusbis 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
Ubuntusudo apt update
sudo apt install -y \
python3 \
python3-pip \
python3-venv \
libiio-utils \
usbutils
| Package | Purpose |
|---|---|
python3 | Python runtime |
python3-pip | Python package installer |
python3-venv | Virtual environment support |
libiio-utils | libiio library and tools (iio_info) to discover and talk to IIO devices such as the Pluto |
usbutils | Provides lsusb |
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
Ubuntulsusb
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).
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_Sensingpython3 -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) activepython -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) activepython -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
============================================
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
| Option | What it does | Needs the Pluto |
|---|---|---|
| 1. Test Pluto connection | Connects and receives one IQ buffer | yes |
| 2. Single full-band sweep | One 2400–2500 MHz sweep; saves full_band_*.png and full_band_spectrum.npz | yes |
| 3. Analyze saved spectrum | Channel table, full_band_channels.png, channel_occupancy.csv from the last sweep (run option 2 first) | no |
| 4. Start live monitoring | 30 repeated sweeps: live spectrum, heatmap, instant and stable candidates, history files. Ctrl+C stops early and saves | yes |
| 5. Exit |
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/
| File | Created by | Description |
|---|---|---|
results/figures/live_spectrum.png | live | Latest stitched full-band spectrum, noise floor, instant (dashed orange) and stable (solid purple) candidates; overwritten after every sweep |
results/figures/live_heatmap.png | live | Time-frequency activity heatmap (frequency left→right, oldest sweep at top, color = relative power in dB); overwritten after every sweep |
results/figures/full_band_spectrum.png | sweep | Single stitched 2400–2500 MHz average spectrum |
results/figures/full_band_average_maxhold.png | sweep | Stitched average and max hold |
results/figures/full_band_segments.png | sweep | Valid part of each LO segment before stitching (diagnostic) |
results/figures/full_band_channels.png | analyze | Channel energy / candidate analysis: spectrum, noise floor, energy status per channel, center candidates |
results/figures/channel6_*.png | test_spectrum | Single-channel diagnostic figures |
Processed data — data/processed/
| File | Created by | Description |
|---|---|---|
data/processed/full_band_spectrum.npz | sweep | Numerical stitched spectrum: frequencies, average and max hold (linear and dB), capture settings |
data/processed/channel_occupancy.csv | analyze | Per-channel metrics: powers, excess, active fraction, energy status, center score, candidate flag |
data/processed/spectrum_history.npz | live | Spectrum history for repeated sweeps: power_history_db (sweeps × 20000 bins), UTC timestamps, noise floor per sweep, per-channel matrices, settings |
data/processed/occupancy_history.csv | live | Channel 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
- Capture and FFT: 20 MS/s, FFT size 16384; per buffer the DC offset is removed, a Hann window applied and the power computed. 20 captures per LO center (after 2 discarded buffers) are averaged in linear power; max hold keeps the per-bin maximum.
- Sweep: LO centers 2408.5, 2416.5, 2433.5, 2441.5, 2458.5, 2466.5, 2483.5 and 2491.5 MHz with fixed manual RX gain.
- Masking: ±0.2 MHz around each LO and 1 MHz at each capture edge are excluded; the paired centers fill each other's excluded regions.
- Stitching: valid bins are averaged in linear power into 5 kHz cells over 2400–2500 MHz; overlapping segments are averaged (max hold: maximum).
- Noise floor: 20th percentile of the stitched spectrum.
- Energy occupancy: a channel window (center ±10 MHz) is energy-occupied if its median is ≥ 5 dB above the noise floor or ≥ 20 % of its bins are ≥ 6 dB above it.
- Center candidate:
score = max(core median − floor, 0) × core active fractionover center ±8 MHz; local maximum, score ≥ 2.5, 15 MHz separation. - Stable candidate (temporal smoothing): scores averaged over the last 5 sweeps, same rules, supported (score ≥ 2.5) in at least 3 of them.
- Heatmap: each completed sweep becomes one row of the time-frequency history.
All thresholds are in src/config.py.
Important Scientific Limitations
- 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.
- No simultaneous observation. The ADALM-PLUTO does not observe the entire 100 MHz band simultaneously; one capture covers about 20 MHz.
- 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.
- Bursty traffic. Wi-Fi is bursty, so RF activity may change during a single sweep, and overlapping segments may show different levels.
- 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.
- No packet decoding. No IEEE 802.11 packets are decoded.
- 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.
- 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: lsusb → iio_info -S usb → Python imports →
python -m scripts.test_pluto. The first failing check shows where the problem is.
| Problem | Likely cause | Solution |
|---|---|---|
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 |
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.
- Start Ubuntu.
- PowerShell (Administrator)
If the device is listed as Not shared (e.g. after using a different USB port), bind it first:usbipd list usbipd attach --wsl --busid <BUSID>usbipd bind --busid <BUSID> - Ubuntu
If the URI in square brackets changed, updatelsusb iio_info -S usbPLUTO_URIinsrc/config.py. - 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