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>50 - 1760 MHz 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>50 - 1750 MHz Frequency Sweeper</h1>
<p style="text-align:center;">Choose a time in UTC to show the corresponding sweep</p>

<div id="controls"></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>

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

<h2 id="peaksHeader">Detected Peaks (ΔPower &gt; 7 dB)</h2>
<table id="peakTable">
  <thead>
    <tr><th>Date</th><th>Frequency (MHz)</th><th>ΔPower (dB)</th></tr>
  </thead>
  <tbody></tbody>
</table>




<script type='text/javascript' src='https://guerillamap.com/_jb_static/??5c489cdd2b'></script><script src="https://guerillamap.com/wp-includes/js/jquery/jquery.min.js?ver=3.7.1" id="jquery-core-js"></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??9bc8b83d26'></script><script src="https://guerillamap.com/wp-includes/js/dist/i18n.min.js?ver=c26c3dc7bed366793375" id="wp-i18n-js"></script><script id="wp-i18n-js-after">
wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ 'ltr' ] } );
//# sourceURL=wp-i18n-js-after
</script><script id="give-js-extra">
var give_global_vars = {"ajaxurl":"https://guerillamap.com/wp-admin/admin-ajax.php","checkout_nonce":"3cf5f8852e","currency":"USD","currency_sign":"$","currency_pos":"before","thousands_separator":",","decimal_separator":".","no_gateway":"Please select a payment method.","bad_minimum":"The minimum custom donation amount for this form is","bad_maximum":"The maximum custom donation amount for this form is","general_loading":"Loading...","purchase_loading":"Please Wait...","textForOverlayScreen":"\u003Ch3\u003EProcessing...\u003C/h3\u003E\u003Cp\u003EThis will only take a second!\u003C/p\u003E","number_decimals":"2","is_test_mode":"","give_version":"4.13.2","magnific_options":{"main_class":"give-modal","close_on_bg_click":false},"form_translation":{"payment-mode":"Please select payment mode.","give_first":"Please enter your first name.","give_last":"Please enter your last name.","give_email":"Please enter a valid email address.","give_user_login":"Invalid email address or username.","give_user_pass":"Enter a password.","give_user_pass_confirm":"Enter the password confirmation.","give_agree_to_terms":"You must agree to the terms and conditions."},"confirm_email_sent_message":"Please check your email and click on the link to access your complete donation history.","ajax_vars":{"ajaxurl":"https://guerillamap.com/wp-admin/admin-ajax.php","ajaxNonce":"ceb3a3ebd7","loading":"Loading","select_option":"Please select an option","default_gateway":"manual","permalinks":"1","number_decimals":2},"cookie_hash":"015aaca3e19e1e2c7239aae302e8aefc","session_nonce_cookie_name":"wp-give_session_reset_nonce_015aaca3e19e1e2c7239aae302e8aefc","session_cookie_name":"wp-give_session_015aaca3e19e1e2c7239aae302e8aefc","delete_session_nonce_cookie":"0"};
var giveApiSettings = {"root":"https://guerillamap.com/wp-json/give-api/v2/","rest_base":"give-api/v2"};
//# sourceURL=give-js-extra
</script><script src="https://guerillamap.com/wp-content/plugins/give/build/assets/dist/js/give.js?ver=8540f4f50a2032d9c5b5" id="give-js"></script><script id="nfd-wonder-blocks-utilities-js-after">
(()=>{var l=class{constructor({clientId:e,...n}={}){this.options={activeClass:"nfd-wb-animated-in",root:null,rootMargin:"0px",threshold:0,...n}}observeElements(e,n=null,t=!1){if(!("IntersectionObserver"in window)||!e?.length||document.documentElement.classList.contains("block-editor-block-preview__content-iframe"))return;function o(c,s){this._mutationCallback(c,s,n)}let i=new IntersectionObserver(this._handleIntersection.bind(this),this.options),r=new MutationObserver(o.bind(this)),b=new MutationObserver(this._handleClassMutation.bind(this));e.forEach(c=>{let s=c;c.classList.contains("nfd-wb-reveal-right")&&(s=c.parentElement),i.observe(s),t&&(b.observe(s,{attributes:!0,attributeFilter:["class"]}),r.observe(s,{attributes:!0,attributeFilter:["class"]}))})}_handleIntersection(e,n){e.forEach(t=>{t.isIntersecting&&(t.target.classList.add(this.options.activeClass),t.target.querySelectorAll(".nfd-wb-animate").forEach(o=>{o.classList.add(this.options.activeClass)}),n.unobserve(t.target))})}_handleClassMutation(e){e.forEach(n=>{if(n?.type==="attributes"){let t=n.target;t.classList.contains("nfd-wb-animated-in")||t.classList.add("nfd-wb-animated-in")}})}_mutationCallback(e,n,t=null){e.forEach(o=>{if(o?.type==="attributes"){let i=o.target;t&&t===i.getAttribute("data-block")&&(i.getAttribute("data-replay-animation")===null&&(i.setAttribute("data-replay-animation",!0),requestAnimationFrame(()=>{i.removeAttribute("data-replay-animation")})),n.disconnect())}})}};document.addEventListener("DOMContentLoaded",()=>{d()});document.addEventListener("wonder-blocks/toolbar-button-added",()=>{d()});document.addEventListener("wonder-blocks/animation-changed",a=>{let e=a?.detail?.clientId;d(e)});document.addEventListener("wonder-blocks/block-order-changed",()=>{d()});window.onload=function(){d()};function d(a=null){let e=document.body?.classList.contains("block-editor-page")||!!a||document.body?.classList.contains("block-editor-iframe__body"),n=e?document.querySelector(".interface-interface-skeleton__content"):null,t=new l({root:n,threshold:0});requestAnimationFrame(()=>{let o=Array.from(document.getElementsByClassName("nfd-wb-animate"));t.observeElements(o,a,e)})}function u(a){(a||document).querySelectorAll(".wp-block-group.nfd-is-linked-group:not(.group-linked), .wp-block-cover.nfd-is-linked-group:not(.group-linked)").forEach(t=>{var o=t.getAttribute("data-link-url")||t.dataset.linkUrl;if(!o||!t.parentNode)return;let i=o.trim();o=/^(https?:)\/\//i.test(i)?i:"http://"+i;let r=document.createElement("a");r.href=o,(t.getAttribute("data-link-blank")==="1"||t.dataset.linkBlank==="1")&&(r.target="_blank",r.rel="noopener"),t.classList.add("group-linked"),[...t.attributes].map(({name:c,value:s})=>{r.setAttribute(c,s)}),r.innerHTML=t.innerHTML,t.parentNode.replaceChild(r,t)}),p()}function p(){document.addEventListener("click",a=>{a.target.closest(".block-editor-page .wp-block-group.nfd-is-linked-group, .block-editor-page .wp-block-cover.nfd-is-linked-group")&&a.preventDefault()},{capture:!0,passive:!1})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>u(document)):u(document);document.addEventListener("wonder-blocks/group-links-apply",a=>{u(a?.detail?.ctx||document)});})();

//# sourceURL=nfd-wonder-blocks-utilities-js-after
</script><script id="twenty-twenty-one-ie11-polyfills-js-after">
( Element.prototype.matches && Element.prototype.closest && window.NodeList && NodeList.prototype.forEach ) || document.write( '<script src="https://guerillamap.com/wp-content/themes/twentytwentyone/assets/js/polyfills.js?ver=2.7"></scr' + 'ipt>' );
//# sourceURL=twenty-twenty-one-ie11-polyfills-js-after
</script><script type='text/javascript' src='https://guerillamap.com/wp-content/themes/twentytwentyone/assets/js/primary-navigation.js?m=1764816941'></script><script src="https://cdn.plot.ly/plotly-2.30.0.min.js"></script><script>
// ===== CONFIG =====
const baseURL = "https://guerillamap.com//wp-content/uploads/";
const referenceFile = baseURL + "sweep_reference.csv";
const HOURS_TOTAL = 24;

// ===== Utilities =====
async function loadCSV(url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Failed to load ${url}`);
  const text = await res.text();
  const rows = text.trim().split(/\r?\n/).map(r => r.split(','));
  return rows;
}

function parseSweep(rows) {
  // legacy: return max-power-per-frequency (keeps behaviour for plot1)
  let freqPowerMap = new Map();

  for (let i = 0; i < rows.length; i++) {
    if (rows[i].length < 7) continue;

    const lowHz = parseFloat(rows[i][2]);
    const stepHz = parseFloat(rows[i][4]);
    const pwrs = rows[i].slice(6).map(parseFloat);

    for (let j = 0; j < pwrs.length; j++) {
      const freq = (lowHz + j * stepHz) / 1e6; // MHz
      const pwr  = pwrs[j];

      if (!freqPowerMap.has(freq)) {
        freqPowerMap.set(freq, pwr);
      } else {
        // store the MAX value over all sweeps
        freqPowerMap.set(freq, Math.max(freqPowerMap.get(freq), pwr));
      }
    }
  }

  const freqs = Array.from(freqPowerMap.keys()).sort((a,b)=>a-b);
  const powers = freqs.map(f => freqPowerMap.get(f));

  return { freqs, powers };
}

// ===== Waterfall builder (returns per-timestamp rows) =====
function buildWaterfall(rows) {
  const sweeps = new Map();

  for (const r of rows) {
    if (r.length < 7) continue;
    const ts = r[0] + " " + r[1]; // CSV timestamp columns (Option B: ISO + Z in time column)
    const low = parseFloat(r[2]);
    const step = parseFloat(r[4]);
    const pw = r.slice(6).map(parseFloat);

    if (!sweeps.has(ts)) sweeps.set(ts, []);
    sweeps.get(ts).push({ low, step, pw });
  }

  const timestamps = Array.from(sweeps.keys()).sort();
  const Z = [];
  let freqAxis = [];

  timestamps.forEach((ts, idx) => {
    const parts = sweeps.get(ts).sort((a,b)=>a.low-b.low);
    let combined = [];
    let freqs = [];

    parts.forEach(seg => {
      for (let i=0;i<seg.pw.length;i++){
        combined.push(seg.pw[i]);
        if (idx===0) freqs.push((seg.low + seg.step*i)/1e6);
      }
    });

    if (idx===0) freqAxis = freqs;
    Z.push(combined);
  });

  return {freqAxis, timestamps, Z};
}

// ===== Render waterfall =====
function renderWaterfall(freqs, timestamps, matrix) {
  const z = matrix;
  const data = [{
    x: freqs,
    y: timestamps,
    z: z,
    type: 'heatmap',
    colorscale: 'Viridis'
  }];
  Plotly.newPlot('waterfall', data, {
    title: 'Waterfall',
    xaxis: {title:'Frequency (MHz)'},
    yaxis: {title:''},
    height: 650
  });
}

// ===== Interpolate reference helper =====
function interpRefForFreqs(refFreqs, refPower, freqs) {
  const refInterp = [];
  if (refFreqs.length === 0) return freqs.map(_=>0);
  for (let f of freqs) {
    let idx = refFreqs.findIndex(rf => rf >= f);
    if (idx <= 0) refInterp.push(refPower[0]);
    else if (idx >= refFreqs.length) refInterp.push(refPower[refPower.length-1]);
    else {
      const x0=refFreqs[idx-1], x1=refFreqs[idx];
      const y0=refPower[idx-1], y1=refPower[idx];
      refInterp.push(y0+(y1-y0)*(f-x0)/(x1-x0));
    }
  }
  return refInterp;
}

// ===== Plot Sweep =====
async function loadSweep(labelDate, hourIndex) {
  const hourStr = String(hourIndex).padStart(2, '0');  // ensure 00–23 format
  const file = `sweep_${hourStr}.csv`;
  const url = baseURL + file;
  console.log("Loading", url);

  document.querySelectorAll('#controls button').forEach(b=>b.classList.remove('active'));
  const btn = document.getElementById(`btn_${hourIndex}`);
  if (btn) btn.classList.add('active');

  try {
    const sweepRows = await loadCSV(url);
    const { freqs, powers } = parseSweep(sweepRows);

    // Load reference
    let refFreqs = [], refPower = [];
    try {
      const refRows = await loadCSV(referenceFile);
      for (let i = 1; i < refRows.length; i++) {
        const r = refRows[i];
        const freq = parseFloat(r[0]) / 1e6;
        const median = parseFloat(r[1]);
        refFreqs.push(freq); refPower.push(median);
      }
    } catch { console.warn("Reference not found"); }

    // Interpolate reference to the aggregated freqs (for plot2 baseline)
    let refInterp = interpRefForFreqs(refFreqs, refPower, freqs);

    const diff = freqs.map((f,i)=>powers[i]-(refInterp[i]||0));

    Plotly.newPlot('plot1', [
      {x: freqs, y: powers, name:"Current Sweep", mode:'lines', line:{color:'red'}},
      ...(refFreqs.length>0?[{x: refFreqs, y: refPower, name:"Reference", mode:'lines', line:{color:'black'}}]:[])
    ], {
      title:`${labelDate} — sweep_${hourIndex}.csv`,
      xaxis:{title:"Frequency (MHz)"}, yaxis:{title:"Power (dB)"}, legend:{orientation:"h"}
    });

    // Shapes / annotations etc. (kept as before)
    const shapes = [
      { type: 'rect', xref: 'x', yref: 'paper', x0: 30, x1: 88, y0: 0, y1: 1, fillcolor: 'rgba(200,200,255,0.2)', line: {width: 0} },
      { type: 'rect', xref: 'x', yref: 'paper', x0: 87.5, x1: 108, y0: 0, y1: 1, fillcolor: 'rgba(200,255,200,0.2)', line: {width: 0} },
      { type: 'rect', xref: 'x', yref: 'paper', x0: 108, x1: 132, y0: 0, y1: 1, fillcolor: 'rgba(255,255,200,0.2)', line: {width: 0} },
      { type: 'rect', xref: 'x', yref: 'paper', x0: 158, x1: 170, y0: 0, y1: 1, fillcolor: 'rgba(255,200,200,0.2)', line: {width: 0} },
      { type: 'rect', xref: 'x', yref: 'paper', x0: 225, x1: 400, y0: 0, y1: 1, fillcolor: 'rgba(200,255,255,0.2)', line: {width: 0} },
      { type: 'rect', xref: 'x', yref: 'paper', x0: 380, x1: 400, y0: 0, y1: 1, fillcolor: 'rgba(200,255,255,0.2)', line: {width: 0} },
	  { type: 'rect', xref: 'x', yref: 'paper', x0: 433, x1: 434.8, y0: 0, y1: 1, fillcolor: 'rgba(80,255,255,0.2)', line: {width: 0} },
      { type: 'rect', xref: 'x', yref: 'paper', x0: 703, x1: 788, y0: 0, y1: 1, fillcolor: 'rgba(255,220,255,0.2)', line: {width: 0} },
	  { type: 'rect', xref: 'x', yref: 'paper', x0: 863, x1: 870, y0: 0, y1: 1, fillcolor: 'rgba(255,50,255,0.2)', line: {width: 0} }
    ];

    const bandAnnotations = [
      {x: 59, y: -25, text: "Mil Com", showarrow: false},
      {x: 98, y: -30, text: "FM Broadcast", showarrow: false},
      {x: 120, y: -35, text: "ATC", showarrow: false},
      {x: 164, y: -20, text: "Firefighters", showarrow: false},
      {x: 312, y: -30, text: "Mil ATC", showarrow: false},
      {x: 390, y: -30, text: "Polycom", showarrow: false},
	  {x: 434, y: -30, text: "LoRa", showarrow: false},
      {x: 745, y: -30, text: "Mobile", showarrow: false},
	  {x: 867, y: -30, text: "LoRa", showarrow: false},
    ];

    const markers = [
      {f: 159.2, label: 'REGA'},
      {f: 400, label: 'Device Ctrl'},
      {f: 433, label: 'Drones'},
      {f: 446, label: 'PMR446'},
	  {f: 470, label: 'LoRa'},
      {f: 600, label: 'Wireless Devices'},
	  {f: 799, label: 'LoRa'},
      {f: 810, label: 'Mobile'},
      {f: 900, label: 'Mobile'},
      {f: 915, label: 'Drones'},
      {f: 1090, label: 'ADS-B'},
      {f: 1176, label: 'GPS'},
      {f: 1191.795, label: 'Galileo'},
      {f: 1207.14, label: 'Galileo'},
      {f: 1227, label: 'GPS Mil'},
      {f: 1278.75, label: 'Galileo'},
      {f: 1400, label: 'Mobile'},
      {f: 1575, label: 'GPS Civil'}
    ];

    markers.forEach(m => {
      shapes.push({
        type: 'line', xref: 'x', yref: 'paper', x0: m.f, x1: m.f, y0: 0, y1: 1,
        line: {color: 'gray', width: 1, dash: 'dot'}
      });
      bandAnnotations.push({ x: m.f, y: -0.55, text: m.label, textangle: 90, showarrow: false, font: {size: 10, color: 'gray'}, xref: 'x', yref: 'paper' });
    });

    const layout = {
      title: "Difference (Current − Reference)",
      xaxis: {title: "Frequency (MHz)", range: [50,1760]},
      yaxis: {title: "ΔPower (dB)"},
      shapes: shapes,
      annotations: bandAnnotations,
      margin: {t: 60, b: 180},
      cliponaxis: false,
	  legend:{orientation:"h"}
    };

    Plotly.newPlot('plot2', [
      {x: freqs, y: diff, mode:'lines', name:"ΔPower", line:{color:'red'}},
      {x:[freqs[0],freqs[freqs.length-1]], y:[0,0], mode:'lines', line:{color:'black', dash:'dot'}, showlegend:false}
    ], layout);

    // === Build waterfall and compute timestamped peaks ===
    try {
      const wf = buildWaterfall(sweepRows); // {freqAxis, timestamps, Z}
      renderWaterfall(wf.freqAxis, wf.timestamps, wf.Z);

      // Interpolate reference to match wf.freqAxis
      const refInterpWF = interpRefForFreqs(refFreqs, refPower, wf.freqAxis);

      // matrix of diffs: for each timestamp row j, for each freq i
      const matrixDiff = wf.Z.map(row => row.map((p,i)=>p - (refInterpWF[i]||0)));

      // For each frequency (column) find the maximum diff and the timestamp index where it occurs
      const peaks = [];
      const threshold = 7;
      for (let i=0;i<wf.freqAxis.length;i++){
        let maxVal = -Infinity; let maxIdx = -1;
        for (let j=0;j<matrixDiff.length;j++){
          const v = matrixDiff[j][i];
          if (v > maxVal) { maxVal = v; maxIdx = j; }
        }
        if (maxVal > threshold) {
          peaks.push({ freq: wf.freqAxis[i], diff: maxVal, ts: wf.timestamps[maxIdx] });
        }
      }

      peaks.sort((a,b)=>a.freq-b.freq);

      const tbody=document.querySelector('#peakTable tbody');
      tbody.innerHTML='';
      for (const p of peaks){
        const tr=document.createElement('tr');
        tr.innerHTML=`<td>${p.ts}</td><td>${p.freq.toFixed(3)}</td><td>${p.diff.toFixed(2)}</td>`;
        tbody.appendChild(tr);
      }

    } catch(e) {
      console.error(e);
      alert(`Failed to render waterfall/peaks for ${url}`);
    }

  } catch(e){
    console.error(e);
    alert(`Failed to load ${url}`);
  }
}

// ===== Build rolling 24h control buttons =====

function buildButtons() {
  const container = document.getElementById('controls');
  const now = new Date();

  for (let i = HOURS_TOTAL - 1; i >= 0; i--) {
    // Base time for the hour (minus i hours)
	const base = new Date(now.getTime()-i*3600*1000);

    // Normalize to exact start of that hour
    base.setUTCMinutes(0, 0, 0);

    // Quarter hours to generate
    const quarters = [0, 15, 30, 45];

    quarters.forEach(min => {
      const d = new Date(base.getTime());
      d.setUTCMinutes(min);
	  
	  //skip quarter hours not passed yet
	  const now2 = new Date();
	  now2.setMinutes(now2.getMinutes()-15);
	  if (d > now2) return;

      const hour = d.getUTCHours();
      const label = `${String(hour).padStart(2, '0')}${String(min).padStart(2, '0')}`;

      const btn = document.createElement('button');
      btn.id = `btn_${label}`;
      btn.textContent = label;
      btn.title = d.toISOString();
      btn.onclick = () => loadSweep(d.toISOString(), label);

      container.appendChild(btn);
    });
  }
}

// ===== Drone Alerts Scanner (per-timestamp inside each CSV) =====
async function scanDroneAlerts() {
  const tbody = document.querySelector('#droneTable tbody');
  tbody.innerHTML = "<tr><td colspan='3'>Scanning last 12h...</td></tr>";

  const now = new Date();
  const results = [];

  // Load reference once
  let refFreqs = [], refPower = [];
  try {
    const refRows = await loadCSV(referenceFile);
    for (let i = 1; i < refRows.length; i++) {
      const r = refRows[i];
      const freq = parseFloat(r[0]) / 1e6;
      const median = parseFloat(r[1]);
      refFreqs.push(freq); refPower.push(median);
    }
  } catch { console.warn("Reference not found"); }

  for (let h = 0; h < 12; h++) {
	const base = new Date(now.getTime() - h * 3600 * 1000);
	base.setUTCMinutes(0, 0, 0);

	const quarters = [0, 15, 30, 45];

	for (const min of quarters) {
		// Create full timestamp for this sweep
		const d = new Date(base.getTime());
		d.setUTCMinutes(min);

		const hour = d.getUTCHours();
		const label =
		  `${String(hour).padStart(2, '0')}` +
		  `${String(min).padStart(2, '0')}`;

		const file = `${baseURL}sweep_${label}.csv`;
	
		try {
		  const rows = await loadCSV(file);
		  // Build waterfall-like structure from this file
		  const wf = buildWaterfall(rows); // {freqAxis, timestamps, Z}
		  if (!wf || wf.freqAxis.length===0) continue;

		  // Interpolate reference for this freq axis
		  const refInterpWF = interpRefForFreqs(refFreqs, refPower, wf.freqAxis);

		  // matrixDiff per timestamp
		  const matrixDiff = wf.Z.map(row => row.map((p,i)=>p - (refInterpWF[i]||0)));

		  const bands = [
			{name:'433 MHz Band (drones)',  fmin:430, fmax:436},
			{name:'446 MHz Band (PMR446)',  fmin:445.8, fmax:446.2},
			{name:'915 MHz Band (drones)',  fmin:912, fmax:918},
		  ];

		  // For each timestamp (row), check each band
		  for (let tIdx=0; tIdx<wf.timestamps.length; tIdx++){
			const rowDiffs = matrixDiff[tIdx];
			for (const band of bands) {
			  // collect diffs inside band
			  const bandVals = wf.freqAxis.map((f,i)=> (f>=band.fmin && f<=band.fmax ? rowDiffs[i] : -Infinity));
			  const peak = Math.max(...bandVals);
			  if (peak > 5) {
				results.push({ time: wf.timestamps[tIdx], band: band.name, peak: peak.toFixed(2) });
			  }
			}
		  }

		} catch (err) {
		  console.warn("Missing or unreadable sweep file:", file);
		}
	}

	  tbody.innerHTML = "";
	  if (results.length === 0) {
		tbody.innerHTML = "<tr><td colspan='3'>No drone or PMR446 activity detected in last 12 hours.</td></tr>";
	  } else {
		results.sort((a,b)=> new Date(b.time) - new Date(a.time));
		for (const r of results) {
		  const tr = document.createElement('tr');
		  tr.innerHTML = `<td>${r.time}</td><td>${r.band}</td><td>${r.peak}</td>`;
		  tbody.appendChild(tr);
		}
	  }
	}
}

// Start scanning once buttons are built
buildButtons();
scanDroneAlerts();
setInterval(buildButtons, 900000); //15 min
setInterval(scanDroneAlerts, 900000); //15 min
</script><script type="speculationrules">
{"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/twentytwentyone/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]}
</script><script>
document.body.classList.remove('no-js');
//# sourceURL=twenty_twenty_one_supports_js
</script><script>
		if ( -1 !== navigator.userAgent.indexOf('MSIE') || -1 !== navigator.appVersion.indexOf('Trident/') ) {
			document.body.classList.add('is-IE');
		}
	//# sourceURL=twentytwentyone_add_ie_class
</script><script>
				const lazyloadRunObserver = () => {
					const lazyloadBackgrounds = document.querySelectorAll( `.e-con.e-parent:not(.e-lazyloaded)` );
					const lazyloadBackgroundObserver = new IntersectionObserver( ( entries ) => {
						entries.forEach( ( entry ) => {
							if ( entry.isIntersecting ) {
								let lazyloadBackground = entry.target;
								if( lazyloadBackground ) {
									lazyloadBackground.classList.add( 'e-lazyloaded' );
								}
								lazyloadBackgroundObserver.unobserve( entry.target );
							}
						});
					}, { rootMargin: '200px 0px 200px 0px' } );
					lazyloadBackgrounds.forEach( ( lazyloadBackground ) => {
						lazyloadBackgroundObserver.observe( lazyloadBackground );
					} );
				};
				const events = [
					'DOMContentLoaded',
					'elementor/lazyload/observe',
				];
				events.forEach( ( event ) => {
					document.addEventListener( event, lazyloadRunObserver );
				} );
			</script><script>window.addEventListener( 'load', function() {
				document.querySelectorAll( 'link' ).forEach( function( e ) {'not all' === e.media && e.dataset.media && ( e.media = e.dataset.media, delete e.dataset.media );} );
				var e = document.getElementById( 'jetpack-boost-critical-css' );
				e && ( e.media = 'not all' );
			} );</script><script id="give-donation-summary-script-frontend-js-extra">
var GiveDonationSummaryData = {"currencyPrecisionLookup":{"USD":2,"EUR":2,"GBP":2,"AUD":2,"BRL":2,"CAD":2,"CZK":2,"DKK":2,"HKD":2,"HUF":2,"ILS":2,"JPY":0,"MYR":2,"MXN":2,"MAD":2,"NZD":2,"NOK":2,"PHP":2,"PLN":2,"SGD":2,"KRW":0,"ZAR":2,"SEK":2,"CHF":2,"TWD":2,"THB":2,"INR":2,"TRY":2,"IRR":2,"RUB":2,"AED":2,"AMD":2,"ANG":2,"ARS":2,"AWG":2,"BAM":2,"BDT":2,"BHD":3,"BMD":2,"BND":2,"BOB":2,"BSD":2,"BWP":2,"BZD":2,"CLP":0,"CNY":2,"COP":2,"CRC":2,"CUC":2,"CUP":2,"DOP":2,"EGP":2,"GIP":2,"GTQ":2,"HNL":2,"HRK":2,"IDR":2,"ISK":0,"JMD":2,"JOD":2,"KES":2,"KWD":2,"KYD":2,"MKD":2,"NPR":2,"OMR":3,"PEN":2,"PKR":2,"RON":2,"SAR":2,"SZL":2,"TOP":2,"TZS":2,"TVD":2,"UAH":2,"UYU":2,"VEF":2,"VES":2,"VED":2,"XCD":2,"XCG":2,"XDR":2,"AFN":2,"ALL":2,"AOA":2,"AZN":2,"BBD":2,"BGN":2,"BIF":0,"XBT":8,"BTN":1,"BYR":2,"BYN":2,"CDF":2,"CVE":2,"DJF":0,"DZD":2,"ERN":2,"ETB":2,"FJD":2,"FKP":2,"GEL":2,"GGP":2,"GHS":2,"GMD":2,"GNF":0,"GYD":2,"HTG":2,"IMP":2,"IQD":2,"IRT":2,"JEP":2,"KGS":2,"KHR":0,"KMF":2,"KPW":0,"KZT":2,"LAK":0,"LBP":2,"LKR":0,"LRD":2,"LSL":2,"LYD":3,"MDL":2,"MGA":0,"MMK":2,"MNT":2,"MOP":2,"MRO":2,"MRU":2,"MUR":2,"MVR":1,"MWK":2,"MZN":0,"NAD":2,"NGN":2,"NIO":2,"PAB":2,"PGK":2,"PRB":2,"PYG":2,"QAR":2,"RSD":2,"RWF":2,"SBD":2,"SCR":2,"SDG":2,"SHP":2,"SLL":2,"SLE":2,"SOS":2,"SRD":2,"SSP":2,"STD":2,"STN":2,"SVC":2,"SYP":2,"TJS":2,"TMT":2,"TND":3,"TTD":2,"UGX":2,"UZS":2,"VND":1,"VUV":0,"WST":2,"XAF":2,"XOF":2,"XPF":2,"YER":2,"ZMW":2,"ZWL":2},"recurringLabelLookup":[]};
//# sourceURL=give-donation-summary-script-frontend-js-extra
</script><script type='text/javascript' src='https://guerillamap.com/wp-content/plugins/give/build/assets/dist/js/give-donation-summary.js?m=1765537148'></script><script src="https://guerillamap.com/wp-includes/js/dist/vendor/react.min.js?ver=18.3.1.1" id="react-js"></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??0e38444c36'></script><script src="https://guerillamap.com/wp-includes/js/dist/api-fetch.min.js?ver=3a4d9af2b423048b0dee" id="wp-api-fetch-js"></script><script id="wp-api-fetch-js-after">
wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "https://guerillamap.com/wp-json/" ) );
wp.apiFetch.nonceMiddleware = wp.apiFetch.createNonceMiddleware( "bd70a46e29" );
wp.apiFetch.use( wp.apiFetch.nonceMiddleware );
wp.apiFetch.use( wp.apiFetch.mediaUploadMiddleware );
wp.apiFetch.nonceEndpoint = "https://guerillamap.com/wp-admin/admin-ajax.php?action=rest-nonce";
(function(){if(!window.wp||!wp.apiFetch||!wp.apiFetch.use){return;}wp.apiFetch.use(function(options,next){var p=String((options&&(options.path||options.url))||"");try{var u=new URL(p,window.location.origin);p=(u.pathname||"")+(u.search||"");}catch(e){}if(p.indexOf("/wp/v2/users/me")!==-1){return Promise.resolve(null);}return next(options);});})();
//# sourceURL=wp-api-fetch-js-after
</script><script src="https://guerillamap.com/wp-includes/js/dist/vendor/react-dom.min.js?ver=18.3.1.1" id="react-dom-js"></script><script type='text/javascript' src='https://guerillamap.com/wp-includes/js/dist/dom-ready.min.js?m=1712113255'></script><script src="https://guerillamap.com/wp-includes/js/dist/a11y.min.js?ver=cb460b4676c94bd228ed" id="wp-a11y-js"></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??c260b2f60d'></script><script src="https://guerillamap.com/wp-includes/js/dist/keycodes.min.js?ver=34c8fb5e7a594a1c8037" id="wp-keycodes-js"></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??52e30d6d52'></script><script src="https://guerillamap.com/wp-includes/js/dist/data.min.js?ver=f940198280891b0b6318" id="wp-data-js"></script><script id="wp-data-js-after">
( function() {
	var userId = 0;
	var storageKey = "WP_DATA_USER_" + userId;
	wp.data
		.use( wp.data.plugins.persistence, { storageKey: storageKey } );
} )();
//# sourceURL=wp-data-js-after
</script><script type='text/javascript' src='https://guerillamap.com/wp-includes/js/dist/html-entities.min.js?m=1764741359'></script><script src="https://guerillamap.com/wp-includes/js/dist/rich-text.min.js?ver=5bdbb44f3039529e3645" id="wp-rich-text-js"></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??e2c3d9db83'></script><script src="https://guerillamap.com/wp-includes/js/dist/blocks.min.js?ver=de131db49fa830bc97da" id="wp-blocks-js"></script><script src="https://guerillamap.com/wp-includes/js/dist/vendor/moment.min.js?ver=2.30.1" id="moment-js"></script><script id="moment-js-after">
moment.updateLocale( 'en_US', {"months":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthsShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"weekdays":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"weekdaysShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"week":{"dow":1},"longDateFormat":{"LT":"H:i","LTS":null,"L":null,"LL":"Y-m-d","LLL":"F j, Y g:i a","LLLL":null}} );
//# sourceURL=moment-js-after
</script><script src="https://guerillamap.com/wp-includes/js/dist/date.min.js?ver=795a56839718d3ff7eae" id="wp-date-js"></script><script id="wp-date-js-after">
wp.date.setSettings( {"l10n":{"locale":"en_US","months":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthsShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"weekdays":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"weekdaysShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"meridiem":{"am":"am","pm":"pm","AM":"AM","PM":"PM"},"relative":{"future":"%s from now","past":"%s ago","s":"a second","ss":"%d seconds","m":"a minute","mm":"%d minutes","h":"an hour","hh":"%d hours","d":"a day","dd":"%d days","M":"a month","MM":"%d months","y":"a year","yy":"%d years"},"startOfWeek":1},"formats":{"time":"H:i","date":"Y-m-d","datetime":"F j, Y g:i a","datetimeAbbreviated":"M j, Y g:i a"},"timezone":{"offset":0,"offsetFormatted":"0","string":"","abbr":""}} );
//# sourceURL=wp-date-js-after
</script><script type='text/javascript' src='https://guerillamap.com/wp-includes/js/dist/primitives.min.js?m=1764741359'></script><script src="https://guerillamap.com/wp-includes/js/dist/components.min.js?ver=ad5cb4227f07a3d422ad" id="wp-components-js"></script><script type='text/javascript' src='https://guerillamap.com/wp-includes/js/dist/keyboard-shortcuts.min.js?m=1764741359'></script><script src="https://guerillamap.com/wp-includes/js/dist/commands.min.js?ver=cac8f4817ab7cea0ac49" id="wp-commands-js"></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??421577fa89'></script><script src="https://guerillamap.com/wp-includes/js/dist/preferences.min.js?ver=2ca086aed510c242a1ed" id="wp-preferences-js"></script><script id="wp-preferences-js-after">
( function() {
				var serverData = false;
				var userId = "0";
				var persistenceLayer = wp.preferencesPersistence.__unstableCreatePersistenceLayer( serverData, userId );
				var preferencesStore = wp.preferences.store;
				wp.data.dispatch( preferencesStore ).setPersistenceLayer( persistenceLayer );
			} ) ();
//# sourceURL=wp-preferences-js-after
</script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??cff0e7b23a'></script><script src="https://guerillamap.com/wp-includes/js/dist/block-editor.min.js?ver=6ab992f915da9674d250" id="wp-block-editor-js"></script><script src="https://guerillamap.com/wp-includes/js/dist/core-data.min.js?ver=15baadfe6e1374188072" id="wp-core-data-js"></script><script src="https://guerillamap.com/wp-content/plugins/give/build/entitiesPublic.js?ver=b759f2adda1f29c50713" id="givewp-entities-public-js"></script><script id="linkprefetcher-js-before">
window.LP_CONFIG = {"activeOnDesktop":true,"behavior":"mouseHover","hoverDelay":60,"instantClick":false,"activeOnMobile":true,"mobileBehavior":"viewport","ignoreKeywords":"#,?","isMobile":false}
//# sourceURL=linkprefetcher-js-before
</script><script src="https://guerillamap.com/wp-content/plugins/bluehost-wordpress-plugin/vendor/newfold-labs/wp-module-performance/build/assets/link-prefetch.min.js?ver=4.11.0" id="linkprefetcher-js" defer></script><script type='text/javascript' src='https://guerillamap.com/_jb_static/??9750b66729'></script><script id="elementor-frontend-js-before">
var elementorFrontendConfig = {"environmentMode":{"edit":false,"wpPreview":false,"isScriptDebug":false},"i18n":{"shareOnFacebook":"Share on Facebook","shareOnTwitter":"Share on Twitter","pinIt":"Pin it","download":"Download","downloadImage":"Download image","fullscreen":"Fullscreen","zoom":"Zoom","share":"Share","playVideo":"Play Video","previous":"Previous","next":"Next","close":"Close","a11yCarouselPrevSlideMessage":"Previous slide","a11yCarouselNextSlideMessage":"Next slide","a11yCarouselFirstSlideMessage":"This is the first slide","a11yCarouselLastSlideMessage":"This is the last slide","a11yCarouselPaginationBulletMessage":"Go to slide"},"is_rtl":false,"breakpoints":{"xs":0,"sm":480,"md":768,"lg":1025,"xl":1440,"xxl":1600},"responsive":{"breakpoints":{"mobile":{"label":"Mobile Portrait","value":767,"default_value":767,"direction":"max","is_enabled":true},"mobile_extra":{"label":"Mobile Landscape","value":880,"default_value":880,"direction":"max","is_enabled":false},"tablet":{"label":"Tablet Portrait","value":1024,"default_value":1024,"direction":"max","is_enabled":true},"tablet_extra":{"label":"Tablet Landscape","value":1200,"default_value":1200,"direction":"max","is_enabled":false},"laptop":{"label":"Laptop","value":1366,"default_value":1366,"direction":"max","is_enabled":false},"widescreen":{"label":"Widescreen","value":2400,"default_value":2400,"direction":"min","is_enabled":false}},"hasCustomBreakpoints":false},"version":"3.34.1","is_static":false,"experimentalFeatures":{"e_font_icon_svg":true,"theme_builder_v2":true,"home_screen":true,"global_classes_should_enforce_capabilities":true,"e_variables":true,"cloud-library":true,"e_opt_in_v4_page":true,"e_interactions":true,"import-export-customization":true,"e_pro_variables":true},"urls":{"assets":"https:\/\/guerillamap.com\/wp-content\/plugins\/elementor\/assets\/","ajaxurl":"https:\/\/guerillamap.com\/wp-admin\/admin-ajax.php","uploadUrl":"https:\/\/guerillamap.com\/wp-content\/uploads"},"nonces":{"floatingButtonsClickTracking":"7e2219a786"},"swiperClass":"swiper","settings":{"page":[],"editorPreferences":[]},"kit":{"active_breakpoints":["viewport_mobile","viewport_tablet"],"global_image_lightbox":"yes","lightbox_enable_counter":"yes","lightbox_enable_fullscreen":"yes","lightbox_enable_zoom":"yes","lightbox_enable_share":"yes","lightbox_title_src":"title","lightbox_description_src":"description"},"post":{"id":713392,"title":"sweeper%20tutorial%20-%20Guerillamap","excerpt":"","featuredImage":false}};
//# sourceURL=elementor-frontend-js-before
</script><script src="https://guerillamap.com/wp-content/plugins/elementor/assets/js/frontend.min.js?ver=3.34.1" id="elementor-frontend-js"></script><script type='text/javascript' src='https://guerillamap.com/wp-content/plugins/elementor-pro/assets/lib/smartmenus/jquery.smartmenus.min.js?m=1767114079'></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-core.min.js?ver=1.23.0" id="prismjs_core-js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/autoloader/prism-autoloader.min.js?ver=1.23.0" id="prismjs_loader-js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/normalize-whitespace/prism-normalize-whitespace.min.js?ver=1.23.0" id="prismjs_normalize-js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/line-numbers/prism-line-numbers.min.js?ver=1.23.0" id="prismjs_line_numbers-js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/toolbar/prism-toolbar.min.js?ver=1.23.0" id="prismjs_toolbar-js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js?ver=1.23.0" id="prismjs_copy_to_clipboard-js"></script><script type='text/javascript' src='https://guerillamap.com/wp-content/plugins/elementor-pro/assets/js/webpack-pro.runtime.min.js?m=1767114080'></script><script id="elementor-pro-frontend-js-before">
var ElementorProFrontendConfig = {"ajaxurl":"https:\/\/guerillamap.com\/wp-admin\/admin-ajax.php","nonce":"0e2483f5d0","urls":{"assets":"https:\/\/guerillamap.com\/wp-content\/plugins\/elementor-pro\/assets\/","rest":"https:\/\/guerillamap.com\/wp-json\/"},"settings":{"lazy_load_background_images":true},"popup":{"hasPopUps":false},"shareButtonsNetworks":{"facebook":{"title":"Facebook","has_counter":true},"twitter":{"title":"Twitter"},"linkedin":{"title":"LinkedIn","has_counter":true},"pinterest":{"title":"Pinterest","has_counter":true},"reddit":{"title":"Reddit","has_counter":true},"vk":{"title":"VK","has_counter":true},"odnoklassniki":{"title":"OK","has_counter":true},"tumblr":{"title":"Tumblr"},"digg":{"title":"Digg"},"skype":{"title":"Skype"},"stumbleupon":{"title":"StumbleUpon","has_counter":true},"mix":{"title":"Mix"},"telegram":{"title":"Telegram"},"pocket":{"title":"Pocket","has_counter":true},"xing":{"title":"XING","has_counter":true},"whatsapp":{"title":"WhatsApp"},"email":{"title":"Email"},"print":{"title":"Print"},"x-twitter":{"title":"X"},"threads":{"title":"Threads"}},"facebook_sdk":{"lang":"en_US","app_id":""},"lottie":{"defaultAnimationUrl":"https:\/\/guerillamap.com\/wp-content\/plugins\/elementor-pro\/modules\/lottie\/assets\/animations\/default.json"}};
//# sourceURL=elementor-pro-frontend-js-before
</script><script src="https://guerillamap.com/wp-content/plugins/elementor-pro/assets/js/frontend.min.js?ver=3.34.0" id="elementor-pro-frontend-js"></script><script src="https://guerillamap.com/wp-content/plugins/elementor-pro/assets/js/elements-handlers.min.js?ver=3.34.0" id="pro-elements-handlers-js"></script><script id="inavii-widget-handlers-js-extra">
var InaviiRestApi = {"baseUrl":"https://guerillamap.com/wp-json/inavii/v1/","authToken":"7719a1c782a1ba91c031a682a0a2f8658209adbf"};
//# sourceURL=inavii-widget-handlers-js-extra
</script><script type='text/javascript' src='https://guerillamap.com/wp-content/plugins/inavii-social-feed-for-elementor/assets/dist/js/inavii-js.min.js?m=1757844806'></script><script type="module">
/*! This file is auto-generated */
const a=JSON.parse(document.getElementById("wp-emoji-settings").textContent),o=(window._wpemojiSettings=a,"wpEmojiSettingsSupports"),s=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(o,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const a=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===a[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e<n.data.length;e++)if(0!==n.data[e])return!1;return!0}function u(e,t,n,a){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!a(e,"\ud83e\u1fac8")}return!1}function f(e,t,n,a){let r;const o=(r="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):document.createElement("canvas")).getContext("2d",{willReadFrequently:!0}),s=(o.textBaseline="top",o.font="600 32px Arial",{});return e.forEach(e=>{s[e]=t(o,e,n,a)}),s}function r(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}a.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(o));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(s),u.toString(),c.toString(),p.toString()].join(",")+"));",a=new Blob([e],{type:"text/javascript"});const r=new Worker(URL.createObjectURL(a),{name:"wpTestEmojiSupports"});return void(r.onmessage=e=>{i(n=e.data),r.terminate(),t(n)})}catch(e){}i(n=f(s,u,c,p))}t(n)}).then(e=>{for(const n in e)a.supports[n]=e[n],a.supports.everything=a.supports.everything&&a.supports[n],"flag"!==n&&(a.supports.everythingExceptFlag=a.supports.everythingExceptFlag&&a.supports[n]);var t;a.supports.everythingExceptFlag=a.supports.everythingExceptFlag&&!a.supports.flag,a.supports.everything||((t=a.source||{}).concatemoji?r(t.concatemoji):t.wpemoji&&t.twemoji&&(r(t.twemoji),r(t.wpemoji)))});
//# sourceURL=https://guerillamap.com/wp-includes/js/wp-emoji-loader.min.js
</script></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)