hackrfpy 1.0.0__py3-none-any.whl

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/cli.py ADDED
@@ -0,0 +1,292 @@
1
+ #! /usr/bin/python3
2
+
3
+ ##--------------------------------------------------------------------\
4
+ # hackrfpy
5
+ # 'src/hackrfpy/cli.py'
6
+ #
7
+ # The CLI shell. One class owns the parser; main(args) dispatches on the
8
+ # subcommand. __init__(a=None) takes argv from sys.argv OR a passed list, so
9
+ # the same class drives from the shell or from a script:
10
+ # app = HackRFCLI(['rx', '-f', '433.92M', '-s', '8M', '-n', '1000000'])
11
+ # app.main(app.getArgs())
12
+ #
13
+ # The CLI layer is thin: it parses, resolves the persisted operating mode,
14
+ # instantiates HackRF once, calls the method, and maps typed exceptions to
15
+ # clean stderr + exit codes. All real work lives in core + mixins.
16
+ #
17
+ # Author(s): <you>
18
+ ##--------------------------------------------------------------------\
19
+
20
+ import argparse
21
+ import os
22
+ import sys
23
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
24
+
25
+ try:
26
+ _VERSION = _pkg_version("hackrfpy")
27
+ except PackageNotFoundError: # running from a source tree, not installed
28
+ _VERSION = "0+unknown"
29
+
30
+ from .core import HackRF, parse_freq
31
+ from .exceptions import HackRFError, HackRFValueError
32
+ from . import constants as C
33
+ from . import presets as P
34
+
35
+
36
+ # frequency parsing now lives in core.parse_freq (single source of truth, and
37
+ # the typed-exception version) so library callers share it. Imported above.
38
+
39
+
40
+ # ---- mode state file (mode persists across CLI invocations) ------------------
41
+ def _print_detect(det):
42
+ print("hackrfpy detect")
43
+ print("-" * 40)
44
+ if det["problem"] and not det["found"]:
45
+ print(f" {det['problem']}")
46
+ return
47
+ print(f" boards found : {det['count']}")
48
+ if det["tools_version"]:
49
+ print(f" hackrf-tools : {det['tools_version']}")
50
+ if det["libhackrf_version"]:
51
+ print(f" libhackrf : {det['libhackrf_version']}")
52
+ for b in det["boards"]:
53
+ tag = "HackRF" if b["is_hackrf"] else "UNCONFIRMED"
54
+ print(f" [{b['index']}] {tag}")
55
+ print(f" serial : {b['serial']}")
56
+ print(f" name : {b['name']}")
57
+ print(f" firmware : {b['firmware']}"
58
+ + (" (stale: <2021, streaming/-N may be unsupported)"
59
+ if b["firmware_stale"] else ""))
60
+ if det["multiple"]:
61
+ print(" ! multiple boards: select one with --serial <serial>")
62
+ for w in det.get("warnings", []):
63
+ print(f" ! {w}")
64
+ print(f" ready : {'yes' if det['ready'] else 'no'}")
65
+ if det["problem"] and det["found"]:
66
+ print(f" note : {det['problem']}")
67
+
68
+
69
+ def _state_path():
70
+ base = os.environ.get("XDG_CONFIG_HOME",
71
+ os.path.join(os.path.expanduser("~"), ".config"))
72
+ return os.path.join(base, C.STATE_DIR_NAME, C.STATE_FILE_NAME)
73
+
74
+
75
+ def read_mode():
76
+ path = _state_path()
77
+ try:
78
+ with open(path) as f:
79
+ for line in f:
80
+ if line.strip().startswith("mode"):
81
+ return line.split("=", 1)[1].strip().strip('"').strip("'")
82
+ except OSError:
83
+ pass
84
+ return C.DEFAULT_MODE
85
+
86
+
87
+ def write_mode(mode):
88
+ path = _state_path()
89
+ os.makedirs(os.path.dirname(path), exist_ok=True)
90
+ with open(path, "w") as f:
91
+ f.write(f'mode = "{mode}"\n')
92
+
93
+
94
+ # Subcommands whose only job is to relay to a device method with **flags.
95
+ def _add_common_rf(p, freq_required=True):
96
+ p.add_argument("-f", "--frequency", type=parse_freq, required=freq_required,
97
+ help="center frequency (e.g. 433.92M, 1.09G, or Hz)")
98
+ p.add_argument("-s", "--sample-rate", dest="sample_rate", type=parse_freq,
99
+ default=8e6, help="sample rate (default 8M)")
100
+ p.add_argument("--force", action="store_true",
101
+ help="downgrade out-of-range rejects to warnings")
102
+ p.add_argument("--print-cmd", dest="print_cmd", action="store_true",
103
+ help="print the hackrf_* command without running it")
104
+ p.add_argument("-v", "--verbose", action="store_true")
105
+ p.add_argument("--serial", help="select a board by serial number "
106
+ "(multi-board setups)")
107
+
108
+
109
+ class HackRFCLI:
110
+ def __init__(self, a=None):
111
+ main_parser = argparse.ArgumentParser(prog="hrf", description="hackrfpy")
112
+ main_parser.add_argument("--version", action="version",
113
+ version=f"%(prog)s {_VERSION}")
114
+ sub = main_parser.add_subparsers(dest="subparser_name")
115
+
116
+ # info
117
+ sp = sub.add_parser("info", help="device + firmware info")
118
+ sp.add_argument("--raw", action="store_true")
119
+ sp.add_argument("-v", "--verbose", action="store_true")
120
+ sp.add_argument("--print-cmd", dest="print_cmd", action="store_true")
121
+
122
+ # detect
123
+ sp = sub.add_parser("detect",
124
+ help="autodetect + identify connected HackRF(s)")
125
+ sp.add_argument("-v", "--verbose", action="store_true")
126
+
127
+ # doctor
128
+ sp = sub.add_parser("doctor", help="environment / preflight check")
129
+ sp.add_argument("--path", default=".", help="disk to check for capture")
130
+
131
+ # mode
132
+ sp = sub.add_parser("mode", help="get/set operating mode (rx|tx)")
133
+ sp.add_argument("value", nargs="?", choices=C.MODES,
134
+ help="omit to print current mode")
135
+
136
+ # rx / capture (-f optional HERE because --preset can supply it;
137
+ # main() enforces that one of the two resolves a frequency)
138
+ sp = sub.add_parser("rx", help="receive IQ to a file")
139
+ _add_common_rf(sp, freq_required=False)
140
+ sp.add_argument("-o", "--out", default="capture.iq")
141
+ sp.add_argument("-n", "--num-samples", dest="num_samples", type=int)
142
+ sp.add_argument("-d", "--duration", type=float, help="seconds")
143
+ sp.add_argument("-l", "--lna", type=int, default=16)
144
+ sp.add_argument("-g", "--vga", type=int, default=20)
145
+ sp.add_argument("-a", "--amp", action="store_true")
146
+ sp.add_argument("--bias-tee", dest="bias_tee", action="store_true")
147
+ sp.add_argument("--bw", dest="baseband_bw", type=parse_freq)
148
+ sp.add_argument("--segment", dest="segment_secs", type=float,
149
+ help="rolling files of N seconds each")
150
+ sp.add_argument("--no-sigmf", dest="sigmf", action="store_false")
151
+ sp.add_argument("--preset", help="apply a band preset for freq/rate")
152
+
153
+ # tx
154
+ sp = sub.add_parser("tx", help="transmit IQ from a file (TX mode only)")
155
+ _add_common_rf(sp)
156
+ sp.add_argument("source", help="int8 I/Q file to transmit")
157
+ sp.add_argument("-x", "--txvga", type=int, default=20)
158
+ sp.add_argument("-a", "--amp", action="store_true")
159
+ sp.add_argument("--bias-tee", dest="bias_tee", action="store_true")
160
+ sp.add_argument("--bw", dest="baseband_bw", type=parse_freq)
161
+ sp.add_argument("-R", "--repeat", action="store_true")
162
+ sp.add_argument("-n", "--num-samples", dest="num_samples", type=int)
163
+ sp.add_argument("-d", "--duration", type=float)
164
+ sp.add_argument("--max-duration", dest="max_duration", type=float,
165
+ help="hard time cap (s), enforced even for --repeat")
166
+
167
+ # sweep
168
+ sp = sub.add_parser("sweep", help="spectrum sweep (CSV to stdout)")
169
+ sp.add_argument("--f-min", dest="f_min", type=parse_freq, required=True)
170
+ sp.add_argument("--f-max", dest="f_max", type=parse_freq, required=True)
171
+ sp.add_argument("-w", "--bin-width", dest="bin_width", type=parse_freq)
172
+ sp.add_argument("-l", "--lna", type=int, default=16)
173
+ sp.add_argument("-g", "--vga", type=int, default=20)
174
+ sp.add_argument("-a", "--amp", action="store_true")
175
+ sp.add_argument("-1", "--one-shot", dest="one_shot", action="store_true")
176
+ sp.add_argument("-N", "--num-sweeps", dest="num_sweeps", type=int)
177
+ sp.add_argument("--force", action="store_true")
178
+ sp.add_argument("--print-cmd", dest="print_cmd", action="store_true")
179
+ sp.add_argument("-v", "--verbose", action="store_true")
180
+ sp.add_argument("--serial", help="select a board by serial number")
181
+
182
+ # presets
183
+ sp = sub.add_parser("presets", help="list band presets")
184
+
185
+ self.args = main_parser.parse_args(a)
186
+ self._parser = main_parser
187
+
188
+ def getArgs(self):
189
+ return self.args
190
+
191
+ def _make_device(self, args):
192
+ h = HackRF(verbose=getattr(args, "verbose", False),
193
+ serial=getattr(args, "serial", None))
194
+ h.allow_out_of_spec = getattr(args, "force", False)
195
+ # restore persisted mode so rx/tx/sweep honor it across invocations
196
+ h.restore_mode(read_mode())
197
+ return h
198
+
199
+ def main(self, args):
200
+ name = args.subparser_name
201
+ if name is None:
202
+ self._parser.print_help()
203
+ return
204
+
205
+ if name == "presets":
206
+ for k, v in sorted(P.load_presets().items()):
207
+ print(f" {k:10} {v.get('desc','')}")
208
+ return
209
+
210
+ if name == "mode":
211
+ if args.value is None:
212
+ print(read_mode())
213
+ else:
214
+ h = HackRF()
215
+ h.restore_mode(read_mode()) # so the banner fires only on a
216
+ h.set_mode(args.value) # real rx -> tx switch
217
+ write_mode(args.value)
218
+ return
219
+
220
+ h = self._make_device(args)
221
+
222
+ if name == "info":
223
+ res = h.info(raw=args.raw, print_cmd=args.print_cmd)
224
+ if not args.print_cmd:
225
+ print(res)
226
+
227
+ elif name == "detect":
228
+ det = h.detect()
229
+ _print_detect(det)
230
+ # exit non-zero when not ready, so `hrf detect && hrf rx ...` works
231
+ if not det["ready"]:
232
+ sys.exit(1)
233
+
234
+ elif name == "doctor":
235
+ report = h.doctor(capture_path=args.path)
236
+ if report["problems"]:
237
+ sys.exit(1) # make preflight usable in `doctor && capture`
238
+
239
+ elif name == "rx":
240
+ if getattr(args, "preset", None):
241
+ pre = P.get_preset(args.preset)
242
+ if args.frequency is None:
243
+ args.frequency = pre.get("center") or pre.get("f_min")
244
+ args.sample_rate = pre.get("sample_rate", args.sample_rate)
245
+ if args.frequency is None:
246
+ raise HackRFValueError(
247
+ "rx needs a frequency: pass -f/--frequency or --preset")
248
+ h.capture(args.frequency, args.sample_rate, out=args.out,
249
+ num_samples=args.num_samples, duration=args.duration,
250
+ lna=args.lna, vga=args.vga, amp=args.amp,
251
+ bias_tee=args.bias_tee, baseband_bw=args.baseband_bw,
252
+ sigmf=args.sigmf, segment_secs=args.segment_secs,
253
+ print_cmd=args.print_cmd)
254
+
255
+ elif name == "tx":
256
+ h.transmit(args.frequency, args.sample_rate, args.source,
257
+ txvga=args.txvga, amp=args.amp, bias_tee=args.bias_tee,
258
+ baseband_bw=args.baseband_bw, repeat=args.repeat,
259
+ num_samples=args.num_samples, duration=args.duration,
260
+ max_duration=args.max_duration,
261
+ print_cmd=args.print_cmd)
262
+
263
+ elif name == "sweep":
264
+ gen = h.sweep(args.f_min, args.f_max, bin_width=args.bin_width,
265
+ lna=args.lna, vga=args.vga, amp=args.amp,
266
+ one_shot=args.one_shot, num_sweeps=args.num_sweeps,
267
+ print_cmd=args.print_cmd)
268
+ if gen is not None:
269
+ # CSV to stdout, as documented -- same shape hackrf_sweep
270
+ # emits, so it round-trips through parse_sweep_line and pipes
271
+ # cleanly into other tools.
272
+ for row in gen:
273
+ print(", ".join(
274
+ [row["date"], row["time"],
275
+ str(row["hz_low"]), str(row["hz_high"]),
276
+ f"{row['bin_width']:.2f}", str(row["num_samples"])]
277
+ + [f"{d:.2f}" for d in row["db"]]))
278
+
279
+
280
+ def main():
281
+ app = HackRFCLI()
282
+ try:
283
+ app.main(app.getArgs())
284
+ except HackRFError as e:
285
+ print(f"error: {e}", file=sys.stderr)
286
+ sys.exit(e.exit_code)
287
+ except KeyboardInterrupt:
288
+ sys.exit(130)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ main()
hackrfpy/comparison.md ADDED
@@ -0,0 +1,175 @@
1
+ # comparison.md — hackrfpy vs other Python HackRF libraries
2
+
3
+ An honest look at where this library sits among the Python options for the
4
+ HackRF One. The goal is **not** to claim it's the best at everything — each of
5
+ these makes a different, defensible tradeoff. The goal is to identify the niche
6
+ hackrfpy fills and be clear about what it gives up to fill it.
7
+
8
+ ## The one decision that separates them
9
+
10
+ Every Python HackRF library makes a single architectural choice that determines
11
+ almost everything else: **bind `libhackrf` (the C library) or wrap the
12
+ `hackrf-tools` binaries (the command-line programs).**
13
+
14
+ * **libhackrf binders** (`python_hackrf`, `pyhackrf2`, `pyhackrf`, the various
15
+ `py-hackrf-ctypes` / `pylibhackrf` projects) call the C library's functions
16
+ directly — through ctypes, Cython, or a C extension. They get low-level
17
+ control and real-time sample callbacks, at the cost of a compiler/headers
18
+ dependency and a callback-based programming model.
19
+ * **hackrfpy wraps the binaries.** It runs `hackrf_info`, `hackrf_transfer`,
20
+ `hackrf_sweep`, etc. as subprocesses and manages their I/O and lifecycle. It
21
+ gives up real-time callback control and adds subprocess overhead, and in
22
+ exchange gets a dependency-free install, full tool coverage, and a place to
23
+ put a validation/safety layer.
24
+
25
+ Almost every difference below traces back to this one fork.
26
+
27
+ ## The landscape
28
+
29
+ | Library | Approach | Install | TX | Sweep | Device mgmt (clock/spiflash/etc.) | Maintained |
30
+ |---|---|---|---|---|---|---|
31
+ | **hackrfpy** (this) | wraps **binaries** | `pip` + binaries on system; no compiler | yes (gated) | yes | **all 8 tools** | active |
32
+ | **python_hackrf** | Cython → libhackrf | `pip` + compiler + libhackrf headers/libusb | yes | yes | info, sweep, operacake, transfer only | active, most complete binder |
33
+ | **pyhackrf2** | Python → libhackrf (ctypes) | `pip` + libusb/libhackrf | yes | yes | minimal | moderate |
34
+ | **pyhackrf** (dressel/4thel00z) | ctypes → libhackrf | `pip` + libusb/fftw | partial | via callback | minimal | stale |
35
+ | **py-hackrf-ctypes / pylibhackrf** | ctypes / C ext → libhackrf | manual build | varies | via callback | minimal | stale/experimental |
36
+
37
+ (Approaches verified against each project's own PyPI/GitHub description; feature
38
+ columns reflect what each documents, not exhaustive testing.)
39
+
40
+ ## What the libhackrf binders do well (and where hackrfpy now stands)
41
+
42
+ Being honest about the architectural tradeoff, and what has and hasn't been narrowed:
43
+
44
+ * **Real-time sample callbacks.** *Gap narrowed.* The binders expose libhackrf's
45
+ callback model — register a function that fires as buffers arrive. hackrfpy
46
+ now offers the same *ergonomics* via `capture_callback(freq, rate, on_block)`:
47
+ your function is called with each decoded `complex64` block as it streams, and
48
+ returns `False` to stop. The mechanism differs (it rides the subprocess stream
49
+ rather than a libhackrf USB-thread callback), so it is not zero-copy — but the
50
+ measured block-delivery latency is low: on a laptop at 20 Msps, median ~0.04 ms
51
+ between blocks with ~0.5 ms jitter (`examples/benchmark.py`). So *when data is
52
+ flowing*, the "latency floor is the pipe" concern is sub-millisecond; the real
53
+ constraint at top rates is throughput (above), not per-block latency.
54
+ * **Lower overhead at high rates.** *Gap narrowed; measured, with an honest
55
+ ceiling.* The decode path was optimized to construct `complex64` directly
56
+ (≈3× faster; benchmarked at ~190–390 MB/s depending on machine, i.e. 5–10× the
57
+ 40 MB/s the device produces at 20 Msps — so **decode is not the bottleneck**).
58
+ The remaining cost is the subprocess + kernel pipe. Measured on a laptop with a
59
+ shared USB bus (`examples/benchmark.py`): a 20 Msps capture received **100% of
60
+ samples with no bulk loss** and only a single startup-transient shortfall over
61
+ 120M samples, but the consumer drained the pipe at a **steady-state ~14 Msps**,
62
+ so the capture ran slower than real-time (the pipe back-pressures and
63
+ `hackrf_transfer` blocks rather than dropping). The practical reading: for
64
+ *capture-to-file or batch* work you lose nothing; for a *live real-time*
65
+ consumer at the very top of the rate range, the pipe drain is a real ceiling a
66
+ binder avoids. There is also a fixed ~1–2 s per-capture **startup cost**
67
+ (process launch + USB setup + device settle) the in-process binders don't pay.
68
+ These numbers are machine-specific — run the benchmark on your own setup.
69
+ * **Fine-grained mid-stream control.** *Genuine remaining gap.* `hackrf_transfer`
70
+ takes one frequency and runs until stopped; it cannot retune mid-stream, so
71
+ hackrfpy cannot either — that is the binary's limit, not a missing feature.
72
+ What hackrfpy adds is `scan_frequencies([...], rate, n)` to make sequential
73
+ multi-frequency capture a single clean call, but each retune is still a fresh
74
+ process with a short re-open gap. **True gapless retuning needs the C library**
75
+ — if that's your requirement, use a binder.
76
+ * **Android.** `python_hackrf` documents an Android build path. hackrfpy targets
77
+ desktop OSes. Unchanged.
78
+
79
+ If your work is "process every sample in real time with the lowest possible
80
+ latency, retuning on the fly," a libhackrf binder — `python_hackrf` is the most
81
+ complete and actively maintained — is still the better fit. The gap is now
82
+ narrower (callback ergonomics and fast decode), but the architectural ceiling is
83
+ real and this document won't pretend otherwise.
84
+
85
+ ## What hackrfpy does that the others don't
86
+
87
+ The niche, stated plainly:
88
+
89
+ * **Dependency-free install, especially on Windows.** Because it wraps the
90
+ binaries, there is no C compiler, no `libhackrf.h`, no `libusb` headers, no
91
+ `PYTHON_HACKRF_CFLAGS`/`LDFLAGS` to set. The binders all require a build
92
+ toolchain and the native headers present at install time — the exact friction
93
+ that makes HackRF-in-Python painful on Windows. hackrfpy needs only the
94
+ `hackrf-tools` binaries (which ship prebuilt) and pip. This is the single
95
+ biggest practical differentiator.
96
+ * **Complete tool coverage.** hackrfpy wraps **all eight** `hackrf-tools`
97
+ binaries, including the ones the binders explicitly skip. `python_hackrf`'s
98
+ own docs list `hackrf_clock`, `hackrf_cpldjtag`, `hackrf_debug`, and
99
+ `hackrf_spiflash` as "Will not be implemented." hackrfpy wraps clock, debug,
100
+ and operacake as full argument passthroughs and provides brick-guarded
101
+ spiflash/cpldjtag. If you need clock configuration or firmware operations from
102
+ Python, this is currently the option.
103
+ * **A validation / safety layer.** None of the binders police your inputs — they
104
+ pass values straight to libhackrf. hackrfpy adds a reject-by-default operating
105
+ envelope (catch a 60 GHz typo for 6 GHz), gain snapping with honest
106
+ notification, a sub-recommended-sample-rate warning, an explicit RX/TX mode
107
+ gate so you can't transmit by accident, a TX gain ceiling against
108
+ order-of-magnitude fat-fingers, and a `max_duration` dead-man for open-ended
109
+ transmits. This is a deliberate "help you not break your hardware or the law"
110
+ layer that a thin binding has no place to put.
111
+ * **Lifecycle safety for scripts.** Clean child reaping on `KeyboardInterrupt`,
112
+ context managers (`capture_stream`, `sweep_stream`) that guarantee the radio
113
+ is stopped on exit, and an `atexit` backstop so a dying script doesn't leave a
114
+ transmitter running. These matter precisely because subprocess management is
115
+ the risk hackrfpy takes on — so it's where the engineering went.
116
+ * **A real CLI.** `hrf` is a first-class command-line tool (`hrf detect`,
117
+ `hrf rx`, `hrf sweep`, `hrf doctor`), not just a library. The binders are
118
+ libraries first; `python_hackrf` has a CLI but centered on info/sweep.
119
+ * **Self-describing captures.** SigMF sidecar metadata written automatically, so
120
+ a recording carries its frequency/rate/gains instead of being a headerless
121
+ int8 blob.
122
+ * **Hardware-validated parsing.** The parsers are tested against frozen verbatim
123
+ output from a real HackRF One (info, sweep, IQ), including real quirks like
124
+ out-of-order sweep segments and continuation-line fields.
125
+
126
+ ## Honest weaknesses
127
+
128
+ Where hackrfpy is genuinely behind, beyond the architectural give-ups above:
129
+
130
+ * **Younger and fewer users** than the established binders. It is validated
131
+ against a real HackRF One (hardware-marked tests pass; parsers are pinned to
132
+ verbatim device output), but it has far less field exposure across diverse
133
+ setups, firmware versions, and edge cases than libraries that have been in
134
+ wide use for years. Maturity is about breadth of real-world exposure, and
135
+ that takes time and users.
136
+ * **Subprocess dependence on tool behavior.** It relies on the `hackrf-tools`
137
+ stdout format and exit codes, which it parses and version-checks — but a
138
+ future tools change could require parser updates. A binder against the stable
139
+ C ABI is insulated from that.
140
+ * **No real-time callback path.** Restated because it's the real ceiling: if you
141
+ outgrow "stream blocks from a subprocess," the architecture won't follow you
142
+ into zero-latency callback territory without becoming a different library.
143
+ * **Not a DSP library.** By design — it yields `complex64` and stops. The binders
144
+ don't do DSP either, but their callback model sits closer to where you'd build
145
+ it.
146
+
147
+ ## Who should use which
148
+
149
+ * **Use a libhackrf binder (`python_hackrf` first) if:** you need real-time
150
+ per-buffer callbacks, minimum latency at high sample rates, mid-stream
151
+ retuning, Android support, or you're already in a compiled-toolchain
152
+ environment where the build dependency is free.
153
+ * **Use hackrfpy if:** you want a clean `pip` install with no compiler
154
+ (especially on Windows), a scriptable + CLI workflow, full access to *all* the
155
+ hackrf tools including clock/debug/firmware, a safety/validation layer that
156
+ guards against expensive mistakes, robust script lifecycle (clean stop, no
157
+ orphaned transmitters), and self-describing recordings — and your data path is
158
+ "capture/sweep to file or to `complex64`," not real-time callback DSP.
159
+
160
+ ## The one-line version
161
+
162
+ The other libraries are **C bindings** that give you low-level, real-time
163
+ control at the cost of a build toolchain. hackrfpy is a **binary wrapper** that
164
+ gives you a dependency-free install, complete tool coverage, and a safety layer.
165
+ It now matches the binders' callback *ergonomics* (`capture_callback`) and has a
166
+ fast decode path, so the usability gap is small; the remaining hard difference
167
+ is true gapless mid-stream retuning, which needs the C library. Different tools
168
+ for different jobs; the niche is "scriptable HackRF control that installs cleanly
169
+ on Windows and tries to keep you from breaking things."
170
+
171
+ ---
172
+
173
+ *Landscape current as of mid-2026; library capabilities and maintenance status
174
+ change. Verify against each project's current PyPI/GitHub before relying on a
175
+ specific claim. Feature columns reflect each project's own documentation.*
hackrfpy/constants.py ADDED
@@ -0,0 +1,104 @@
1
+ #! /usr/bin/python3
2
+
3
+ ##--------------------------------------------------------------------\
4
+ # hackrfpy
5
+ # 'src/hackrfpy/constants.py'
6
+ #
7
+ # Single source of truth for the HackRF One operating envelope: hard
8
+ # frequency / sample-rate ranges, the stepped gain tables, the discrete
9
+ # baseband-filter bandwidths, and library defaults. EVERYTHING that is a
10
+ # "limit" lives here so that:
11
+ # 1. there is exactly one place to edit as hardware/firmware evolves,
12
+ # 2. the validation code and the test suite import the SAME numbers and
13
+ # therefore can never silently drift apart.
14
+ #
15
+ # WARNING: these describe the LIBRARY's checking envelope, not live device
16
+ # state. Editing them changes what the library will accept/reject; it does
17
+ # not change anything on the board.
18
+ #
19
+ # Author(s): <you>
20
+ ##--------------------------------------------------------------------\
21
+
22
+ # ---- hard ranges (reject-by-default; --force / allow_out_of_spec downgrades
23
+ # a reject to a warning) -------------------------------------------------
24
+ # HackRF One tunes 1 MHz - 6 GHz. Values outside this are almost always a
25
+ # fat-finger (typing 60e9 for 6e9), which is exactly what reject-by-default
26
+ # is meant to catch.
27
+ FREQ_MIN_HZ = 1_000_000 # 1 MHz
28
+ FREQ_MAX_HZ = 6_000_000_000 # 6 GHz
29
+
30
+ # Usable sample rates. The device technically accepts 2-20 Msps; 8-20 is the
31
+ # commonly recommended band. We reject outside 2-20 and warn below 8.
32
+ SR_MIN = 2_000_000 # 2 Msps
33
+ SR_MAX = 20_000_000 # 20 Msps
34
+ SR_WARN_BELOW = 8_000_000 # warn (don't reject) under 8 Msps
35
+
36
+ # ---- stepped gains (snap-and-notify, round DOWN to the honest device value) -
37
+ # Each entry: (min_db, max_db, step_db). The silicon rounds to a step anyway,
38
+ # so snapping is just telling the truth about what you'll actually get.
39
+ LNA_GAIN = (0, 40, 8) # IF gain (MAX2837)
40
+ VGA_GAIN = (0, 62, 2) # baseband RX gain
41
+ TXVGA_GAIN = (0, 47, 1) # TX IF gain
42
+
43
+ # Front-end amplifier is a fixed ~14 dB block, on/off only.
44
+ AMP_DB = 14
45
+
46
+ # ---- TX safety ceilings (default at true device max so an order-of-magnitude
47
+ # typo rejects; tighten these for a specific bench) ----------------------
48
+ TX_VGA_CEILING_DB = TXVGA_GAIN[1] # 47; lower this to cap output on a bench
49
+ TX_AMP_ALLOWED = True # set False to forbid the 14 dB amp on TX
50
+
51
+ # ---- discrete baseband filter bandwidths (Hz), MAX2837 supported set --------
52
+ # hackrf_transfer auto-selects one if unset; we replicate a sensible default
53
+ # (~0.75 x sample_rate, snapped to nearest supported) and snap explicit values.
54
+ BASEBAND_FILTER_BW_HZ = (
55
+ 1_750_000, 2_500_000, 3_500_000, 5_000_000, 5_500_000, 6_000_000,
56
+ 7_000_000, 8_000_000, 9_000_000, 10_000_000, 12_000_000, 14_000_000,
57
+ 15_000_000, 20_000_000, 24_000_000, 28_000_000,
58
+ )
59
+ BASEBAND_AUTO_FRACTION = 0.75 # default BW as a fraction of sample rate
60
+
61
+ # ---- sample format ----------------------------------------------------------
62
+ # HackRF native capture format: interleaved signed 8-bit I, Q, I, Q ...
63
+ BYTES_PER_SAMPLE = 2 # one int8 I + one int8 Q
64
+ IQ_DTYPE = "int8"
65
+
66
+ # ---- disk / capture lifecycle ----------------------------------------------
67
+ # Used by the capture estimator + guard. Bytes/sec = sample_rate * 2.
68
+ DEFAULT_DISK_HEADROOM = 0.95 # refuse if capture would exceed 95% of free
69
+ DEFAULT_SEGMENT_SECONDS = None # None = single file; set for rolling segments
70
+
71
+ # ---- external binaries ------------------------------------------------------
72
+ # Resolved via shutil.which by default; override the directory in config for
73
+ # Windows installs where hackrf-tools aren't on PATH.
74
+ TOOLS = {
75
+ "info": "hackrf_info",
76
+ "transfer": "hackrf_transfer",
77
+ "sweep": "hackrf_sweep",
78
+ "clock": "hackrf_clock",
79
+ "spiflash": "hackrf_spiflash",
80
+ "operacake": "hackrf_operacake",
81
+ "cpldjtag": "hackrf_cpldjtag",
82
+ "debug": "hackrf_debug",
83
+ }
84
+
85
+ # Tools that accept `-d <serial>` for multi-board selection. hackrf_info
86
+ # always lists every board and hackrf_cpldjtag predates the convention, so
87
+ # both are excluded; HackRF(serial=...) injects -d for the rest.
88
+ TOOLS_WITH_SERIAL = ("transfer", "sweep", "clock", "spiflash",
89
+ "operacake", "debug")
90
+
91
+ # The minimum set required for the common capture/sweep workflow. doctor()
92
+ # treats only these as hard problems when missing; the rest are optional
93
+ # device-management extras that not every install ships.
94
+ CORE_TOOLS = ("info", "transfer", "sweep")
95
+
96
+ # ---- operating modes --------------------------------------------------------
97
+ MODE_RX = "rx"
98
+ MODE_TX = "tx"
99
+ MODES = (MODE_RX, MODE_TX)
100
+ DEFAULT_MODE = MODE_RX
101
+
102
+ # CLI state-file location (mode persists across invocations here).
103
+ STATE_DIR_NAME = "hackrfpy"
104
+ STATE_FILE_NAME = "state.toml"