hackrf-web

hackrf-web

A quick bench page for the HackRF One. Run a small bridge on the computer the board is plugged into, open the page, and you have live spectrum and waterfall across any span to 6 GHz, IQ at one centre frequency with the FFT done locally, IQ snapshots as time-domain and constellation views, IQ recording to SigMF, and a transmit section that stays locked unless you unlock it on the bridge.

Use it: https://lc-linkous.github.io/hackrf-web/ — this README, rendered, lives at /readme/ and the protocol at /protocol/ on the same site.

pip install hackrf-web
hackrf-web           # starts the bridge, prints a token, opens the page

The page has a Try without hardware mode that needs no bridge: it replays frames computed from a real HackRF One recording (the FM band at 8 Msps that ships with the library's tests).

hackrf-web with four windows: spectrum of the FM band from a real HackRF recording with markers on the stations, waterfall, IQ time domain, and constellation; sidebar with IQ-mode controls on the left

This is the browser companion to the hackRF_python library (hackrfpy). It is deliberately not the whole library: the page covers the plug-in-and-look workflow, and scripts cover everything else.

How it fits together

The HackRF is not a serial device, so unlike nanovna-web and tinysa-web the browser cannot talk to it directly. A Python bridge stands in:

          GitHub Pages (or the bridge's own copy at http://127.0.0.1:8765/)
   ┌──────────────────────────────────────────────────────────────────┐
   │ hackrf-web page                                                  │
   │   js/device/bridge-link.js   WebSocket client, binary frames     │
   │   js/device/hackrf.js        method names follow hackrfpy        │
   │   js/plot/  js/ui/           windows, markers, recorder, console │
   └───────────────┬──────────────────────────────────────────────────┘
                   │  ws://127.0.0.1:8765/?token=…      (PROTOCOL.md)
                   │  JSON calls and events; spectrum + IQ as binary
   ┌───────────────▼──────────────────────────────────────────────────┐
   │ hackrf-web bridge   (pip install hackrf-web → `hackrf-web`)      │
   │   session manager   idle ─ sweep ─ iq ─ tx, one child at a time │
   │   frames.py         sweep stitching · Hann FFT · int8 re-encode  │
   │   recorder          int8 .iq + .sigmf-meta, time cap, disk check │
   └───────────────┬──────────────────────────────────────────────────┘
                   │  hackrfpy  (unmodified, from PyPI)
   ┌───────────────▼──────────────────────────────────────────────────┐
   │ hackrf-tools        hackrf_info · hackrf_sweep · hackrf_transfer │
   └───────────────┬──────────────────────────────────────────────────┘
                   │  USB
                HackRF One

Reading it top to bottom:

hackrf-web --fake runs the whole bridge against a simulated board — every state transition is real, only the samples are synthesised — and hackrf-web --doctor prints hackrfpy's preflight report and exits.

Where the page is served from

The page is static and identical in both places:

Pages updates on every push; the bridge updates with pip install -U hackrf-web. The handshake carries both versions and the page says if they disagree.

Layout

┌──────────────────────────────────────────────────────────────────┐
│ hackrf-web  [Connect to bridge] [Demo]  ● status  [iq · 98.1 MHz] │
├────────────┬─────────────────────────────────────────────────────┤
│ Bridge     │ Windows [1][2][4]        Max  Min  Avg  Hold  Clear │
│  address   │ ┌ Spectrum ─────────────┐ ┌ Waterfall ───────────┐   │
│  token     │ │   dBFS vs frequency   │ │   rolling history    │   │
│  preflight │ │   DC marker, median   │ │                      │   │
│ ────────── │ └───────────────────────┘ └──────────────────────┘   │
│ Sweep mode │ ┌ IQ time ──────────────┐ ┌ Constellation ───────┐   │
│  band      │ │   I and Q, int8       │ │   I/Q density        │   │
│  start/stop│ │                       │ │                      │   │
│  bin width │ └───────────────────────┘ └──────────────────────┘   │
│ ────────── ├─────────────────────────────────────────────────────┤
│ IQ mode    │ Markers [peak] [top 4] [tune IQ to marker 1]        │
│  centre    │ # freq* level max −3dB BW Δlevel Δfreq              │
│  rate, FFT ├─────────────────────────────────────────────────────┤
│ Gains      │ Bridge console ▲ (drag)          Log ▾ [⤓][copy]    │
│ Recording  │ > session.start {"mode":"iq","center":98100000,…}   │
│ Export     │   hackrf_transfer -r - -f 98100000 -s 8000000 …     │
│ Transmit 🔒│ ┌──────────────────────────────────┐ [Send]         │
│ Diagnostics│ │ method.name {json}               │                │
│ Settings   │                                                     │
│ About →    │                                                     │
└────────────┴─────────────────────────────────────────────────────┘

Each window has its own view tabs; Windows 1 / 2 / 4 sets how many. The sidebar is an accordion; drag its edge to resize. The console shows the bridge calls, their replies, and the argv the bridge actually ran.

What it does (v0.1)

Not yet: digital tuning inside the IQ band (every retune restarts the child), offset tuning for the DC spike, calibration files (dBm), transmit from a file, audio. See the About page in the tool for the full list.

Levels are dBFS

The HackRF is not a calibrated instrument. Everything on the page is dBFS (0 dBFS = full-scale 8-bit sample) and depends on the gains; the unit string is on the axis, the markers table and every export. Approximate dBm through the library's calibrate.py is on the list.

Shared with the other two tools

Unchanged (byte-identical with tinysa-web) Instrument-specific
css/layout.css, css/components.css js/device/bridge-link.js, demo-link.js, hackrf.js, models.js
js/plot/common.js, js/plot/palettes.js js/plot/iq.js; spectrum.js / waterfall.js (unit label parameter)
js/ui/sidebar.js, js/ui/console.js, js/ui/panes.js js/ui/settings.js, about.js; recorder.js / export.js (unit)
css/theme.css (except one comment) js/sa.js (one line: fmtDbm takes a unit), index.html, js/main.js, and the whole bridge/

The transport changed completely (WebSocket instead of Web Serial, binary frames instead of a console prompt) and the kit didn't notice.

Repository layout

web/                       the page — this is what GitHub Pages deploys
  index.html  css/  js/  demo/  media/
bridge/                    the Python package (pip install hackrf-web)
  pyproject.toml  README.md
  src/hackrf_web/
    cli.py                 `hackrf-web` command
    server.py              WebSocket + tiny HTTP on 127.0.0.1, token, CORS/PNA
    session.py             idle/sweep/iq/tx state machine, recorder, TX gates
    frames.py              binary frames, sweep stitching, FFT, int8 re-encode
    fake.py                simulated HackRF for --fake and the demo data
PROTOCOL.md                the contract between the two
tests/smoke.py             drives a running --fake bridge end to end (CI)
tools-sync-page.py         copies web/ into the package before building a wheel

Running locally

# page
cd web && python -m http.server 8000          # http://localhost:8000/
# bridge, no board
cd bridge && PYTHONPATH=src python -m hackrf_web.cli --fake --port 8765
# then open http://localhost:8000/#bridge=127.0.0.1:8765&token=<printed token>

Releasing

Contributing

See CONTRIBUTING.md and the issue templates. Please read the short code of conduct.

License

GPL-2.0 — see LICENSE. Same as hackRF_python.

hackrf-web is not affiliated with or endorsed by Great Scott Gadgets or the HackRF project. Read the official documentation before transmitting anything.