Skip to content

Commit 72e476a

Browse files
committed
add sample data in the examples
1 parent 642d608 commit 72e476a

5 files changed

Lines changed: 248 additions & 0 deletions

File tree

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#! /usr/bin/python3
2+
3+
##--------------------------------------------------------------------\
4+
# hackrfpy 'examples/collect_sample_data.py'
5+
#
6+
# Collect REAL, usable example datasets from a connected HackRF One and
7+
# write them into a sample library (examples/sample_data/) with SigMF
8+
# metadata, so anyone who clones the repo has actual recordings to load,
9+
# inspect, and feed into downstream processing -- without owning a board.
10+
#
11+
# This is DIFFERENT from tests/collect_real_data.py:
12+
# - collect_real_data.py freezes TINY verbatim slices as PARSER TEST
13+
# FIXTURES (16 bytes of IQ, a few sweep rows).
14+
# - this script collects SAMPLE DATASETS: real captures big enough to
15+
# be useful, with SigMF sidecars, organized as a sample library.
16+
#
17+
# SAFETY: strictly READ-ONLY. Receive and sweep only. Never transmits,
18+
# never writes firmware. Stays in RX mode throughout.
19+
#
20+
# Sizes are kept small by default so the samples can live in the repo
21+
# (a 0.5 s capture at 2 Msps is ~2 MB of int8 I/Q). Use --seconds /
22+
# --sample-rate to collect larger local datasets.
23+
#
24+
# Usage:
25+
# uv run python examples/collect_sample_data.py
26+
# uv run python examples/collect_sample_data.py --tools-dir "C:\hackrf-tools-windows"
27+
# uv run python examples/collect_sample_data.py --band fm --band ism433
28+
# uv run python examples/collect_sample_data.py --seconds 1.0 --sample-rate 4e6
29+
#
30+
# (Use `uv run` so the project environment with numpy is used.)
31+
##--------------------------------------------------------------------\
32+
33+
import argparse
34+
import datetime
35+
import os
36+
import sys
37+
38+
_HERE = os.path.dirname(os.path.abspath(__file__))
39+
for cand in (os.path.join(_HERE, "..", "src"), os.path.join(_HERE, "src")):
40+
if os.path.isdir(os.path.join(cand, "hackrfpy")):
41+
sys.path.insert(0, os.path.abspath(cand))
42+
break
43+
44+
from hackrfpy import HackRF, load_iq # noqa: E402
45+
from hackrfpy.exceptions import HackRFError # noqa: E402
46+
47+
try:
48+
import numpy as np # noqa: E402
49+
except ModuleNotFoundError:
50+
sys.stderr.write(
51+
"ERROR: numpy not available -- run through uv so the project env is "
52+
"used:\n uv run python examples/collect_sample_data.py [args]\n")
53+
sys.exit(1)
54+
55+
OUT_DIR = os.path.join(_HERE, "sample_data")
56+
57+
# Named band presets: an IQ capture at `center`, and (optionally) a sweep over
58+
# (sweep_min, sweep_max). All legal to RECEIVE; FM is the universal default.
59+
BANDS = {
60+
"fm": {
61+
"center": 98_000_000, "sweep": (88_000_000, 108_000_000),
62+
"desc": "FM broadcast band"},
63+
"airband": {
64+
"center": 124_000_000, "sweep": (118_000_000, 137_000_000),
65+
"desc": "VHF airband (AM voice)"},
66+
"ism433": {
67+
"center": 433_920_000, "sweep": (433_000_000, 435_000_000),
68+
"desc": "433 MHz ISM"},
69+
"ism915": {
70+
"center": 915_000_000, "sweep": (902_000_000, 928_000_000),
71+
"desc": "915 MHz ISM (US)"},
72+
"noaa": {
73+
"center": 137_500_000, "sweep": (137_000_000, 138_000_000),
74+
"desc": "NOAA weather satellite downlink"},
75+
}
76+
77+
78+
def _signal_summary(iq):
79+
# A quick, honest description of what was captured: mean power and whether
80+
# there's evident signal vs noise floor. Not DSP -- just a sanity readout.
81+
if len(iq) == 0:
82+
return "empty"
83+
power = float(np.mean(np.abs(iq) ** 2))
84+
peak = float(np.max(np.abs(iq)))
85+
db = 10 * np.log10(power + 1e-12)
86+
return f"mean {db:.1f} dBFS, peak |amp| {peak:.3f}"
87+
88+
89+
def collect_band(h, name, args):
90+
band = BANDS[name]
91+
os.makedirs(OUT_DIR, exist_ok=True)
92+
n = int(args.sample_rate * args.seconds)
93+
results = []
94+
95+
# ---- IQ capture (with SigMF sidecar via capture()) ----
96+
iq_path = os.path.join(OUT_DIR, f"{name}_{int(args.sample_rate/1e6)}Msps.iq")
97+
print(f"\n== {name}: IQ capture @ {band['center']/1e6:g} MHz "
98+
f"({args.seconds}s, {args.sample_rate/1e6:g} Msps) ==")
99+
try:
100+
h.capture(band["center"], args.sample_rate, num_samples=n,
101+
out=iq_path, sigmf=True)
102+
iq = load_iq(iq_path)
103+
size_mb = os.path.getsize(iq_path) / 1e6
104+
print(f" wrote {os.path.basename(iq_path)} "
105+
f"({size_mb:.1f} MB, {len(iq)} samples) -- {_signal_summary(iq)}")
106+
print(f" + {os.path.basename(iq_path).rsplit('.',1)[0]}.sigmf-meta")
107+
results.append(iq_path)
108+
except HackRFError as e:
109+
print(f" IQ capture failed: {e}", file=sys.stderr)
110+
111+
# ---- sweep dataset (CSV) ----
112+
if not args.no_sweep and band.get("sweep"):
113+
lo, hi = band["sweep"]
114+
sweep_path = os.path.join(OUT_DIR, f"{name}_sweep.csv")
115+
print(f" sweep {lo/1e6:g}-{hi/1e6:g} MHz ...")
116+
try:
117+
rows = h.sweep_collect(lo, hi, num_sweeps=args.sweep_count)
118+
with open(sweep_path, "w", newline="\n") as f:
119+
f.write("date,time,hz_low,hz_high,bin_width,num_samples,db...\n")
120+
for r in rows:
121+
f.write(", ".join(
122+
[r["date"], r["time"], str(r["hz_low"]),
123+
str(r["hz_high"]), f"{r['bin_width']:.2f}",
124+
str(r["num_samples"])]
125+
+ [f"{d:.2f}" for d in r["db"]]) + "\n")
126+
print(f" wrote {os.path.basename(sweep_path)} ({len(rows)} rows)")
127+
results.append(sweep_path)
128+
except HackRFError as e:
129+
print(f" sweep failed: {e}", file=sys.stderr)
130+
131+
return results
132+
133+
134+
def write_readme(collected, args, det):
135+
# A README for the sample library so the data is self-documenting.
136+
path = os.path.join(OUT_DIR, "README.md")
137+
with open(path, "w", newline="\n") as f:
138+
f.write("# hackrfpy sample data\n\n")
139+
f.write("Real recordings from a HackRF One, for trying the library "
140+
"and downstream processing without owning a board.\n\n")
141+
f.write(f"- collected: "
142+
f"{datetime.datetime.now().isoformat(timespec='seconds')}\n")
143+
f.write(f"- device firmware: {det['boards'][0].get('firmware')}\n")
144+
f.write(f"- tools: {det['tools_version']}\n")
145+
f.write(f"- sample rate: {args.sample_rate/1e6:g} Msps, "
146+
f"{args.seconds}s per IQ capture\n\n")
147+
f.write("Each `.iq` is interleaved int8 I/Q (HackRF native) with a "
148+
"`.sigmf-meta` sidecar describing frequency, rate, and gains. "
149+
"Load with:\n\n")
150+
f.write("```python\nfrom hackrfpy import load_iq, read_sigmf_meta\n"
151+
"iq = load_iq('fm_2Msps.iq')\n"
152+
"meta = read_sigmf_meta('fm_2Msps.iq')\n```\n\n")
153+
f.write("## Files\n\n")
154+
for p in collected:
155+
f.write(f"- `{os.path.basename(p)}`\n")
156+
print(f"\n wrote {os.path.relpath(path, _HERE)}")
157+
158+
159+
def main():
160+
p = argparse.ArgumentParser(
161+
description="Collect real sample datasets from a HackRF (READ-ONLY).")
162+
p.add_argument("--tools-dir", default=None)
163+
p.add_argument("--band", action="append", choices=list(BANDS),
164+
help="band(s) to collect; repeatable. Default: fm")
165+
p.add_argument("--seconds", type=float, default=0.5,
166+
help="capture duration per band (default 0.5s)")
167+
p.add_argument("--sample-rate", type=float, default=2e6,
168+
help="sample rate in sps (default 2e6, small + USB-friendly)")
169+
p.add_argument("--sweep-count", type=int, default=1,
170+
help="sweeps per band dataset (default 1)")
171+
p.add_argument("--no-sweep", action="store_true",
172+
help="IQ captures only, skip sweep datasets")
173+
args = p.parse_args()
174+
bands = args.band or ["fm"]
175+
176+
h = HackRF(tools_dir=args.tools_dir, verbose=False)
177+
print("== confirming a real board before collecting ==")
178+
det = h.detect()
179+
if not det["ready"]:
180+
print(f" NO USABLE HACKRF: {det['problem']}", file=sys.stderr)
181+
return 1
182+
print(f" ready: firmware {det['boards'][0].get('firmware')}")
183+
for w in det.get("warnings", []):
184+
print(f" ! {w}")
185+
186+
collected = []
187+
try:
188+
for name in bands:
189+
collected += collect_band(h, name, args)
190+
except KeyboardInterrupt:
191+
print("\ninterrupted", file=sys.stderr)
192+
if collected:
193+
write_readme(collected, args, det)
194+
total = sum(os.path.getsize(p) for p in collected
195+
if p.endswith(".iq")) / 1e6
196+
print(f"\n== done: {len(collected)} files, ~{total:.1f} MB of IQ "
197+
f"in {os.path.relpath(OUT_DIR, _HERE)} ==")
198+
return 0
199+
200+
201+
if __name__ == "__main__":
202+
sys.exit(main())
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# hackrfpy sample data
2+
3+
Real recordings from a HackRF One, for trying the library and downstream processing without owning a board.
4+
5+
- collected: 2026-06-15T13:25:40
6+
- device firmware: 2024.02.1 (API:1.08)
7+
- tools: git-b1dbb47
8+
- sample rate: 2 Msps, 0.5s per IQ capture
9+
10+
Each `.iq` is interleaved int8 I/Q (HackRF native) with a `.sigmf-meta` sidecar describing frequency, rate, and gains. Load with:
11+
12+
```python
13+
from hackrfpy import load_iq, read_sigmf_meta
14+
iq = load_iq('fm_2Msps.iq')
15+
meta = read_sigmf_meta('fm_2Msps.iq')
16+
```
17+
18+
## Files
19+
20+
- `fm_2Msps.iq`
21+
- `fm_sweep.csv`
1.91 MB
Binary file not shown.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"global": {
3+
"core:datatype": "ci8",
4+
"core:sample_rate": 2000000.0,
5+
"core:hw": "HackRF One",
6+
"core:version": "1.0.0",
7+
"core:recorder": "hackrfpy",
8+
"hackrf:lna_gain_db": 16,
9+
"hackrf:vga_gain_db": 20,
10+
"hackrf:amp_enabled": false
11+
},
12+
"captures": [
13+
{
14+
"core:sample_start": 0,
15+
"core:frequency": 98000000.0,
16+
"core:datetime": "2026-06-15T17:25:40.123618+00:00"
17+
}
18+
],
19+
"annotations": []
20+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
date,time,hz_low,hz_high,bin_width,num_samples,db...
2+
2026-06-15, 13:25:40.237131, 88000000, 93000000, 1000000.00, 20, -74.54, -60.18, -60.16, -61.93, -62.57
3+
2026-06-15, 13:25:40.237131, 98000000, 103000000, 1000000.00, 20, -68.76, -61.95, -59.54, -60.54, -66.19
4+
2026-06-15, 13:25:40.237131, 93000000, 98000000, 1000000.00, 20, -65.89, -68.06, -58.49, -57.89, -64.70
5+
2026-06-15, 13:25:40.237131, 103000000, 108000000, 1000000.00, 20, -65.80, -70.94, -64.81, -62.66, -62.84

0 commit comments

Comments
 (0)