1+ #! /usr/bin/python3
2+ ##--------------------------------------------------------------------\
3+ # hackrfpy 'examples/waterfall_persistent.py'
4+ # Single-frequency spectrum waterfall over time, driven by a PERSISTENT
5+ # receiver. Needs the [plotting] extra.
6+ #
7+ # This is the COMPLEMENT to waterfall_realtime.py, not a replacement:
8+ # - waterfall_realtime.py : SWEEP-based. x-axis = frequency across a wide
9+ # band; shows WHERE activity is across the spectrum.
10+ # - waterfall_persistent.py (this) : RECEIVER-based. Locks ONE center
11+ # frequency, FFTs each block; x-axis = frequency WITHIN the capture
12+ # bandwidth; shows how one channel evolves over time at full rate.
13+ #
14+ # It uses open_receiver() so the hackrf_transfer process spins up once and
15+ # streams continuously -- the natural fit for a live single-frequency view.
16+ # Read-only / receive-only; the receiver is reaped on exit.
17+ #
18+ # Usage:
19+ # uv run python examples/waterfall_persistent.py --freq 100e6 --rate 8e6
20+ # uv run python examples/waterfall_persistent.py --freq 2437e6 --rate 10e6
21+ ##--------------------------------------------------------------------\
22+ import argparse
23+ import sys
24+ import numpy as np
25+ import matplotlib .pyplot as plt
26+ from matplotlib import colors
27+ from hackrfpy import HackRF
28+
29+ # ---- aesthetic: same signals-monitor identity as the sweep waterfall ------
30+ BG = "#0a0e14"
31+ PANEL = "#0d1320"
32+ GRID = "#1c2738"
33+ ACCENT = "#36e0c8"
34+ TEXT = "#9fb3c8"
35+ TEXTDIM = "#52617a"
36+ CMAP = "turbo"
37+
38+ plt .rcParams .update ({
39+ "figure.facecolor" : BG , "axes.facecolor" : PANEL , "savefig.facecolor" : BG ,
40+ "font.family" : "monospace" , "text.color" : TEXT , "axes.edgecolor" : GRID ,
41+ "axes.labelcolor" : TEXT , "xtick.color" : TEXTDIM , "ytick.color" : TEXTDIM ,
42+ "axes.linewidth" : 0.8 ,
43+ })
44+
45+
46+ def main ():
47+ p = argparse .ArgumentParser (description = "Single-freq waterfall (persistent RX)." )
48+ p .add_argument ("--freq" , default = "100e6" , help = "center frequency Hz" )
49+ p .add_argument ("--rate" , default = "8e6" , help = "sample rate sps" )
50+ p .add_argument ("--fft" , type = int , default = 1024 , help = "FFT size (bins)" )
51+ p .add_argument ("--rows" , type = int , default = 400 , help = "time history depth" )
52+ p .add_argument ("--tools-dir" , default = None )
53+ args = p .parse_args ()
54+ freq , rate , nfft , rows = float (args .freq ), float (args .rate ), args .fft , args .rows
55+
56+ h = HackRF (tools_dir = args .tools_dir )
57+ det = h .detect ()
58+ if not det ["ready" ]:
59+ print (f"no usable HackRF: { det ['problem' ]} " , file = sys .stderr )
60+ return 1
61+
62+ # color scale in dBFS-ish FFT magnitude; fixed so colors are stable
63+ DB_FLOOR , DB_CEIL = - 90 , - 20
64+ win = np .hanning (nfft ).astype (np .float32 )
65+
66+ def spectrum (iq ):
67+ # Average several FFT frames across the big block for a smoother line,
68+ # then suppress the center DC / LO-leakage spike that every direct-
69+ # conversion SDR shows at 0 Hz (the bright line dead-center). We
70+ # replace the few central bins with their neighbors so a real signal
71+ # isn't hidden under the artifact.
72+ nframes = max (1 , len (iq ) // nfft )
73+ acc = np .zeros (nfft , dtype = np .float64 )
74+ for k in range (nframes ):
75+ seg = iq [k * nfft :(k + 1 ) * nfft ]
76+ if len (seg ) < nfft :
77+ break
78+ sp = np .fft .fftshift (np .fft .fft (seg * win ))
79+ acc += (np .abs (sp ) / nfft ) ** 2
80+ acc /= nframes
81+ db = 10 * np .log10 (acc + 1e-12 )
82+ # flatten the DC spike: overwrite the center +/-2 bins with a neighbor
83+ c = nfft // 2
84+ db [c - 2 :c + 3 ] = db [c + 3 ]
85+ return db
86+
87+ fig , ax = plt .subplots (figsize = (11 , 6 ))
88+ try :
89+ fig .canvas .manager .set_window_title (
90+ "hackrfpy :: persistent-RX waterfall" )
91+ except Exception :
92+ pass
93+
94+ history = []
95+ img = None
96+ cbar = None
97+ # frequency axis: center +/- rate/2, in MHz
98+ f_lo = (freq - rate / 2 ) / 1e6
99+ f_hi = (freq + rate / 2 ) / 1e6
100+
101+ print (f"[*] persistent waterfall @ { freq / 1e6 :g} MHz, { rate / 1e6 :g} Msps, "
102+ f"{ nfft } -pt FFT (close window or Ctrl-C to stop)" )
103+
104+ try :
105+ with h .open_receiver (freq , rate ) as rx :
106+ # Read a LARGE block per iteration so the pipe drains fast enough
107+ # to keep hackrf_transfer streaming (reading only nfft=1024 samples
108+ # per loop, with a redraw each time, stalls the device at 10 Msps).
109+ # Each big block becomes one averaged waterfall row.
110+ block_samples = 262144
111+ while True :
112+ iq = rx .read (block_samples )
113+ if len (iq ) < nfft :
114+ break
115+ history .append (spectrum (iq ))
116+ history = history [- rows :]
117+ arr = np .array (history )[::- 1 ] # newest on top
118+
119+ if img is None :
120+ img = ax .imshow (
121+ arr , aspect = "auto" , cmap = CMAP ,
122+ norm = colors .Normalize (vmin = DB_FLOOR , vmax = DB_CEIL ),
123+ interpolation = "nearest" , origin = "upper" ,
124+ extent = [f_lo , f_hi , 0 , len (arr )])
125+ cbar = fig .colorbar (img , ax = ax , pad = 0.01 , fraction = 0.046 )
126+ cbar .set_label ("magnitude (dB)" , color = TEXT , fontsize = 9 )
127+ cbar .outline .set_edgecolor (GRID )
128+ plt .setp (plt .getp (cbar .ax , "yticklabels" ), color = TEXTDIM )
129+ ax .set_title (
130+ f"SINGLE-FREQ WATERFALL \u2014 { freq / 1e6 :g} MHz "
131+ f"\u00b1 { rate / 2e6 :g} MHz" ,
132+ color = ACCENT , fontsize = 13 , fontweight = "bold" ,
133+ loc = "left" , pad = 12 , family = "monospace" )
134+ ax .set_xlabel ("frequency (MHz)" , fontsize = 9 )
135+ ax .set_ylabel ("time (newest at top)" , fontsize = 9 )
136+ ax .set_yticks ([])
137+ for s in ax .spines .values ():
138+ s .set_color (GRID )
139+ fig .tight_layout ()
140+ else :
141+ img .set_data (arr )
142+ img .set_extent ([f_lo , f_hi , 0 , len (arr )])
143+ plt .pause (0.001 )
144+ if not plt .fignum_exists (fig .number ):
145+ break
146+ except KeyboardInterrupt :
147+ pass
148+ print ("[*] stopped" )
149+ return 0
150+
151+
152+ if __name__ == "__main__" :
153+ sys .exit (main ())
0 commit comments