RTL-SDR Sweeper & Listener Tutorial

1. Hardware Prerequisites

  • 1x Raspberry Pi (recommended Pi4 or Pi5, at least 8 Gb)
  • 1-2 RTL-SDR Dongles (v3)
  • Antenna (e.g. a simple monopole or dipole)

2. Raspberry Pi Setup

  • Install the default Raspberry Pi OS (Debian Trixie)
  • Install RTL-SDR driver &rtl_power
    Go into the command and type:
				
					sudo apt upgrade
sudo apt install cmake
sudo apt install librtlsdr-dev
sudo apt install -y cmake libad9361-dev libairspy-dev libairspyhf-dev libfftw3-dev libglfw3-dev libhackrf-dev libiio-dev librtaudio-dev libvolk2-dev libzstd-dev

				
			

3. Python script to sweep directly and show the plots locally
Consider creating a reference sweep first (see next step), but you can also create an empty reference file.

				
					#!/usr/bin/env python3
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sys
from datetime import datetime
import subprocess
import os

csvfile = "/home/x/Desktop/sweep.csv"
reference_file = "/home/x/Desktop/reference.csv"

#Do the sweep through the bash command
# Build the rtl_power command
rtl_cmd = [
    "rtl_power",
    "-f", "50M:1760M:125k", #startfreq, stopfreq, bandwidth eg 125k
    "-i", "0.5",   # integration interval 0.5s
    "-g", "30", # gain
    "-1", #fast sweep
    csvfile
]

# Run rtl_power
print("Starting sweep...")
subprocess.run(rtl_cmd)
print(f"Sweep finished. Results in {csvfile}")

# Read first line to get header info (some rtl_power versions have no header)
# Use pandas to parse; skip initial bad rows if present
df = pd.read_csv(csvfile, header=None)

# Extract all low/high ranges and flatten the data
freqs = []
powers = []

for i in range(len(df)):
    lowHz = df.iloc[i, 2]
    highHz = df.iloc[i, 3]
    stepHz = df.iloc[i, 4]
    pwr = df.iloc[i, 6:].astype(float).values.flatten()
    # build freq axis for this row
    f = lowHz + np.arange(len(pwr)) * stepHz
    freqs.extend(f)
    powers.extend(pwr)

freqs = np.array(freqs) / 1e6  # MHz
powers = np.array(powers)

# Sort by frequency (just in case)
idx = np.argsort(freqs)
freqs = freqs[idx]
powers = powers[idx]

if os.path.exists(reference_file):
    ref_df = pd.read_csv(reference_file, sep=",")
    
    # Compute center frequencies (in MHz)
    ref_freqs = ((ref_df["start"] + ref_df["end"]) / 2) / 1e6
    ref_power = ref_df["median"].values

    # Interpolate reference powers to match current sweep frequencies
    ref_interp = np.interp(freqs, ref_freqs, ref_power)

    # Compute difference (current - reference)
    diff = powers - ref_interp

    # Plot reference vs current
    plt.figure(figsize=(12,5))         
    plt.plot(freqs, powers, color="red", label="Current Sweep", linewidth=1.2, alpha=0.7)
    plt.plot(ref_freqs, ref_power, color="black", label="Reference Sweep", linewidth=2)
    plt.xlabel("Frequency (MHz)")
    plt.ylabel("Power (dB)")
    plt.title("Current vs Reference Spectrum")
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()

    # Plot difference (ΔdB)
    plt.figure(figsize=(12,4))
    plt.plot(freqs, diff, color="red", linewidth=1)
    plt.axhline(0, color="black", linestyle="--", linewidth=0.8)
    plt.xlabel("Frequency (MHz)")
    plt.ylabel("Power Difference (dB)")
    plt.title("Difference: Current Sweep − Reference Sweep")
    plt.grid(True)
    plt.tight_layout()
    plt.show()

    # Optional: print significant deviations
    threshold = 5  # dB
    sig_idx = np.where(diff > threshold)[0]
    if len(sig_idx) > 0:
        print("\nSignificant deviations (> +5 dB):")
        for i in sig_idx:
            print(f"{freqs[i]:8.3f} MHz  Δ = {diff[i]:6.2f} dB")
    else:
        print("\nNo significant deviations from reference.")
else:
    print(f"\n Reference file not found: {reference_file}")

				
			

4. Create a Reference sweep
Collect a bunch of sweeps throughout the time and calculate the median value for each frequency step. Then name it sweep_reference.csv. This way you can easily identify real signals and eliminate permanent ones, like radio broadcasting stations.

5. Python script to sweep regularly and save results as csv

				
					#!/usr/bin/env python3
import sys
import time
from datetime import datetime, timedelta, timezone
import subprocess
import os
import requests
import base64


RESULTS_DIR = "/home/x/Desktop/results/"

def rtl_sdr_sweep():
    """Perform a frequency sweep and save it as sweep_<HHmm>.csv"""
    now = datetime.now(timezone.utc)
    hour = now.hour
    minute = now.minute
    csvfile = os.path.join(RESULTS_DIR, f"sweep_{hour:02d}{minute:02d}.csv")
    #Do the sweep through the bash command
    # Build the rtl_power command
    rtl_cmd = [
        "rtl_power",
        "-f", "50M:1760M:125k", #startfreq, stopfreq, bandwidth eg 125k
        "-i", "1",
        "-g", "30", # gain
        "-e", "10m", #stop sweeping after 10 min
        csvfile
    ]

    # Run rtl_power
    subprocess.run(rtl_cmd)
    print(f"Sweep finished. Results in {csvfile}")
    
    return csvfile

try:
    csvfile = rtl_sdr_sweep()
    #you could then also upload each csv file through a function here. I use a function to upload them to my server on guerillamap.com

except Exception as e:
    print(f"An error occurred: {e}")

				
			

6. HTML for visualization

I use the csv’s previously uploaded to guerillamap.com – change accordingly to local files or upload them to your own server. Of course we can also arrange an upload to guerillamap and display your station on guerillamap.com. Just write us a mail to info@guerillamap.com

				
					<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Frequency Sweeper</title>

<style>
  body {
    font-family: Arial, sans-serif;
    margin: 20px;
    background: #fafafa;
  }
  h1 { text-align: center; }
  #controls {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
    margin-bottom: 20px;
  }
  button {
    padding: 6px 10px;
    border-radius: 8px;
    border: 1px solid #ccc;
    background: white;
    cursor: pointer;
  }
  button:hover { background: #eee; }
  button.active {
    background: #007bff;
    color: white;
    border-color: #007bff;
  }
  #plots {
    max-width: 80wv;
    margin: 10px;
	position: relative;
	padding-bottom: 20px;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 25px;
    font-size: 14px;
  }
  th, td {
    border: 1px solid #ccc;
    padding: 6px;
    text-align: center;
  }
  th { background-color: #eee; }
  
  #alertHeader {margin-top: 20px;}
  #peaksHeader {margin-top: 20px;}
  
  #waterfall {
  display: block;
  margin-bottom: 20px;   
  position: relative;   
}


  
</style>
</head>
<body>

<h1>Frequency Sweeper</h1>
<h2 style="text-align:center;">Choose a Time in UTC to show the Corresponding Sweep</h2>



<div id="controls"></div>

<a href=https://guerillamap.com/sweeper-tutorial target=_blank rel=noopener noreferrer>Set up your own Sweeper</a>

<div id="plots">
  <div id="plot1"></div>
  <div id="plot2"></div>
  <div id="waterfall"></div>
</div>

<h2 id="alertHeader">Alerts</h2>
<table id="droneTable">
  <thead>
    <tr><th>Date/Time</th><th>Frequency Band</th><th>Peak ΔPower (dB)</th></tr>
  </thead>
  <tbody></tbody>
</table>



</body>
</html>

				
			

7. Installation of SDR++, icecast and darkice to listen on the SDR remotely
(If you just want to sweep, without the possibility to actually listen live, then you can skip all the next steps.)
You can only listen in SDR++ as long as your RTL-SDR is not used for sweeping – therefore you might want to attach 2 RTL-SDR dongles to your PI; one for sweeping, the other to listen.

  • Install SDR++
    Go into the command and type:
				
					cd ~/Downloads
wget https://github.com/AlexandreRouma/SDRPlusPlus/archive/refs/heads/master.zip
unzip master.zip
cd ~/Downloads/SDRPlusPlus-master
mkdir build
cd build
cmake ..
make -j4
cd ..
sh ~/Downloads/SDRPlusPlus-master/create_root.sh
cd ~/Downloads/SDRPlusPlus-master/build
sudo make install

				
			
  • Install icecast&darkice
    (If you just want to listen directly on the raspberry Pi, then you can skipt the next steps.)
    Go into the command and type:
				
					sudo apt-get install icecast2
//There will be a mask to choose your stream name and password. Set it to mystream and choose a password
sudo apt-get install darkice
sudo nano /etc/darkice.cfg

				
			
  • Put in the following values:
    [general]

Duration                      = 0

bufferSecs                  = 5

reconnect                    = yes

 

[input]

device              = plughw:2,1

sampleRate                 = 48000

bitsPerSample = 16

channel                       = 2

 

[icecast2-0]

bitrateMode                 = cbr

format                          = mp3

bitrate                          = 128

server                          = 127.0.0.1

port                               = 8000

password                     = *yourpassword*

mountPoint                  = mystream

name                           = My Stream

description                  = My live stream

url                                = http://mywebsite.com

genre                           = myGenre

public                          = no

(Control+X to save)

				
					sudomodprobesnd-aloop
echo snd-aloop | sudo tee -a /etc/modules
arecord -l

				
			
  • Now you should see sth like:

card 1: Loopback [Loopback], device 0: Loopback PCM [Loopback PCM]

Subdevices: 8/8

  • Open SDR++ & set the Audio Output Device to:
    Loopback (Loopback PCM) (The upper one)

8. Listen remotely to your SDR++

  • Start listening on your SDR in SDR++, e.g. select a frequency, adjust gain, badwidth etc.
  • Go into the command and type:
				
					sudosystemctl start icecast2
sudodarkice
				
			
  • Find your Pi’s IP with “hostname –I”
  • Open http://xxx.xxx.x.xxx:8000/mystream (xxx.xxx.x.xxx stands for your Pi’s IP) in your browser, or in a VLC Media Player
    If you are in the same network as the Pi this will work, if you want it to function everywhere, then you have to go into your router’s settings and make the port 8000 a public one.
    • Log into your router’s admin page.
    • Find Port Forwarding / NAT
    • “Server IP Address”→ your Pi’s IP (xxx.xxx.x.xxx:8000).
    • Find your public IP at https://whatismyipaddress.com (This is not the same as your Pi’s IP)
    • Forward external TCP port 8000.
  • Open http://xx.xx.xx.xxx:8000/mystream in your browser or VLC. xx.xx.xx.xxx stands for your public IP (not Pi’s IP)