Skip to content

Commit e0401e5

Browse files
committed
README checkpoint
1 parent 721eb69 commit e0401e5

1 file changed

Lines changed: 112 additions & 12 deletions

File tree

README.md

Lines changed: 112 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![License: GPL v2](https://img.shields.io/badge/License-GPL_v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
44
[![Python versions](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
55

6-
## An UNOFFICIAL Python CLI + scripting wrapper for the HackRF One
6+
## AN UNOFFICIAL Python CLI + scripting wrapper for the HackRF One
77

88
A non-GUI Python wrapper and command-line tool for the [HackRF One](https://greatscottgadgets.com/hackrf/one/) software-defined radio.
99

@@ -40,13 +40,16 @@ The primary GitHub: [https://github.com/LC-Linkous/hackRF_python](https://github
4040
* [Scan-then-Capture Pipeline](#scan-then-capture-pipeline)
4141
* [Transmitting](#transmitting)
4242
* [Reading Recordings Back](#reading-recordings-back)
43+
* [Sample Data (no board required)](#sample-data-no-board-required)
44+
* [Runnable Examples](#runnable-examples)
4345
* [Method Reference](#method-reference)
4446
* [Receive / Capture](#receive--capture)
4547
* [Sweep](#sweep)
4648
* [Transmit](#transmit)
4749
* [Info and Preflight](#info-and-preflight)
4850
* [Device Management (advanced)](#device-management-advanced)
4951
* [Decoding and File Helpers](#decoding-and-file-helpers)
52+
* [Power and Relative Calibration](#power-and-relative-calibration)
5053
* [Mode, Validation, and Feedback](#mode-validation-and-feedback)
5154
* [Device Capability Coverage](#device-capability-coverage)
5255
* [CLI Reference](#cli-reference)
@@ -107,7 +110,7 @@ cd hackrfpy
107110
# hackrfpy. Writes uv.lock + .python-version.
108111
uv sync
109112

110-
# add plotting deps (matplotlib + PyQt5) when running the examples
113+
# add plotting deps (matplotlib) when running the examples
111114
uv sync --extra plotting
112115
```
113116

@@ -148,14 +151,14 @@ If that prints a board (or at least runs), the binaries are reachable. If it is
148151

149152
The library itself depends only on **numpy**. It shells out to the `hackrf-tools` binaries (`hackrf_info`, `hackrf_transfer`, `hackrf_sweep`, and the device-management tools), which are a **system dependency**, not a pip one. There is no serial port and no `pyserial` involved — the HackRF presents as a USB device the binaries talk to.
150153

151-
The plotting examples additionally need matplotlib and PyQt5, grouped under the optional `[plotting]` extra:
154+
The plotting examples additionally need matplotlib, grouped under the optional `[plotting]` extra:
152155

153156
```bash
154157
pip install nvnapython # (illustrative) library only
155158
pip install "hackrfpy[plotting]" # library + example plotting deps
156159
```
157160

158-
On Linux, `PyQt5` is the matplotlib backend used to draw the example figures and must be installed to run them; on many Windows machines it is not required. Install it if in doubt.
161+
matplotlib draws the example figures using a native GUI backend (TkAgg on Windows and most platforms, which ships with Python — no extra install). If a live window doesn't appear, your matplotlib may have selected a non-interactive backend; set one explicitly with the `MPLBACKEND=TkAgg` environment variable before running.
159162

160163
Python 3.11+ is required (the library uses `tomllib` and modern typing). The examples default to common ISM/broadcast bands but take frequency arguments for other ranges.
161164

@@ -192,7 +195,15 @@ hackRF_python/ repo root (this README, CITATION, etc.)
192195
│ │ ├── capture_to_file.py
193196
│ │ ├── scan_then_capture.py
194197
│ │ ├── sweep_collect.py
195-
│ │ └── waterfall_realtime.py
198+
│ │ ├── waterfall_realtime.py
199+
│ │ ├── waterfall_persistent.py single-freq FFT waterfall (persistent RX)
200+
│ │ ├── device_explorer.py detect + identify + capabilities (read-only)
201+
│ │ ├── power_meter.py live dBFS meter via capture_callback
202+
│ │ ├── persistent_capture.py multi-segment capture, one process
203+
│ │ ├── benchmark.py measure decode/throughput/latency
204+
│ │ ├── calibrate.py calibration workflow (Levels 2-3)
205+
│ │ ├── collect_sample_data.py real sample-data collector (read-only)
206+
│ │ └── sample_data/ committed real recordings + SigMF + README
196207
│ └── tests/
197208
│ ├── conftest.py cross-platform stub-binary factory
198209
│ ├── fixtures/ frozen hackrf_info / sweep / iq samples
@@ -248,12 +259,14 @@ uv run pytest -m hardware
248259
uv run pytest --cov=hackrfpy --cov-report=term-missing
249260
```
250261

251-
> **Note:** this is a configured uv project, so `uv run pytest` uses the synced venv and editable install. If a stray `.venv` appears somewhere unexpected, you ran it from the wrong directory.
262+
> **Note:** this is a configured uv project, so `uv run pytest` uses the synced venv and editable install. **Run *scripts* the same way**`uv run python tests/collect_real_data.py ...`, not bare `python ...`. Bare `python` can pick up a different activated `.venv` (for example one at the repo root, outside the `hackrfpy/` project dir) that doesn't have the project's dependencies installed, producing a confusing `ModuleNotFoundError: No module named 'numpy'` even though numpy is declared. If you see uv warn that `VIRTUAL_ENV ... does not match the project environment`, that's the mismatch — prefer `uv run` so the right environment is always used.
252263
253264
The suite is split into hardware-free tests and tests marked `@pytest.mark.hardware`, which auto-skip when no board is detected. Hardware detection is intentionally **not cached**, so you can plug/unplug between runs.
254265

255266
**A note on cross-platform coverage:** the process-lifecycle tests (the riskiest code) use **cross-platform stub binaries**, not bash scripts, so they run on Windows — the platform this library targets. Stubs are generated by a factory in `conftest.py` that writes a small Python program plus a launcher (`.bat` on Windows, a shebang'd file on POSIX). This matters because the Windows interrupt path (`CTRL_BREAK_EVENT`, used to stop a running `hackrf_transfer`) is otherwise untested on the exact platform where it must work. Run `pytest tests/test_lifecycle_xplat.py` on Windows to prove the reap/stop machinery before attaching hardware.
256267

268+
**Hardware-validated parsing.** The parsers (`parse_info`, `parse_sweep_line`, IQ decode) are tested against frozen *verbatim output from a real HackRF One* — see `tests/fixtures/*_real.*` and `tests/test_real_output.py`. Collecting that real output surfaced behaviors no synthetic fixture had: a git-style tools version with no year, a field whose value sits on an indented continuation line, trailing free-text USB warnings, and **out-of-order sweep segments**. The hardware-marked tests (`test_hardware.py`) pass against a connected board. You can regenerate the real fixtures yourself with `tests/collect_real_data.py` (read-only).
269+
257270

258271
## Error Handling
259272

@@ -460,6 +473,52 @@ fs = meta["global"]["core:sample_rate"]
460473
fc = meta["captures"][0]["core:frequency"]
461474
```
462475

476+
### Sample Data (no board required)
477+
478+
The repo ships **real recordings** under `examples/sample_data/` so you can try
479+
the library — and downstream signal processing — without owning a HackRF. Each
480+
`.iq` is interleaved int8 I/Q with a `.sigmf-meta` sidecar, plus sweep CSVs and a
481+
provenance README noting the firmware/tools that produced them.
482+
483+
```python
484+
from hackrfpy import load_iq, read_sigmf_meta
485+
iq = load_iq("examples/sample_data/fm_2Msps.iq")
486+
meta = read_sigmf_meta("examples/sample_data/fm_2Msps.iq")
487+
```
488+
489+
To collect your own (read-only; never transmits), use the sample collector. It
490+
preflights for a board, then captures real IQ + sweep data per band with SigMF
491+
metadata:
492+
493+
```bash
494+
uv run python examples/collect_sample_data.py --band fm --band ism433
495+
```
496+
497+
Defaults are kept small (0.5 s at 2 Msps) so they can live in the repo; use
498+
`--seconds` / `--sample-rate` for larger local datasets. This is distinct from
499+
`tests/collect_real_data.py`, which freezes tiny verbatim slices as *parser test
500+
fixtures* rather than usable sample datasets.
501+
502+
### Runnable Examples
503+
504+
The `examples/` directory has end-to-end scripts you can run against a board (all read-only except where noted):
505+
506+
| Script | What it shows |
507+
|---|---|
508+
| `device_explorer.py` | The "first thing you run": `detect` + `identify` + `features`, reporting board, firmware, capabilities, and any device warnings. |
509+
| `capture_to_file.py` | Bounded capture to a file with a SigMF sidecar, then read it back. |
510+
| `power_meter.py` | Live dBFS power meter at one frequency via `capture_callback` — the callback API on a real stream, with the gain-normalized relative reading. |
511+
| `scan_then_capture.py` | Sweep a band, find the strongest bin, then capture there — a pipeline the class expresses that CLI tools can't. |
512+
| `sweep_collect.py` | One sweep across a band saved to CSV. |
513+
| `waterfall_realtime.py` | Live spectrum waterfall (needs the `[plotting]` extra). |
514+
| `waterfall_persistent.py` | Single-frequency FFT waterfall over time, driven by a persistent receiver — the complement to the sweep waterfall (one channel evolving vs. a wide band). Needs the `[plotting]` extra. |
515+
| `persistent_capture.py` | Collect many segments at one frequency from a single long-lived process (amortizes startup). |
516+
| `benchmark.py` | Measure decode throughput, sustained-rate drop behavior, and callback latency on your hardware. |
517+
| `calibrate.py` | Calibration workflow (Levels 2–3): derive an absolute-ish `offset_db` from a known reference and/or a frequency-response curve, saved to `calibration.json`. |
518+
| `collect_sample_data.py` | Collect real sample datasets into `examples/sample_data/`. |
519+
520+
Run any of them through uv so the project environment is used, e.g. `uv run python examples/device_explorer.py`.
521+
463522

464523
## Method Reference
465524

@@ -488,18 +547,46 @@ The `HackRF` class is composed from mixins. Methods build a `hackrf-tools` comma
488547
* **Signature:** `capture_stream(freq, sample_rate, **kwargs)`
489548
* **Returns:** a context manager yielding decoded `complex64` blocks; reaps `hackrf_transfer` on exit.
490549

550+
#### `capture_callback`
551+
* **Signature:** `capture_callback(freq, sample_rate, on_block, *, max_samples=None, max_blocks=None, **kwargs)`
552+
* **Returns:** total samples delivered.
553+
* **Notes:** callback-style receive — `on_block(iq, n_so_far)` fires with each decoded `complex64` block as it streams; return `False` to stop. This is the ergonomic analog of libhackrf's RX callback (the loop is inverted so calling code resembles the C-binding libraries), riding the same subprocess stream as `capture_stream`. The child is always reaped on exit. Stops on `False`, on `max_samples`/`max_blocks`, or when the source ends.
554+
555+
#### `scan_frequencies`
556+
* **Signature:** `scan_frequencies(freqs, sample_rate, num_samples, *, on_capture=None, **kwargs)`
557+
* **Returns:** `{freq: complex64}` dict, or `None` if `on_capture` is given.
558+
* **Notes:** sequentially captures `num_samples` at each frequency, retuning between them. Makes multi-frequency capture one call. **Not gapless** — each retune is a fresh `hackrf_transfer` with a short re-open (the binary can't retune mid-stream); pass `on_capture(freq, iq)` to process-and-discard instead of holding every array.
559+
560+
#### `open_receiver`
561+
* **Signature:** `open_receiver(freq, sample_rate, *, lna=16, vga=20, amp=False, baseband_bw=None, read_samples=131072)`
562+
* **Returns:** a `PersistentReceiver` (use as a context manager).
563+
* **Notes:** opens **one long-lived** `hackrf_transfer` you drain in segments, so you pay the ~startup cost **once** instead of per capture — the efficient path for collecting many segments over time. **Fixed-frequency by design** (the binary can't retune mid-stream); for multiple frequencies use `scan_frequencies` or `monitor_frequencies`. The receiver offers `read(n)` (exactly *n* `complex64`), `blocks()` (iterate raw decoded blocks), and `callback(on_block, max_samples=)`. The child is reaped on `__exit__` and registered on the atexit backstop. `read_samples` tunes the stdout read granularity (larger = higher sustained throughput).
564+
565+
```python
566+
with h.open_receiver(100e6, 8e6) as rx:
567+
a = rx.read(1_000_000) # exact count
568+
b = rx.read(1_000_000) # again, NO new process spin-up
569+
for block in rx.blocks(): # or iterate decoded blocks
570+
...
571+
```
572+
491573
### Sweep
492574

493575
#### `sweep`
494576
* **Builds:** `hackrf_sweep -f <lo:hi MHz> -l <lna> -g <vga> -a <0|1> [-w <bin_hz>] [-1] [-N <n>]`
495577
* **Signature:** `sweep(f_min_hz, f_max_hz, *, bin_width=None, lna=16, vga=20, amp=False, one_shot=False, num_sweeps=None, print_cmd=False)`
496578
* **Returns:** a generator of parsed rows: `{date, time, hz_low, hz_high, bin_width, num_samples, db: [...]}`. `None` for blank/garbled lines (filtered out).
497-
* **Notes:** band edges are taken in integer MHz; sub-MHz edges are snapped and a warning is printed. A single sweep over a wide band arrives as multiple rows sharing one timestamp.
579+
* **Notes:** band edges are taken in integer MHz; sub-MHz edges are snapped and a warning is printed. A single sweep over a wide band arrives as multiple rows sharing one timestamp. **Real `hackrf_sweep` does not emit segments in frequency order** — it interleaves them (e.g. 88, 98, 93, 103 MHz). To reconstruct a contiguous spectrum, group rows by timestamp and sort segments by `hz_low`; never assume the rows arrive low-to-high.
498580

499581
#### `sweep_collect`
500582
* **Signature:** `sweep_collect(f_min_hz, f_max_hz, num_sweeps=1, **kwargs)`
501583
* **Returns:** a list (bounded — relies on `-N` terminating the sweep).
502584

585+
#### `monitor_frequencies`
586+
* **Signature:** `monitor_frequencies(freqs_hz, *, span_hz=2_000_000, duration=None, on_update=None, lna=16, vga=20, amp=False)`
587+
* **Returns:** a list of `{freq_hz: power_db}` dicts (one per sweep pass), or `None` if `on_update` is given.
588+
* **Notes:** watch **power over time** at several frequencies, backed by `hackrf_sweep`'s fast internal retuning. Deliberately separate from `scan_frequencies`: that one returns **IQ samples** (per-frequency captures); this returns **power** (spectrum bins) and never yields IQ. Use it for "is there activity on these channels?" monitoring. `on_update(update)` returning `False` stops it.
589+
503590
#### `sweep_stream`
504591
* **Signature:** `sweep_stream(f_min_hz, f_max_hz, **kwargs)`
505592
* **Returns:** a context manager around the sweep generator; reaps `hackrf_sweep` on exit. Use for live/indefinite consumers.
@@ -585,12 +672,13 @@ Modes (`-m manual|frequency|time`), port assignment (`-a`/`-b`), frequency routi
585672

586673
```python
587674
h = HackRF()
588-
h.clock("-r") # read clock configuration
589-
h.clock("-o", "1") # enable clock output
590-
h.clock("-i") # read clock input status
675+
h.clock("-r", "3") # read settings for clock 3 (CLKOUT); -r needs a clock num
676+
h.clock("-a") # read settings for ALL clocks
677+
h.clock("-i") # get CLKIN status
678+
h.clock("-o", "1") # enable CLKOUT
591679
```
592680

593-
Refer to `hackrf_clock -h` for the supported arguments on your tools version (the flag set has varied across releases).
681+
Note that `-r` requires a clock number (e.g. `-r 3`); calling `clock("-r")` alone prints the tool's usage rather than reading anything. Refer to `hackrf_clock -h` for the full set on your tools version (it has varied across releases — newer builds add HackRF Pro P1/P2 connector signal selection).
594682

595683
#### Debug register access — `debug`
596684

@@ -634,6 +722,18 @@ The `confirm=True` requirement exists because flashing firmware is the single mo
634722
| `parse_sweep_line(line)` (staticmethod) | one `hackrf_sweep` CSV row → dict, or `None`. |
635723
| `parse_freq(txt)` | `"433.92M"`, `"1.09G"`, `"2.5k"`, `"100MHz"`, or plain Hz → float Hz. Module-level. |
636724

725+
### Power and Relative Calibration
726+
727+
The HackRF is **not a calibrated instrument** — its raw dBFS amplitude is relative to ADC full scale, not power at the antenna, and it depends on the gain you set. These helpers (Level 1) make readings *consistent*; turning them into approximate dBm (Levels 2–3) is a hardware-and-reference workflow shown in `examples/calibrate.py`.
728+
729+
| Method | Purpose |
730+
|---|---|
731+
| `power_dbfs(iq)` | mean power of a `complex64` block in dBFS (≤ 0; raw, uncalibrated). |
732+
| `gain_db(lna, vga, amp)` | total RX gain through the chain in dB. The quantity that makes a raw dBFS reading ambiguous. |
733+
| `relative_power_db(iq_or_dbfs, *, lna=, vga=, amp=, offset_db=0, freq_hz=, freq_correction=)` | gain-normalized power: subtracts the gain chain so readings at *different gains are comparable*. Gains default from `last_params`. Pass `offset_db` (from a calibration run) to approximate dBm, and a `freq_correction(freq)→dB` callable to flatten the front-end response. |
734+
735+
The key property: the **same physical signal reads the same `relative_power_db` value regardless of gain**, where raw dBFS would shift by the gain delta. The library does only pure math here — it never invents reference data or claims a dBm it can't back up. To derive `offset_db` (feed a known power) or a `freq_correction` curve (sweep a flat source), run `examples/calibrate.py`; it saves a `calibration.json` you feed back in.
736+
637737
### Mode, Validation, and Feedback
638738

639739
| Method | Purpose |
@@ -798,4 +898,4 @@ This project is licensed under the GNU General Public License v2.0 or later. See
798898

799899
The **code in this repository** is released under GPL-2.0-or-later. This licensing does NOT take priority over the official HackRF releases or the decisions of Great Scott Gadgets, and does NOT apply to their products or firmware.
800900

801-
This software is released **AS-IS** — there may be bugs, especially under active development. It is **UNOFFICIAL**: Great Scott Gadgets does not support, maintain, or bear responsibility for it. You are responsible for operating your hardware safely and legally, particularly when transmitting.
901+
This software is released **AS-IS** — there may be bugs, especially under active development. It is **UNOFFICIAL**: Great Scott Gadgets does not support, maintain, or bear responsibility for it. You are responsible for operating your hardware safely and legally, particularly when transmitting.

0 commit comments

Comments
 (0)