Skip to content

Commit 890e1a6

Browse files
committed
added hardware tests
1 parent 5cc4204 commit 890e1a6

3 files changed

Lines changed: 188 additions & 0 deletions

File tree

hackrfpy/tests/test_hardware.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,29 @@ def test_operacake_list_runs():
205205
out = h.operacake_list()
206206
text = out[0] if isinstance(out, tuple) else str(out)
207207
assert isinstance(text, str)
208+
209+
210+
@pytest.mark.hardware
211+
def test_monitor_frequencies_real():
212+
# sweep-backed power monitoring across two real frequencies, one pass
213+
h = HackRF()
214+
updates = []
215+
h.monitor_frequencies([100_000_000, 433_920_000], span_hz=3_000_000,
216+
on_update=lambda u: (updates.append(u), False)[1])
217+
assert updates, "expected at least one monitor update"
218+
u = updates[0]
219+
assert set(u.keys()) == {100_000_000, 433_920_000}
220+
221+
222+
@pytest.mark.hardware
223+
def test_persistent_receiver_amortizes_startup():
224+
# the persistent receiver should serve multiple reads from ONE process,
225+
# paying the spin-up once. Verify exact counts come back on real hardware.
226+
h = HackRF()
227+
with h.open_receiver(RX_FREQ, RX_RATE) as rx:
228+
a = rx.read(100_000)
229+
b = rx.read(100_000)
230+
assert len(a) == 100_000
231+
assert len(b) == 100_000
232+
assert not np.all(a == 0) # real signal
233+
assert a.dtype == np.complex64

hackrfpy/tests/test_monitor.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#! /usr/bin/python3
2+
3+
##--------------------------------------------------------------------\
4+
# hackrfpy 'tests/test_monitor.py'
5+
# monitor_frequencies: sweep-backed power-over-time monitoring. Distinct
6+
# from scan_frequencies (which returns IQ). Verifies frequency->segment
7+
# mapping, multi-pass yielding, final-pass flush, and callback mode.
8+
##--------------------------------------------------------------------\
9+
10+
from hackrfpy import HackRF
11+
12+
13+
# two sweep passes (two timestamps), segments covering 100 and 433 MHz
14+
_TWO_PASS = [
15+
"2026-06-15, 12:00:00.000000, 98000000, 102000000, 1000000.00, 4, -70, -65, -68, -72",
16+
"2026-06-15, 12:00:00.000000, 431000000, 435000000, 1000000.00, 4, -55, -50, -52, -58",
17+
"2026-06-15, 12:00:01.000000, 98000000, 102000000, 1000000.00, 4, -71, -66, -69, -73",
18+
"2026-06-15, 12:00:01.000000, 431000000, 435000000, 1000000.00, 4, -45, -40, -42, -48",
19+
]
20+
21+
22+
def test_monitor_maps_freqs_to_segments(stub_device):
23+
h = stub_device(sweep=dict(stdout_lines=_TWO_PASS))
24+
out = h.monitor_frequencies([100e6, 433e6], span_hz=2e6)
25+
assert len(out) == 2 # both passes (incl. final flush)
26+
# each pass maps both requested frequencies to a power value
27+
for u in out:
28+
assert set(u.keys()) == {100e6, 433e6}
29+
assert all(v is not None for v in u.values())
30+
31+
32+
def test_monitor_tracks_power_change(stub_device):
33+
h = stub_device(sweep=dict(stdout_lines=_TWO_PASS))
34+
out = h.monitor_frequencies([433e6], span_hz=2e6)
35+
# 433 MHz segment rises from mean(-55,-50,-52,-58)=-53.75 to
36+
# mean(-45,-40,-42,-48)=-43.75 between passes
37+
assert out[0][433e6] < out[1][433e6] # power increased
38+
assert abs(out[1][433e6] - (-43.75)) < 0.1
39+
40+
41+
def test_monitor_callback_mode(stub_device):
42+
h = stub_device(sweep=dict(stdout_lines=_TWO_PASS))
43+
seen = []
44+
r = h.monitor_frequencies([100e6], on_update=lambda u: seen.append(u))
45+
assert r is None # callback mode returns nothing
46+
assert len(seen) == 2
47+
48+
49+
def test_monitor_callback_stops_on_false(stub_device):
50+
h = stub_device(sweep=dict(stdout_lines=_TWO_PASS))
51+
calls = {"n": 0}
52+
53+
def cb(u):
54+
calls["n"] += 1
55+
return False # stop after first pass
56+
57+
h.monitor_frequencies([100e6], on_update=cb)
58+
assert calls["n"] == 1

hackrfpy/tests/test_receiver.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#! /usr/bin/python3
2+
3+
##--------------------------------------------------------------------\
4+
# hackrfpy 'tests/test_receiver.py'
5+
# PersistentReceiver: one long-lived hackrf_transfer drained in segments,
6+
# amortizing the per-capture startup cost. Verifies exact-count reads from
7+
# a SINGLE process launch, the blocks()/callback() access patterns, the
8+
# read-size tunability, and clean close.
9+
##--------------------------------------------------------------------\
10+
11+
import numpy as np
12+
13+
from hackrfpy import HackRF
14+
15+
16+
def _streaming_device(stub_device, nbytes=200_000):
17+
# a stub hackrf_transfer that streams a long continuous int8 ramp
18+
payload = list((bytes(range(256)) * (nbytes // 256 + 1))[:nbytes])
19+
return stub_device(transfer=dict(emit_bytes=payload, idle=False))
20+
21+
22+
def test_receiver_exact_counts_single_process(stub_device):
23+
h = _streaming_device(stub_device)
24+
launches = {"n": 0}
25+
orig = h._stream
26+
def counting(*a, **k):
27+
launches["n"] += 1
28+
return orig(*a, **k)
29+
h._stream = counting
30+
31+
with h.open_receiver(100e6, 8e6) as rx:
32+
a = rx.read(1000)
33+
b = rx.read(1000)
34+
c = rx.read(3000)
35+
assert len(a) == 1000
36+
assert len(b) == 1000
37+
assert len(c) == 3000
38+
assert a.dtype == np.complex64
39+
# THE point: all reads came from ONE process launch
40+
assert launches["n"] == 1
41+
42+
43+
def test_receiver_blocks_iteration(stub_device):
44+
h = _streaming_device(stub_device)
45+
with h.open_receiver(100e6, 8e6) as rx:
46+
got = 0
47+
for blk in rx.blocks():
48+
got += len(blk)
49+
if got >= 5000:
50+
break
51+
assert got >= 5000
52+
53+
54+
def test_receiver_callback_max_samples(stub_device):
55+
h = _streaming_device(stub_device)
56+
fired = {"n": 0}
57+
with h.open_receiver(100e6, 8e6) as rx:
58+
total = rx.callback(
59+
lambda iq, t: fired.__setitem__("n", fired["n"] + 1),
60+
max_samples=4000)
61+
assert fired["n"] >= 1
62+
assert total >= 4000
63+
64+
65+
def test_receiver_callback_stops_on_false(stub_device):
66+
h = _streaming_device(stub_device)
67+
calls = {"n": 0}
68+
def cb(iq, t):
69+
calls["n"] += 1
70+
return False
71+
with h.open_receiver(100e6, 8e6) as rx:
72+
rx.callback(cb)
73+
assert calls["n"] == 1
74+
75+
76+
def test_receiver_read_size_tunable(stub_device):
77+
h = _streaming_device(stub_device)
78+
rx = h.open_receiver(100e6, 8e6, read_samples=4096)
79+
assert rx.read_samples == 4096
80+
rx.close()
81+
82+
83+
def test_receiver_close_is_clean(stub_device):
84+
h = _streaming_device(stub_device)
85+
rx = h.open_receiver(100e6, 8e6)
86+
rx.read(100)
87+
rx.close()
88+
assert rx._closed is True
89+
assert rx._gen is None
90+
# double close is safe
91+
rx.close()
92+
93+
94+
def test_receiver_records_params_for_calibration(stub_device):
95+
# gains should land in last_params so relative_power_db can read them
96+
h = _streaming_device(stub_device)
97+
with h.open_receiver(100e6, 8e6, lna=16, vga=20) as rx:
98+
rx.read(100)
99+
assert h.last_params is not None
100+
# capture() records under 'lna'/'vga'; relative_power_db reads both
101+
assert h.last_params["lna"] == 16
102+
assert h.last_params["vga"] == 20
103+
# and the calibration helper can read them back
104+
assert h.relative_power_db(-6.0) == -6.0 - 36.0

0 commit comments

Comments
 (0)