Pure-Go digital-trunking radio scanner engine for RTL-SDR · HackRF · Airspy · Airspy HF+.
P25 · DMR · TETRA · NXDN · Motorola Type II · EDACS · LTR · MPT 1327 · dPMR · D-STAR · YSF.
Zero CGO, single static binary, headless daemon + Bubbletea TUI cockpit + browser web console.
GopherTrunk is a software-defined-radio scanner that follows digital
trunked-radio voice calls and decodes them to audio. It runs on a
pool of RTL-SDR (every osmocom tuner), HackRF (One / Jawbreaker /
Rad1o), Airspy R2 / Mini, and Airspy HF+ dongles, has no C
dependencies at build or runtime (no librtlsdr / libhackrf /
libairspy / libairspyhf / libusb / libasound2 /
libmp3lame), and ships as a single ~10 MB static binary for Linux,
macOS 12 (Monterey) or later, and Windows.
Completed calls stream to Broadcastify Calls, RdioScanner, OpenMHz, and live Icecast / ShoutCast mountpoints out of the box. Why does this exist? Read The Story of GopherTrunk.
New to radio or SDR? Start with the Learn RF & SDR path — a free, structured course that takes you from "what is a radio wave?" to decoding digital trunked systems with GopherTrunk, one short lesson at a time.
# Linux x86_64 — see https://gophertrunk.org/downloads.html for macOS, Windows, ARM64.
VERSION=v0.7.1
curl -L -o gophertrunk.tar.gz \
https://github.com/MattCheramie/GopherTrunk/releases/download/${VERSION}/gophertrunk-${VERSION}-linux-amd64.tar.gz
tar xzf gophertrunk.tar.gz && cd gophertrunk-${VERSION}-linux-amd64
cp config.example.yaml config.yaml
./gophertrunk version
# Plain `./gophertrunk` (no subcommand, on a TTY) drops into the
# interactive launcher: pick [1] TUI, [2] Web, or [3] Headless.
# Skip the prompt with -tui / -web / -headless.
./gophertrunk -config config.yamlWindows users get a one-click installer that bundles Zadig for WinUSB driver setup; macOS users get notarised tarballs for Apple Silicon and Intel. Full per-OS recipes at gophertrunk.org/downloads.html.
- Trunked control-channel decoders — P25 Phase 1 + Phase 2 (full
TIA-102 chain), DMR Tier II + Tier III (vendor-aware: Capacity
Plus / Capacity Max grants and rest-channel tracking), NXDN,
Motorola Type II / SmartZone, EDACS / GE-Marc, LTR, MPT 1327,
dPMR Mode 3, TETRA TMO (plus experimental TETRA DMO / direct-mode
monitoring,
protocol: tetra-dmo). Amateur-radio: D-STAR and Yaesu System Fusion. - POCSAG + FLEX paging — protocol + DSP for the two dominant
pager protocols, both decoding straight off the air and sharing
the
pager_logtable //pagerpanel (tagged byprotocol). POCSAG (CCIR 584): BCH(31,21) FEC, batch carve-up, numeric (5 BCD/codeword) + alphanumeric (7-bit packed ASCII), 512 / 1200 / 2400 bps. FLEX: 1600 bps / 2-level mode — 32-bit sync + mode code → frame-info word → block de-interleave → BCH(31,21) → BIW / address / vector / message-word walk → alphanumeric / numeric pages. Pin SDRs viapaging.pocsag/paging.flex. Foundation for fire / EMS dispatch text alongside the trunked-voice pipeline. See docs/pocsag.md. - APRS / AX.25 packet — end-to-end pipeline for the
amateur-radio APRS metadata bus (position beacons, messages,
bulletins, status, Mic-E mobile-tracker compressed format).
Bell-202 AFSK DSP frontend (FM demod → FFSK tone discriminator
→ symbol-time recovery → NRZI → HDLC framer), AX.25 frame
parser with CRC-16-CCITT, APRS info-field decoders including
full Mic-E (lat/lon, speed, course, altitude, message code),
plus
events.KindAPRSPacketbus event, SQLiteaprs_log,GET /api/v1/aprs/packets, and/aprsweb panel. See docs/aprs.md. - AIS marine — end-to-end pipeline for the Automatic
Identification System every commercial vessel broadcasts on
marine VHF 87B / 88B (161.975 / 162.025 MHz). 9600 Bd GMSK DSP
frontend (FM demod → GFSK matched filter at BT = 0.4 →
symbol-time recovery → NRZI → HDLC framer → CRC-CCITT
validation), ITU-R M.1371-5 message-type dispatch (Class A
position reports 1/2/3, Class B 18 + 19 extended, base-station
4, static + voyage 5, Class B static 24 A + B), signed-integer
lat/lon decoder (1/600000 minute resolution), 6-bit ASCII text
fields (vessel name, call-sign, destination), spec
"not-available" sentinels. Plus
events.KindAISMessagebus event, SQLitevessel_log,GET /api/v1/ais/vessels, and/aisweb panel. See docs/ais.md. - MDC1200 signaling — end-to-end pipeline for Motorola's
analog FFSK data burst keyed at the head / tail of a
transmission on conventional VHF / UHF voice channels. 1200-baud
CCIR FFSK DSP frontend (FM demod → FFSK discriminator at
1200 / 1800 Hz → Mueller-Müller timing → NRZ slicer), 40-bit
sync framer with polarity tolerance, 16×7 de-interleave, op /
arg / unit-ID decode with CRC-16-CCITT check, and an op/arg
table (PTT ANI, emergency, status, radio check, call alert,
selective call). Plus
events.KindMDC1200Messagebus event, SQLitemdc1200_log,GET /api/v1/mdc1200/messages, and/mdc1200web panel. See docs/mdc1200.md. - DSC marine distress — protocol layer for the GMDSS Digital
Selective Calling system that fires every marine VHF channel-
70 distress alert. ITU-R M.493-15 format dispatch (Distress,
All-Ships, Individual, Group, Geographic, Auto-Individual),
BCH(10,7) syndrome check, position decoder with quadrant
hemisphere flip, nature-of-distress table. Plus
events.KindDSCMessagebus event, SQLitedsc_log,GET /api/v1/dsc/messages, and/dscweb panel (rows tint by category — distress=red, urgency=orange, safety=blue). DSP frontend decodes straight off the air: FM demod → FFSK discriminator (1200 Bd, 1300/2100 Hz tones) → symbol-timing recovery → direct-FSK slicer → BCH(10,7) character sync → ITU-R M.493 parser. Pin an SDR viadsc.channels. See docs/dsc.md. - ADS-B aviation — end-to-end pipeline for the 1090 MHz
Mode-S transponder broadcasts every commercial flight emits.
ICAO Annex 10 Vol IV / DO-260B parser (CRC-24 + DF dispatch +
type-code dispatch for identification, airborne / surface
position with 12-bit Q-bit altitude, airborne velocity);
globally-unambiguous CPR position decoder + per-ICAO
even+odd pair tracker. BEAST upstream consumes Mode-S
frames from any dump1090 / readsb / BeastSplitter via TCP —
most 1090 MHz receive chains already run one, GopherTrunk
decodes its output. Native PPM DSP frontend is the
alternative: pin an SDR (>= 2 Msps) to 1090 MHz via
adsb.channelsand GopherTrunk demodulates Mode-S itself — magnitude envelope → 8 µs preamble correlation → PPM bit slice → CRC/DF dispatch — feeding the same decode/track path the BEAST upstream uses. Plusevents.KindAircraftReportbus event, SQLiteaircraft_log,GET /api/v1/adsb/aircraft, and/adsbweb panel. See docs/adsb.md. - M17 link layer — metadata decoder for the open, Codec2-based
M17 digital voice mode (4FSK, 4800 sym/s). C4FM demod → symbol
timing → 4FSK slice → sync hunt → LICH reassembly (Golay(24,12),
six chunks) → Link Setup Frame parse: source / destination
base-40 callsigns, mode (voice / data / packet), channel-access
number, CRC-16. Recovers "who's talking to whom" from an
in-progress transmission without decoding audio (Codec2 voice is
a planned follow-up).
events.KindM17LinkSetupbus event, SQLitem17_log,GET /api/v1/m17/linksetups. Pin an SDR viam17.channels. See docs/m17.md. - Live map — Shared Leaflet map at the top of each
position-bearing panel (APRS / AIS / DSC / ADS-B) renders
decoded positions over OpenStreetMap tiles, colour-coded per
protocol (blue / cyan / red-distress / purple), with marker
tooltips and camera auto-fit. Same
<PositionMap>component drives all four panels. - Pure-Go voice path — IMBE (P25 Phase 1) and AMBE+2 (P25 Phase 2 / DMR) vocoders in Go, no DVSI / mbelib dependency. Per-call WAV + raw-frame sidecars; live PCM playback via direct ALSA / WASAPI / CoreAudio.
- Pure-Go SDR drivers — RTL-SDR, HackRF, Airspy R2 / Mini, Airspy HF+ family. USB transport on Linux (USBDEVFS), Windows (WinUSB), macOS (IOKit). USB-disconnect self-healing recovers dongles that drop off the bus and re-enumerate without restarting the daemon. See docs/hardware.md.
- Remote rtl_tcp SDRs — Mount any number of
rtl_tcpendpoints as virtual tuners alongside local USB dongles. The SDR can live on a Raspberry Pi at the antenna while the daemon runs on a beefier host; one entry per remote insdr.rtl_tcp. - Remote SoapySDRServer SDRs — Mount professional / high-bit-depth
hardware (USRP, LimeSDR, bladeRF, HackRF, Airspy, SDRplay, …) over the
network via the SoapyRemote
protocol, in pure Go with no CGO. Carries 16/32-bit IQ with native
frequency / sample-rate / gain control; one entry per remote in
sdr.soapy_remote. See docs/hardware.md. - Live spectrum / waterfall — In-browser FFT waterfall served
off the same IQ stream the trunking decoder consumes. New
internal/sdr/iqtapmulti-consumer fan-out lets future trunking-adjacent decoders (paging, AIS, ADS-B, ...) tap the same source without disturbing CC decode.GET /api/v1/spectrum/devicesWS /api/v1/spectrum/stream; web panel under/spectrum.
- CC Activity panel — focused web view of the trunked control-
channel chatter (grants, affiliations, registrations, patches,
talker aliases, CC lock / loss). Pure filter over the events
stream with per-row payload rendering; web panel at
/cc. RIDs in the feed are clickable chips that pivot into the per-radio detail view. - Radio IDs panel — per-radio (subscriber-unit) entity browser
with the same shape as Talkgroups. Merges the operator-configured
alias catalogue (per-system
rid_alias_fileCSV or JSON: alias, owner, tag, group, priority, lockout, watch) with the live affiliation tracker (last talkgroup, last seen, call count, decoded talker alias). Detail modal pulls the last 50 calls for the RID from the persisted call log. Web panel at/rids; REST at/api/v1/rids; gRPCRIDService. Talker-alias decoders cover the Motorola vendor TSBK form (control channel) and the Motorola voice-channel LCs (P25 Phase 1 LDU1 LCO 0x15 header- N × LCO 0x17 data blocks, run through Motorola's reverse-engineered alias cipher).
- Constellation viewer — live IQ scatter visualization that
taps the same broker the trunking decoder reads. Useful for
identifying signal shape (PSK / QPSK / FSK / C4FM / AM /
noise), spotting frequency offset, and checking demod /
equalizer health. Decimated to 2 ksps for the wire; canvas
scatter with energy banner. Web panel at
/constellation. - Symbol scope — live oscilloscope of the demodulated symbol
stream (OP25's "Symbol" plot): the pre-slicer soft waveform for
P25 C4FM and the sliced dibit decisions for CQPSK, driven off the
production receiver. Shares the constellation's offset / Hold /
follow-active-call controls. Web panel at
/symbols; offline view in SigLab. See docs/symbol-scope.md. - Bookmarks / frequency manager — UI-managed conventional
channel list (marine VHF, NOAA weather, FRS/GMRS, repeater
outputs, public-safety fall-back channels) stored in the
daemon's SQLite database. Edit / create / delete from the web
panel under
/bookmarks; REST at/api/v1/bookmarks. - One dongle, many carriers —
role: widebandpins a single SDR to a centre frequency and runs an internal channelizer so one dongle decodes every DMR Tier II conventional repeater, DMR Tier III control channel, P25 Phase 1 control channel, AND P25 Phase 2 control channel that fit inside its IQ bandwidth (e.g. several 12.5 kHz carriers inside a 2.4 MHz IQ window). Mix protocols on the same dongle. - One dongle, control + voice — with
voice_taps: Non a wideband entry, the daemon allocates per-grant DDC tuners from the dongle's IQ stream so trunked voice grants (DMR T3, P25 Phase 1, P25 Phase 2) decode inline on the same SDR that's already hosting the control channel — no separaterole: voicedongle needed for grants inside the wideband window. Out-of- window grants spill over to a physical voice SDR when present. DMR Tier III is 2-slot TDMA, so a single carrier can run two simultaneous calls — TS1 and TS2 are tracked, recorded (with a_ts1/_ts2filename suffix), and logged as distinct calls, each binding its own voice tap. See docs/hardware.md and samples/dmr-tier2-multichannel/. - DSP — Polyphase channelizer, Kaiser / RRC / Gaussian FIRs, FM / C4FM / GFSK / FFSK / DQPSK / π/4-DQPSK / π/8-H-DQPSK demods, Mueller-Müller + Gardner clock recovery, LMS + CMA equalizers, diversity combining.
- APIs — gRPC + HTTP/SSE + WebSocket; optional TLS +
bearer-token auth on mutations; Prometheus
/metrics; pure-Go SQLite call log; in-process pub/sub event bus. - Hamlib
rigctldintegration — optional TCP server speaking the standard Hamlib wire protocol so loggers, satellite trackers, and amateur-radio tooling (Cloudlog, GridTracker, PSTRotator,rigctl(1)) can read and set the control SDR's frequency. RX-only backend; see docs/rigctld.md. - Outbound call streaming — Broadcastify Calls, RdioScanner,
OpenMHz, live Icecast / ShoutCast with pre-encoded silence keep-alive.
Pure-Go MP3 encoder. See
internal/broadcast. - Baseband recording + offline replay — Two-channel 16-bit WAV capture and a replay driver that mounts captures back into the SDR pool as virtual tuners. Looping replay simulates a continuous source.
- Operator surfaces — Bubbletea TUI cockpit with 11 panels,
pure-browser React SPA web console, runtime config editing via
PATCH /api/v1/settings, RadioReference PDF / CSV importer with a config-builder wizard. - Site/system hunting —
gophertrunk huntmaps a previously undocumented trunked system from one or more control-channel IQ captures: auto-identifies the protocol, accumulates identity (P25 NAC / WACN / SYSID / RFSS / Site, and per-protocol ids), per-site control channels and observed talkgroups, then exports a GopherTrunk import bundle, a trunk-recorder config stanza, and a ready-to-paste RadioReference submission package — with an optional read-only RadioReference duplicate check so you don't submit a system that already exists. See docs/hunt.md. - Location + affiliation — NMEA-0183 GGA / RMC over the air
decoded into a SQLite
location_log; protocol-agnostic affiliation tracker fed from grants / registrations / affiliation events.
For the full per-protocol FEC chain reference, receiver internals, frame layouts, and API routes, see docs/architecture.md and docs/opt-in-features.md.
Once a grant event lands on the bus, the engine + recorder pipeline
runs end-to-end: voice device is allocated, the composer pulls IQ →
PCM, the recorder writes a WAV, the call is logged to SQLite, and
the API + TUI surfaces all light up. Every trunked control
modulation in the Features list has an end-to-end IQ → CC chain
shipping. SDRtrunk-parity subsystems (outbound streaming, baseband
recording, GPS / location, affiliation tracking, decoded-message
log, per-talkgroup policy) all ship.
Remaining gaps:
- Digital-voice composer chains. FM, DMR, P25 Phase 1 / 2, TETRA
TMO + DMO (clean-room ACELP), NXDN, dPMR, and D-STAR decode to
audio — TETRA verified bit-exact against the ETSI EN 300 395-2
reference codec (reproducible via the env-gated harness in
internal/voice/acelp/etsi_reference_test.go; the ETSI vectors are copyrighted and not committed). NXDN, dPMR, and D-STAR are wired end-to-end but not yet verified on air: each chain's AMBE interleave table is a documented placeholder awaiting a real voice capture (seeinternal/radio/{nxdn,dpmr,dstar}/voice_ambe.go). TETRA DMO records audio through the same ACELP vocoder but is experimental: call source / destination identity is not decoded (recordings file under group 0) and the chain still awaits its on-air A/B (issue #1003). YSF voice (plus EDACS ProVoice) is followed and logged but not yet turned into PCM. - DMR 2-slot interleaved voice + embedded-LC labelling. Both
timeslots of a carrier are tracked, recorded, and logged as separate
calls; a stride-2 interleaved superframe decoder
(
voice.NewInterleavedDecoder) separates the two slots and reassembles each slot's embedded Link Control (EMB → variable BPTC(128,72) → talkgroup/source) so a slot is routed to its call by talkgroup. This is the default for DMR Tier II conventional and Tier III trunked (Tier I direct-mode is genuinely single-slot and stays on the single-slot decoder);dmr_interleaved_voiceis a tri-state override to force it on or off per system. The same-slot cadence (264 dibits vs 288 with an inter-burst CACH) is auto-detected per call and re-locks on a later CRC-valid embedded LC. Each slot's calls are visible per-timeslot in the TUI / web active-call views and in thegophertrunk_dmr_voice_calls_total{system,timeslot}metric. What still wants a real IQ capture is narrow: cross-checking the exact ETSI embedded-signalling de-interleave order, the EMB QR(16,7) FEC, and the 5-bit CRC polynomial — internally consistent today but not yet validated against captured traffic. Drop a capture into the skip-gatedinternal/voice/composer/dmr_2slot_realair_test.go(-tags integration,GOPHERTRUNK_DMR_2SLOT_CFILE) — see docs/status.md. - Additional SDR validation. HackRF / Airspy / HF+ drivers
implement the documented USB vendor protocols under unit tests
against a mock transport. HackRF (Pro board-ID detection,
fpga_dc_block,dc_avoid,rf_amp) and Airspy (macOS async bulk-IN rework, native-rate behaviour) have since been exercised and fixed against attached hardware in the field, and all three backends now have hardware-gated harnesses (internal/sdr/{airspy,airspyhf,hackrf}/*_real_test.go, gated onGOPHERTRUNK_{AIRSPY,AIRSPYHF,HACKRF}_REAL;make test-*-real). The remaining gaps: Airspy HF+ has had no attached-hardware exercise at all (its harness has never been run against a real unit), and MRC diversity combining over SoapyRemote (field-tested on USRP X310 / B210) is still experimental pending a broader on-air A/B. - FEC inner-layer real-air validation. The NXDN per-protocol
interleaver + puncture chain is verified only against synthetic
vectors — no real NXDN capture exists in
samples/nxdn/, so the skip-gated real-air harnesses stay dormant. TETRA TMO on-air recovery margins are now characterised against a committed marginal-signal capture (internal/scanner/ccdecoder/testdata/tetra_cc_sync_loss_2s_144k.cs16, pinned in CI by the equalizer tests: ~12% → ~100% CRC-clean BSCH with the blind CMA equalizer, ~1.9× TCH/S yield with soft-decision decoding); the remaining TETRA item is the DMO on-air A/B above. - Vocoder level calibration. The comparison harness
(
internal/voice/calibrate/, CLI:cmd/voice-calibrate) and the AMBE+2 capture fixture (internal/voice/ambe2/testdata/dmr-voice.raw) ship; still missing are the DSD-FME / OP25 reference WAVs (internal/voice/imbe/testdata/p25-p1-voice{.raw,-dsdfme.wav}andinternal/voice/ambe2/testdata/dmr-voice-dsdfme.wav). The spec-faithful §6.2 spectral-amplitude enhancement now ships default-on (recordings.spec_amplitude_enhance); the reference WAVs are what gates its final quality sign-off.
The long-form status, per-protocol detail, and shipping-vs-pending checklist live in docs/status.md. Near-term plans live in docs/roadmap.md. Released work lives in CHANGELOG.md.
make dist # SPA + daemon — single binary that serves the web console at /
make build # Go-only — fast iteration; daemon shows a helpful 404 at / until bundled
make test # go test -race ./...
make vet # go vet ./...
make integration # daemon end-to-end test (no SDR required)The per-protocol "lights up" integration tests
(make integration-cc-<proto>) and the DVSI hardware-backend tests
(make test-dvsi) are documented in
CONTRIBUTING.md.
A bare go build ./cmd/gophertrunk works too — the binary
auto-stamps its version line from Go's built-in VCS info. Useful
when attaching a log to an issue and you want the commit hash in
the build-info line.
docker compose up -d
curl -s http://localhost:8080/api/v1/health
curl -s http://localhost:8080/metrics | grep gophertrunk_build_infoUSB pass-through recipe and the operator hardening playbook (TLS, bearer-token auth, Prometheus catalogue, smoke tests) live in docs/hardening.md.
Operator-facing docs live at gophertrunk.org
(rendered from this docs/ tree):
- Install — Downloads · Hardware · Linux · macOS · Windows
- Operate — Launcher · Windows user guide · TUI · Web console · Live config editing · Import (PDF / CSV) · Hunt (discover unknown systems) · Constellation · Symbol scope · Hardening
- Reference — Architecture · Vocoders · Voice calibration · DMR encryption · Opt-in features · Status · Roadmap · Cutting a release
- Project metadata — CHANGELOG · CONTRIBUTING · SECURITY · THIRD_PARTY_LICENSES
GopherTrunk is developed in the open and powered entirely by community support. If it's useful to you, please consider chipping in:
More ways to help: docs/support.md.
The code is licensed under Apache 2.0. Using GopherTrunk
additionally requires acknowledging the short
Terms of Service — the usual open-SDR ground
rules (lawful monitoring only, no defeating encryption, not for
safety-of-life use, no warranty). The Windows installer asks during
setup; on every other platform the CLI asks once on first run.
Unattended installs (services, containers, CI) accept with
gophertrunk terms accept or GOPHERTRUNK_ACCEPT_TERMS=1; nothing is
ever sent anywhere — acceptance is recorded in a local marker file.
