hackrfpy 1.0.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/PKG-INFO +55 -13
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/README.md +53 -10
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/pyproject.toml +36 -4
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/__init__.py +8 -2
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_commands/capture.py +86 -24
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_commands/device.py +48 -17
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_commands/info.py +39 -13
- hackrfpy-1.2.0/src/hackrfpy/_commands/sweep.py +373 -0
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_commands/transmit.py +43 -10
- hackrfpy-1.2.0/src/hackrfpy/_host.py +80 -0
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_receiver.py +80 -27
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_stream_ctx.py +15 -3
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/cli.py +121 -14
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/constants.py +15 -1
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/core.py +507 -87
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/exceptions.py +3 -1
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/presets.py +12 -11
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/sigmf.py +25 -5
- hackrfpy-1.0.0/src/hackrfpy/_commands/sweep.py +0 -219
- hackrfpy-1.0.0/src/hackrfpy/comparison.md +0 -175
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/LICENSE +0 -0
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/_commands/__init__.py +0 -0
- {hackrfpy-1.0.0 → hackrfpy-1.2.0}/src/hackrfpy/py.typed +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hackrfpy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A self-contained CLI + scriptable wrapper for the HackRF One, built on the hackrf-tools binaries (no libhackrf bindings required).
|
|
5
5
|
Keywords: hackrf,sdr,rf,spectrum,sigmf
|
|
6
6
|
Author: LC-Linkous
|
|
7
7
|
License-Expression: GPL-2.0-or-later
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -27,6 +26,7 @@ Description-Content-Type: text/markdown
|
|
|
27
26
|
|
|
28
27
|
**An Unofficial Python CLI + Scripting Wrapper for the HackRF One that works on Windows**
|
|
29
28
|
|
|
29
|
+
[](https://github.com/LC-Linkous/hackRF_python/actions/workflows/tests.yml)
|
|
30
30
|
[](https://badge.fury.io/py/hackrfpy)
|
|
31
31
|
[](https://pypi.org/project/hackrfpy/)
|
|
32
32
|
[](https://pypi.org/project/hackrfpy/)
|
|
@@ -43,13 +43,27 @@ This repository uses official resources and documentation but is **NOT** endorse
|
|
|
43
43
|
|
|
44
44
|
- **Device Discovery** — detect and identify connected HackRF boards, report firmware and identity
|
|
45
45
|
- **IQ Capture** — bounded, timed, streaming, or callback-style receive; decoded to normalized `complex64`
|
|
46
|
-
- **Spectrum Sweep** — collect or stream `hackrf_sweep` output across a frequency range
|
|
46
|
+
- **Spectrum Sweep** — collect or stream `hackrf_sweep` output across a frequency range, plus multi-frequency power monitoring over time
|
|
47
47
|
- **Transmit** — file playback and constant-wave test mode, behind a deliberate TX-mode gate
|
|
48
48
|
- **Operating Envelope** — per-parameter range checks and gain snapping against the device's real steps
|
|
49
49
|
- **SigMF Recordings** — self-describing `.iq` captures with metadata sidecars
|
|
50
50
|
- **Error Handling** — a typed exception hierarchy and verbose output options
|
|
51
|
+
- **Lifecycle Safety** — clean interrupts on both platforms, an `atexit` backstop, and an OS dead-man (pdeathsig / Job Object) so even a hard-killed script cannot orphan a transmitter
|
|
51
52
|
- **CLI** — the `hrf` command-line shell over the full API
|
|
52
53
|
|
|
54
|
+
## Platform support
|
|
55
|
+
|
|
56
|
+
hackrfpy is developed and tested on **Windows**, and has also been verified on Debian Linux with real hardware. Running the `hackrf-tools` binaries as subprocesses — rather than binding to `libhackrf` through a C extension — is what makes that portability cheap: there is no compiler or build step on any platform, and each OS runs its own native tools. Process control is handled per-platform (`SIGINT` on POSIX, `CTRL_BREAK` on Windows), with the interrupt, flush, and dead-man paths covered by tests on every OS, no skips.
|
|
57
|
+
|
|
58
|
+
A platform is called **tested** here only after the full suite (hardware tests included) passes against a real board:
|
|
59
|
+
|
|
60
|
+
- **Windows** — the primary development platform, verified continuously against real hardware.
|
|
61
|
+
- **Linux** — verified 2026-09-19 on Debian 12 (`hackrf` 2022.09.1, firmware 2024.02.1): full suite, 227/227. Mechanics additionally exercised against tools 2023.01.1 on Ubuntu, so both packaged tools versions are known-good. Setup notes for Debian-family systems are in the [main repository README](https://github.com/LC-Linkous/hackRF_python#linux-setup-debian-family).
|
|
62
|
+
|
|
63
|
+
**macOS remains experimental**: the mechanics are CI-tested there, but board-attached operation is unverified. If you run it with a board, please [open an issue](https://github.com/LC-Linkous/hackRF_python/issues) — a passing hardware suite is what promotes a platform, and the bar is not a formality: the Linux verification run surfaced and fixed two real process-lifecycle bugs.
|
|
64
|
+
|
|
65
|
+
One caveat that applies everywhere: `tools_dir` must point at binaries built for the OS you are on — the Windows `.EXE` bundle will never run on Linux, and vice versa.
|
|
66
|
+
|
|
53
67
|
## Installation
|
|
54
68
|
|
|
55
69
|
```bash
|
|
@@ -64,13 +78,13 @@ pip install "hackrfpy[plotting]"
|
|
|
64
78
|
|
|
65
79
|
Python 3.11+ is required.
|
|
66
80
|
|
|
67
|
-
**You also need the `hackrf-tools` binaries**, which are *not* a pip dependency — they are installed at the OS level
|
|
81
|
+
**You also need the `hackrf-tools` binaries**, which are *not* a pip dependency — they are installed separately at the OS level. hackrfpy locates them on your `PATH` (or via a configured `tools_dir`).
|
|
68
82
|
|
|
69
|
-
- **
|
|
70
|
-
- **
|
|
71
|
-
- **
|
|
83
|
+
- **Windows** — *tested.* The tools are published as CI build artifacts under the [Actions tab](https://github.com/greatscottgadgets/hackrf/actions) of the HackRF repo; see the main repository README for the step-by-step.
|
|
84
|
+
- **Linux** — *tested.* `sudo apt install hackrf` (or your distribution's equivalent).
|
|
85
|
+
- **macOS** — *experimental, see [Platform support](#platform-support).* `brew install hackrf`.
|
|
72
86
|
|
|
73
|
-
Verify the
|
|
87
|
+
Verify the tools are installed with `hackrf_info`.
|
|
74
88
|
|
|
75
89
|
## Quick Start
|
|
76
90
|
|
|
@@ -104,9 +118,20 @@ for r in rows:
|
|
|
104
118
|
print(r["hz_low"], r["hz_high"], min(r["db"]), max(r["db"]))
|
|
105
119
|
```
|
|
106
120
|
|
|
121
|
+
The same operations are available from the shell via the `hrf` entry point:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
hrf detect # is a board attached and ready?
|
|
125
|
+
hrf rx -f 433.92M -s 8M -n 2000000 -o capture.iq
|
|
126
|
+
hrf sweep --f-min 88M --f-max 108M
|
|
127
|
+
hrf monitor 98.1M 103.7M -d 10 # power over time on several frequencies
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Frequencies accept unit suffixes (`433.92M`, `1.09G`), and most commands take `--print-cmd` to show the underlying `hackrf_*` invocation without running it. The full CLI reference is in the repository README.
|
|
131
|
+
|
|
107
132
|
## Transmitting
|
|
108
133
|
|
|
109
|
-
Transmit is gated behind an explicit mode switch, because an accidental transmit is the one operation that can damage equipment or break the law
|
|
134
|
+
Transmit is gated behind an explicit mode switch, because an accidental transmit is the one operation that can damage equipment (or break the law)
|
|
110
135
|
|
|
111
136
|
```python
|
|
112
137
|
from hackrfpy import HackRF
|
|
@@ -116,8 +141,18 @@ h.set_mode("tx") # prints the TX-mode safety banner
|
|
|
116
141
|
h.transmit(433.92e6, 8e6, "signal.iq", txvga=20)
|
|
117
142
|
```
|
|
118
143
|
|
|
144
|
+
A bounded constant-wave test tone is available without a source file: `h.transmit_cw(433.92e6, 2e6, duration=2)`, or `hrf tx --cw -f 433.92M -s 2M -d 2` from the CLI (the duration is mandatory there — an unbounded carrier is exactly the risk the gates exist to prevent). Behind the mode gate sit an `atexit` backstop and an OS dead-man, so even a hard-killed script cannot leave a transmitter on the air.
|
|
145
|
+
|
|
119
146
|
**Transmitting is regulated.** You are responsible for operating within the law and within your equipment's limits.
|
|
120
147
|
|
|
148
|
+
## Thread safety
|
|
149
|
+
|
|
150
|
+
A `HackRF` instance is **not** safe to share across threads: methods mutate
|
|
151
|
+
per-instance state (`last_params`, the persisted operating mode, logging
|
|
152
|
+
wiring) without locks. Instances are cheap -- the constructor touches no
|
|
153
|
+
hardware -- so create one per thread, or confine all hackrfpy calls to a
|
|
154
|
+
single worker thread.
|
|
155
|
+
|
|
121
156
|
## Examples
|
|
122
157
|
|
|
123
158
|
The [main GitHub repository](https://github.com/LC-Linkous/hackRF_python) provides runnable examples, grouped by what they demonstrate.
|
|
@@ -129,9 +164,10 @@ The [main GitHub repository](https://github.com/LC-Linkous/hackRF_python) provid
|
|
|
129
164
|
|
|
130
165
|
**Acquisition**
|
|
131
166
|
|
|
132
|
-
- `persistent_capture.py` —
|
|
167
|
+
- `persistent_capture.py` — gapless back-to-back segments at one frequency from a single long-lived receive process (contrast with `capture(segment_secs=...)`, whose files have a short re-open gap between them)
|
|
133
168
|
- `power_meter.py` — live dBFS power meter at one frequency via the callback API
|
|
134
169
|
- `scan_then_capture.py` — sweep a band, find the strongest bin, then capture there
|
|
170
|
+
- `channel_monitor.py` — live power meter on several frequencies at once via `monitor_frequencies` (one continuous sweep, no plotting extra needed)
|
|
135
171
|
|
|
136
172
|
**Sweep and plotting**
|
|
137
173
|
|
|
@@ -146,14 +182,19 @@ The [main GitHub repository](https://github.com/LC-Linkous/hackRF_python) provid
|
|
|
146
182
|
|
|
147
183
|
**Sample data**
|
|
148
184
|
|
|
149
|
-
- `collect_sample_data.py` — collect real IQ + sweep datasets (read-only; never transmits)
|
|
185
|
+
- `collect_sample_data.py` — collect real IQ + sweep datasets with per-capture validation (read-only; never transmits)
|
|
186
|
+
- `fm_demod_to_wav.py` — demodulate a captured FM broadcast IQ file to an audible mono WAV (numpy + stdlib only; file processing, never touches the device)
|
|
187
|
+
|
|
188
|
+
**Transmit**
|
|
189
|
+
|
|
190
|
+
- `tx_test_tone.py` — the one transmitting example: a bounded CW test tone behind the TX-mode gate, with `--print-cmd` dry-run
|
|
150
191
|
|
|
151
192
|
> Most plotting examples require the optional plotting dependencies:
|
|
152
193
|
> `pip install "hackrfpy[plotting]"`
|
|
153
194
|
|
|
154
195
|
## Documentation
|
|
155
196
|
|
|
156
|
-
|
|
197
|
+
Every public method carries a docstring: `help(hackrfpy.HackRF)` or `python -m pydoc hackrfpy` is the offline method reference, and a test gates the whole surface so it cannot drift. For the narrative documentation, the CLI reference, and the operating envelope:
|
|
157
198
|
|
|
158
199
|
- **Library GitHub repository**: [https://github.com/LC-Linkous/hackRF_python/](https://github.com/LC-Linkous/hackRF_python/)
|
|
159
200
|
- **Official HackRF documentation**: [https://hackrf.readthedocs.io/](https://hackrf.readthedocs.io/) (not associated with this library)
|
|
@@ -163,6 +204,7 @@ For comprehensive documentation, the full method reference, the CLI reference, a
|
|
|
163
204
|
This is an unofficial community project. Contributions welcome!
|
|
164
205
|
|
|
165
206
|
- Report bugs and request features on [GitHub](https://github.com/LC-Linkous/hackRF_python)
|
|
207
|
+
- If you run the library on Linux or macOS, reports from real hardware are especially welcome (see [Platform support](#platform-support))
|
|
166
208
|
- For device information and OFFICIAL resources, see [https://hackrf.readthedocs.io/](https://hackrf.readthedocs.io/)
|
|
167
209
|
- Please do **NOT** request features or report bugs to Great Scott Gadgets or the HackRF project! This is an unofficial project and they do not maintain it.
|
|
168
210
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**An Unofficial Python CLI + Scripting Wrapper for the HackRF One that works on Windows**
|
|
4
4
|
|
|
5
|
+
[](https://github.com/LC-Linkous/hackRF_python/actions/workflows/tests.yml)
|
|
5
6
|
[](https://badge.fury.io/py/hackrfpy)
|
|
6
7
|
[](https://pypi.org/project/hackrfpy/)
|
|
7
8
|
[](https://pypi.org/project/hackrfpy/)
|
|
@@ -18,13 +19,27 @@ This repository uses official resources and documentation but is **NOT** endorse
|
|
|
18
19
|
|
|
19
20
|
- **Device Discovery** — detect and identify connected HackRF boards, report firmware and identity
|
|
20
21
|
- **IQ Capture** — bounded, timed, streaming, or callback-style receive; decoded to normalized `complex64`
|
|
21
|
-
- **Spectrum Sweep** — collect or stream `hackrf_sweep` output across a frequency range
|
|
22
|
+
- **Spectrum Sweep** — collect or stream `hackrf_sweep` output across a frequency range, plus multi-frequency power monitoring over time
|
|
22
23
|
- **Transmit** — file playback and constant-wave test mode, behind a deliberate TX-mode gate
|
|
23
24
|
- **Operating Envelope** — per-parameter range checks and gain snapping against the device's real steps
|
|
24
25
|
- **SigMF Recordings** — self-describing `.iq` captures with metadata sidecars
|
|
25
26
|
- **Error Handling** — a typed exception hierarchy and verbose output options
|
|
27
|
+
- **Lifecycle Safety** — clean interrupts on both platforms, an `atexit` backstop, and an OS dead-man (pdeathsig / Job Object) so even a hard-killed script cannot orphan a transmitter
|
|
26
28
|
- **CLI** — the `hrf` command-line shell over the full API
|
|
27
29
|
|
|
30
|
+
## Platform support
|
|
31
|
+
|
|
32
|
+
hackrfpy is developed and tested on **Windows**, and has also been verified on Debian Linux with real hardware. Running the `hackrf-tools` binaries as subprocesses — rather than binding to `libhackrf` through a C extension — is what makes that portability cheap: there is no compiler or build step on any platform, and each OS runs its own native tools. Process control is handled per-platform (`SIGINT` on POSIX, `CTRL_BREAK` on Windows), with the interrupt, flush, and dead-man paths covered by tests on every OS, no skips.
|
|
33
|
+
|
|
34
|
+
A platform is called **tested** here only after the full suite (hardware tests included) passes against a real board:
|
|
35
|
+
|
|
36
|
+
- **Windows** — the primary development platform, verified continuously against real hardware.
|
|
37
|
+
- **Linux** — verified 2026-09-19 on Debian 12 (`hackrf` 2022.09.1, firmware 2024.02.1): full suite, 227/227. Mechanics additionally exercised against tools 2023.01.1 on Ubuntu, so both packaged tools versions are known-good. Setup notes for Debian-family systems are in the [main repository README](https://github.com/LC-Linkous/hackRF_python#linux-setup-debian-family).
|
|
38
|
+
|
|
39
|
+
**macOS remains experimental**: the mechanics are CI-tested there, but board-attached operation is unverified. If you run it with a board, please [open an issue](https://github.com/LC-Linkous/hackRF_python/issues) — a passing hardware suite is what promotes a platform, and the bar is not a formality: the Linux verification run surfaced and fixed two real process-lifecycle bugs.
|
|
40
|
+
|
|
41
|
+
One caveat that applies everywhere: `tools_dir` must point at binaries built for the OS you are on — the Windows `.EXE` bundle will never run on Linux, and vice versa.
|
|
42
|
+
|
|
28
43
|
## Installation
|
|
29
44
|
|
|
30
45
|
```bash
|
|
@@ -39,13 +54,13 @@ pip install "hackrfpy[plotting]"
|
|
|
39
54
|
|
|
40
55
|
Python 3.11+ is required.
|
|
41
56
|
|
|
42
|
-
**You also need the `hackrf-tools` binaries**, which are *not* a pip dependency — they are installed at the OS level
|
|
57
|
+
**You also need the `hackrf-tools` binaries**, which are *not* a pip dependency — they are installed separately at the OS level. hackrfpy locates them on your `PATH` (or via a configured `tools_dir`).
|
|
43
58
|
|
|
44
|
-
- **
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
59
|
+
- **Windows** — *tested.* The tools are published as CI build artifacts under the [Actions tab](https://github.com/greatscottgadgets/hackrf/actions) of the HackRF repo; see the main repository README for the step-by-step.
|
|
60
|
+
- **Linux** — *tested.* `sudo apt install hackrf` (or your distribution's equivalent).
|
|
61
|
+
- **macOS** — *experimental, see [Platform support](#platform-support).* `brew install hackrf`.
|
|
47
62
|
|
|
48
|
-
Verify the
|
|
63
|
+
Verify the tools are installed with `hackrf_info`.
|
|
49
64
|
|
|
50
65
|
## Quick Start
|
|
51
66
|
|
|
@@ -79,9 +94,20 @@ for r in rows:
|
|
|
79
94
|
print(r["hz_low"], r["hz_high"], min(r["db"]), max(r["db"]))
|
|
80
95
|
```
|
|
81
96
|
|
|
97
|
+
The same operations are available from the shell via the `hrf` entry point:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
hrf detect # is a board attached and ready?
|
|
101
|
+
hrf rx -f 433.92M -s 8M -n 2000000 -o capture.iq
|
|
102
|
+
hrf sweep --f-min 88M --f-max 108M
|
|
103
|
+
hrf monitor 98.1M 103.7M -d 10 # power over time on several frequencies
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Frequencies accept unit suffixes (`433.92M`, `1.09G`), and most commands take `--print-cmd` to show the underlying `hackrf_*` invocation without running it. The full CLI reference is in the repository README.
|
|
107
|
+
|
|
82
108
|
## Transmitting
|
|
83
109
|
|
|
84
|
-
Transmit is gated behind an explicit mode switch, because an accidental transmit is the one operation that can damage equipment or break the law
|
|
110
|
+
Transmit is gated behind an explicit mode switch, because an accidental transmit is the one operation that can damage equipment (or break the law)
|
|
85
111
|
|
|
86
112
|
```python
|
|
87
113
|
from hackrfpy import HackRF
|
|
@@ -91,8 +117,18 @@ h.set_mode("tx") # prints the TX-mode safety banner
|
|
|
91
117
|
h.transmit(433.92e6, 8e6, "signal.iq", txvga=20)
|
|
92
118
|
```
|
|
93
119
|
|
|
120
|
+
A bounded constant-wave test tone is available without a source file: `h.transmit_cw(433.92e6, 2e6, duration=2)`, or `hrf tx --cw -f 433.92M -s 2M -d 2` from the CLI (the duration is mandatory there — an unbounded carrier is exactly the risk the gates exist to prevent). Behind the mode gate sit an `atexit` backstop and an OS dead-man, so even a hard-killed script cannot leave a transmitter on the air.
|
|
121
|
+
|
|
94
122
|
**Transmitting is regulated.** You are responsible for operating within the law and within your equipment's limits.
|
|
95
123
|
|
|
124
|
+
## Thread safety
|
|
125
|
+
|
|
126
|
+
A `HackRF` instance is **not** safe to share across threads: methods mutate
|
|
127
|
+
per-instance state (`last_params`, the persisted operating mode, logging
|
|
128
|
+
wiring) without locks. Instances are cheap -- the constructor touches no
|
|
129
|
+
hardware -- so create one per thread, or confine all hackrfpy calls to a
|
|
130
|
+
single worker thread.
|
|
131
|
+
|
|
96
132
|
## Examples
|
|
97
133
|
|
|
98
134
|
The [main GitHub repository](https://github.com/LC-Linkous/hackRF_python) provides runnable examples, grouped by what they demonstrate.
|
|
@@ -104,9 +140,10 @@ The [main GitHub repository](https://github.com/LC-Linkous/hackRF_python) provid
|
|
|
104
140
|
|
|
105
141
|
**Acquisition**
|
|
106
142
|
|
|
107
|
-
- `persistent_capture.py` —
|
|
143
|
+
- `persistent_capture.py` — gapless back-to-back segments at one frequency from a single long-lived receive process (contrast with `capture(segment_secs=...)`, whose files have a short re-open gap between them)
|
|
108
144
|
- `power_meter.py` — live dBFS power meter at one frequency via the callback API
|
|
109
145
|
- `scan_then_capture.py` — sweep a band, find the strongest bin, then capture there
|
|
146
|
+
- `channel_monitor.py` — live power meter on several frequencies at once via `monitor_frequencies` (one continuous sweep, no plotting extra needed)
|
|
110
147
|
|
|
111
148
|
**Sweep and plotting**
|
|
112
149
|
|
|
@@ -121,14 +158,19 @@ The [main GitHub repository](https://github.com/LC-Linkous/hackRF_python) provid
|
|
|
121
158
|
|
|
122
159
|
**Sample data**
|
|
123
160
|
|
|
124
|
-
- `collect_sample_data.py` — collect real IQ + sweep datasets (read-only; never transmits)
|
|
161
|
+
- `collect_sample_data.py` — collect real IQ + sweep datasets with per-capture validation (read-only; never transmits)
|
|
162
|
+
- `fm_demod_to_wav.py` — demodulate a captured FM broadcast IQ file to an audible mono WAV (numpy + stdlib only; file processing, never touches the device)
|
|
163
|
+
|
|
164
|
+
**Transmit**
|
|
165
|
+
|
|
166
|
+
- `tx_test_tone.py` — the one transmitting example: a bounded CW test tone behind the TX-mode gate, with `--print-cmd` dry-run
|
|
125
167
|
|
|
126
168
|
> Most plotting examples require the optional plotting dependencies:
|
|
127
169
|
> `pip install "hackrfpy[plotting]"`
|
|
128
170
|
|
|
129
171
|
## Documentation
|
|
130
172
|
|
|
131
|
-
|
|
173
|
+
Every public method carries a docstring: `help(hackrfpy.HackRF)` or `python -m pydoc hackrfpy` is the offline method reference, and a test gates the whole surface so it cannot drift. For the narrative documentation, the CLI reference, and the operating envelope:
|
|
132
174
|
|
|
133
175
|
- **Library GitHub repository**: [https://github.com/LC-Linkous/hackRF_python/](https://github.com/LC-Linkous/hackRF_python/)
|
|
134
176
|
- **Official HackRF documentation**: [https://hackrf.readthedocs.io/](https://hackrf.readthedocs.io/) (not associated with this library)
|
|
@@ -138,6 +180,7 @@ For comprehensive documentation, the full method reference, the CLI reference, a
|
|
|
138
180
|
This is an unofficial community project. Contributions welcome!
|
|
139
181
|
|
|
140
182
|
- Report bugs and request features on [GitHub](https://github.com/LC-Linkous/hackRF_python)
|
|
183
|
+
- If you run the library on Linux or macOS, reports from real hardware are especially welcome (see [Platform support](#platform-support))
|
|
141
184
|
- For device information and OFFICIAL resources, see [https://hackrf.readthedocs.io/](https://hackrf.readthedocs.io/)
|
|
142
185
|
- Please do **NOT** request features or report bugs to Great Scott Gadgets or the HackRF project! This is an unofficial project and they do not maintain it.
|
|
143
186
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hackrfpy"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "A self-contained CLI + scriptable wrapper for the HackRF One, built on the hackrf-tools binaries (no libhackrf bindings required)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -9,11 +9,10 @@ license-files = ["LICENSE*"]
|
|
|
9
9
|
authors = [{ name = "LC-Linkous" }]
|
|
10
10
|
keywords = ["hackrf", "sdr", "rf", "spectrum", "sigmf"]
|
|
11
11
|
classifiers = [
|
|
12
|
-
"Development Status ::
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
13
|
"Intended Audience :: Developers",
|
|
14
14
|
"License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)",
|
|
15
15
|
"Operating System :: Microsoft :: Windows",
|
|
16
|
-
"Operating System :: OS Independent",
|
|
17
16
|
"Programming Language :: Python :: 3.11",
|
|
18
17
|
"Programming Language :: Python :: 3.12",
|
|
19
18
|
"Programming Language :: Python :: 3.13",
|
|
@@ -47,10 +46,43 @@ dev = [
|
|
|
47
46
|
"pytest-cov",
|
|
48
47
|
"ruff",
|
|
49
48
|
"mypy",
|
|
49
|
+
"sigmf>=1.2", # official validator: spec compliance is tested, not trusted
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
[tool.pytest.ini_options]
|
|
53
53
|
markers = [
|
|
54
54
|
"hardware: tests that require a connected HackRF (self-skip if absent)",
|
|
55
55
|
]
|
|
56
|
-
testpaths = ["tests"]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
|
|
58
|
+
[tool.mypy]
|
|
59
|
+
python_version = "3.11"
|
|
60
|
+
files = ["src/hackrfpy"]
|
|
61
|
+
# The point of this config: the package ships a py.typed marker, which promises
|
|
62
|
+
# downstream type-checkers that the inline annotations are real. These settings
|
|
63
|
+
# are what make that promise true -- every def in the shipped package must be
|
|
64
|
+
# annotated, or CI fails.
|
|
65
|
+
disallow_untyped_defs = true
|
|
66
|
+
disallow_incomplete_defs = true
|
|
67
|
+
check_untyped_defs = true
|
|
68
|
+
no_implicit_optional = true
|
|
69
|
+
warn_redundant_casts = true
|
|
70
|
+
warn_unused_ignores = true
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
# Match the codebase's existing style rather than reformatting it.
|
|
74
|
+
line-length = 100
|
|
75
|
+
target-version = "py311"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
# Default ruleset (pyflakes + a pycodestyle subset). Kept deliberately small:
|
|
79
|
+
# this gates CI, so every enabled rule should be one we actually want to block
|
|
80
|
+
# a merge on.
|
|
81
|
+
select = ["E", "F", "W"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.per-file-ignores]
|
|
84
|
+
# E741 ("ambiguous variable name l"): `l` reads naturally as "line" in the
|
|
85
|
+
# sweep-line parsing comprehensions. Cosmetic only, and renaming would churn
|
|
86
|
+
# working parser tests.
|
|
87
|
+
"tests/test_parsing.py" = ["E741"]
|
|
88
|
+
"tests/test_real_output.py" = ["E741"]
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# hackrfpy 'src/hackrfpy/__init__.py'
|
|
4
4
|
# Public API surface.
|
|
5
5
|
##--------------------------------------------------------------------\
|
|
6
|
-
from .core import HackRF, load_iq, parse_freq
|
|
6
|
+
from .core import HackRF, load_iq, encode_iq, parse_freq
|
|
7
7
|
from .sigmf import write_sigmf_meta, read_sigmf_meta
|
|
8
8
|
from .exceptions import (
|
|
9
9
|
HackRFError, HackRFValueError, HackRFModeError,
|
|
@@ -11,8 +11,14 @@ from .exceptions import (
|
|
|
11
11
|
)
|
|
12
12
|
from . import constants
|
|
13
13
|
|
|
14
|
+
try:
|
|
15
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
16
|
+
__version__ = _pkg_version("hackrfpy")
|
|
17
|
+
except PackageNotFoundError: # running from a checkout
|
|
18
|
+
__version__ = "0.0.0+unknown"
|
|
19
|
+
|
|
14
20
|
__all__ = [
|
|
15
|
-
"HackRF", "load_iq", "parse_freq", "constants",
|
|
21
|
+
"HackRF", "load_iq", "encode_iq", "parse_freq", "constants", "__version__",
|
|
16
22
|
"write_sigmf_meta", "read_sigmf_meta",
|
|
17
23
|
"HackRFError", "HackRFValueError", "HackRFModeError",
|
|
18
24
|
"HackRFDeviceError", "HackRFEnvironmentError",
|
|
@@ -18,18 +18,38 @@
|
|
|
18
18
|
# Author(s): <you>
|
|
19
19
|
##--------------------------------------------------------------------\
|
|
20
20
|
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
21
23
|
import os
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
22
27
|
|
|
23
28
|
from .. import constants as C
|
|
29
|
+
from .._host import HostOps
|
|
24
30
|
from ..sigmf import write_sigmf_meta
|
|
25
31
|
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from .._receiver import PersistentReceiver
|
|
34
|
+
from .._stream_ctx import StreamCtx
|
|
35
|
+
|
|
26
36
|
|
|
27
|
-
class CaptureMixin:
|
|
28
|
-
def capture(self, freq, sample_rate, *,
|
|
29
|
-
|
|
30
|
-
lna=16, vga
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
class CaptureMixin(HostOps):
|
|
38
|
+
def capture(self, freq: float, sample_rate: float, *,
|
|
39
|
+
out: str = "capture.iq", num_samples: int | None = None,
|
|
40
|
+
duration: float | None = None, lna: int = 16, vga: int = 20,
|
|
41
|
+
amp: bool = False, bias_tee: bool = False,
|
|
42
|
+
baseband_bw: float | None = None, to_stdout: bool = False,
|
|
43
|
+
sigmf: bool = True, segment_secs: float | None = None,
|
|
44
|
+
print_cmd: bool = False) -> Any:
|
|
45
|
+
"""Receive IQ at freq/sample_rate into a file (with SigMF sidecar).
|
|
46
|
+
|
|
47
|
+
Bound with num_samples or duration, or neither for an open-ended handle
|
|
48
|
+
(call .stop() on the returned process handle). segment_secs rolls output
|
|
49
|
+
across numbered files (whole files, short re-open gap between them; use
|
|
50
|
+
open_receiver for gapless). to_stdout=True streams decoded blocks
|
|
51
|
+
instead of writing a file.
|
|
52
|
+
"""
|
|
33
53
|
self.require_mode(C.MODE_RX)
|
|
34
54
|
freq, sample_rate, lna, vga = self.validate_rx(freq, sample_rate, lna, vga)
|
|
35
55
|
bw = self._auto_baseband(sample_rate, baseband_bw)
|
|
@@ -57,7 +77,7 @@ class CaptureMixin:
|
|
|
57
77
|
# The inner stream generator is closed EXPLICITLY on the way out;
|
|
58
78
|
# leaving it to GC can orphan a receiving hackrf_transfer after
|
|
59
79
|
# the caller breaks out of the loop.
|
|
60
|
-
def _gen():
|
|
80
|
+
def _gen() -> Generator[np.ndarray, None, None]: # decoded, not raw
|
|
61
81
|
tail = b""
|
|
62
82
|
inner = self._run(argv, mode="stream")
|
|
63
83
|
try:
|
|
@@ -97,17 +117,30 @@ class CaptureMixin:
|
|
|
97
117
|
return res
|
|
98
118
|
|
|
99
119
|
# ---- aliases ----
|
|
100
|
-
def rx(self, *a, **k):
|
|
120
|
+
def rx(self, *a: Any, **k: Any) -> Any:
|
|
121
|
+
"""Alias of capture()."""
|
|
101
122
|
return self.capture(*a, **k)
|
|
102
123
|
|
|
103
|
-
def capture_samples(self, freq, sample_rate
|
|
124
|
+
def capture_samples(self, freq: float, sample_rate: float,
|
|
125
|
+
num_samples: int, **k: Any) -> Any:
|
|
126
|
+
"""Alias of capture() with a required num_samples bound."""
|
|
104
127
|
return self.capture(freq, sample_rate, num_samples=num_samples, **k)
|
|
105
128
|
|
|
106
|
-
def capture_seconds(self, freq, sample_rate
|
|
129
|
+
def capture_seconds(self, freq: float, sample_rate: float,
|
|
130
|
+
duration: float, **k: Any) -> Any:
|
|
131
|
+
"""Alias of capture() with a required duration bound."""
|
|
107
132
|
return self.capture(freq, sample_rate, duration=duration, **k)
|
|
108
133
|
|
|
109
|
-
def scan_frequencies(self, freqs, sample_rate
|
|
110
|
-
|
|
134
|
+
def scan_frequencies(self, freqs: list[float], sample_rate: float,
|
|
135
|
+
num_samples: int, *,
|
|
136
|
+
on_capture: Callable[..., Any] | None = None,
|
|
137
|
+
**k: Any) -> dict[float, np.ndarray] | None:
|
|
138
|
+
"""Capture num_samples of IQ at each frequency in turn.
|
|
139
|
+
|
|
140
|
+
Returns {freq: complex64 ndarray}, or None when on_capture consumes
|
|
141
|
+
blocks instead. Each visit re-opens the device (short gap between
|
|
142
|
+
frequencies).
|
|
143
|
+
"""
|
|
111
144
|
# Sequentially capture a fixed sample count at each frequency in
|
|
112
145
|
# `freqs`, retuning between them. This does NOT close the gapless-
|
|
113
146
|
# retune gap (each retune is a fresh hackrf_transfer with a short
|
|
@@ -130,8 +163,13 @@ class CaptureMixin:
|
|
|
130
163
|
return results if on_capture is None else None
|
|
131
164
|
|
|
132
165
|
# ---- in-memory + context-managed entry points ----
|
|
133
|
-
def capture_array(self, freq, sample_rate, num_samples,
|
|
134
|
-
return_params=False, **k):
|
|
166
|
+
def capture_array(self, freq: float, sample_rate: float, num_samples: int,
|
|
167
|
+
*, return_params: bool = False, **k: Any) -> Any:
|
|
168
|
+
"""Return exactly num_samples complex64 samples in RAM, no file.
|
|
169
|
+
|
|
170
|
+
The scripting entry point. return_params=True also returns the
|
|
171
|
+
validated parameters actually used.
|
|
172
|
+
"""
|
|
135
173
|
# Scripting entry point: return EXACTLY num_samples complex64 samples
|
|
136
174
|
# in RAM, no file. Built on the stdout-stream path so it shares the
|
|
137
175
|
# odd-byte carry + clean-reap logic. The stream is closed as soon as
|
|
@@ -164,8 +202,16 @@ class CaptureMixin:
|
|
|
164
202
|
return iq, self.last_params
|
|
165
203
|
return iq
|
|
166
204
|
|
|
167
|
-
def open_receiver(self, freq, sample_rate, *, lna
|
|
168
|
-
|
|
205
|
+
def open_receiver(self, freq: float, sample_rate: float, *, lna: int = 16,
|
|
206
|
+
vga: int = 20, amp: bool = False,
|
|
207
|
+
baseband_bw: float | None = None,
|
|
208
|
+
read_samples: int = 131072) -> PersistentReceiver:
|
|
209
|
+
"""Open a long-lived receive stream; returns a PersistentReceiver.
|
|
210
|
+
|
|
211
|
+
One hackrf_transfer process serves many .read(n) calls: consecutive
|
|
212
|
+
reads are gapless (contrast capture(segment_secs=...)). Use as a context
|
|
213
|
+
manager to guarantee the child is reaped.
|
|
214
|
+
"""
|
|
169
215
|
# Open a PERSISTENT fixed-frequency receiver: one long-lived
|
|
170
216
|
# hackrf_transfer you drain in segments over time, so you don't pay the
|
|
171
217
|
# ~1-2 s process spin-up per capture. Use as a context manager:
|
|
@@ -184,7 +230,13 @@ class CaptureMixin:
|
|
|
184
230
|
amp=amp, baseband_bw=baseband_bw,
|
|
185
231
|
read_samples=read_samples)
|
|
186
232
|
|
|
187
|
-
def capture_stream(self, freq, sample_rate,
|
|
233
|
+
def capture_stream(self, freq: float, sample_rate: float,
|
|
234
|
+
**k: Any) -> StreamCtx:
|
|
235
|
+
"""Context manager yielding decoded complex64 blocks from a live stream.
|
|
236
|
+
|
|
237
|
+
The receiving hackrf_transfer is always reaped on block exit, even on
|
|
238
|
+
exception.
|
|
239
|
+
"""
|
|
188
240
|
# Context manager wrapping the live stdout stream so the receiving
|
|
189
241
|
# hackrf_transfer is ALWAYS reaped on exit, even on exception:
|
|
190
242
|
# with h.capture_stream(433.92e6, 8e6) as blocks:
|
|
@@ -194,8 +246,15 @@ class CaptureMixin:
|
|
|
194
246
|
gen = self.capture(freq, sample_rate, to_stdout=True, **k)
|
|
195
247
|
return StreamCtx(gen)
|
|
196
248
|
|
|
197
|
-
def capture_callback(self, freq, sample_rate
|
|
198
|
-
|
|
249
|
+
def capture_callback(self, freq: float, sample_rate: float,
|
|
250
|
+
on_block: Callable[..., Any], *,
|
|
251
|
+
max_samples: int | None = None,
|
|
252
|
+
max_blocks: int | None = None, **k: Any) -> int:
|
|
253
|
+
"""Invoke on_block(iq) per decoded block until a bound or False return.
|
|
254
|
+
|
|
255
|
+
Bounds: max_samples, max_blocks, or the callback returning False.
|
|
256
|
+
Returns the number of samples delivered.
|
|
257
|
+
"""
|
|
199
258
|
# Binder-style ergonomics over the subprocess stream: instead of the
|
|
200
259
|
# caller writing the receive loop, register a callback that fires with
|
|
201
260
|
# each decoded complex64 block as it arrives. This is the usability
|
|
@@ -228,8 +287,9 @@ class CaptureMixin:
|
|
|
228
287
|
return total
|
|
229
288
|
|
|
230
289
|
# ---- internals ----
|
|
231
|
-
def _rx_argv(self, freq, sample_rate, target, lna
|
|
232
|
-
|
|
290
|
+
def _rx_argv(self, freq: float, sample_rate: float, target: str, lna: int,
|
|
291
|
+
vga: int, amp: bool, bias_tee: bool, bw: float,
|
|
292
|
+
num_samples: int | None) -> list[Any]:
|
|
233
293
|
argv = ["transfer", "-r", target,
|
|
234
294
|
"-f", int(freq), "-s", int(sample_rate),
|
|
235
295
|
"-l", lna, "-g", vga,
|
|
@@ -241,8 +301,10 @@ class CaptureMixin:
|
|
|
241
301
|
argv += ["-n", int(num_samples)]
|
|
242
302
|
return argv
|
|
243
303
|
|
|
244
|
-
def _capture_segmented(self, freq, sample_rate, out
|
|
245
|
-
|
|
304
|
+
def _capture_segmented(self, freq: float, sample_rate: float, out: str,
|
|
305
|
+
segment_secs: float, lna: int, vga: int, amp: bool,
|
|
306
|
+
bias_tee: bool, bw: float, sigmf: bool,
|
|
307
|
+
print_cmd: bool) -> list[str]:
|
|
246
308
|
# Rolling files out_000.iq, out_001.iq, ... each `segment_secs` long.
|
|
247
309
|
# Each segment is a bounded blocking capture, so files are whole.
|
|
248
310
|
base, ext = os.path.splitext(out)
|
|
@@ -268,7 +330,7 @@ class CaptureMixin:
|
|
|
268
330
|
seg = f"{base}_{idx:03d}{ext}"
|
|
269
331
|
argv = self._rx_argv(freq, sample_rate, seg, lna, vga, amp,
|
|
270
332
|
bias_tee, bw, n_per)
|
|
271
|
-
|
|
333
|
+
self._run(argv, mode="blocking", print_cmd=print_cmd)
|
|
272
334
|
if sigmf and not print_cmd:
|
|
273
335
|
write_sigmf_meta(seg, freq, sample_rate, lna=lna, vga=vga,
|
|
274
336
|
amp=amp, datatype="ci8")
|