Part 4 of The Operator’s Cookbook, a 14-part series of complete, copy-paste GopherTrunk builds — one working rig per part, antenna to browser. Part 3 squeezed two conversations out of one conventional DMR repeater. This part leaves the 4-level-FSK family entirely for TETRA — π/4-DQPSK, a downlink that never stops transmitting, and the protocol GopherTrunk has spent more hard-won engineering on than any other. The payoff for you: the recipe is *short, because the difficult parts — the control-channel equalizer, the clean-room ACELP vocoder, the sync-loss watchdogs — now ship as defaults.*
TL;DR:
protocol: tetraplus the cell’s main-carrier frequency incontrol_channelsis a working TETRA TMO rig on onerole: controldongle: GopherTrunk channelizes the 25 kHz carrier to 144 kHz, runs the blind CMA equalizer that’s now default on the CC path (it lifted a marginal real-world control channel from ~12% to ~100% CRC-clean BSCH), decodes the cell identity, and registers four same-carrier voice taps — one per TDMA slot — feeding the clean-room ACELP vocoder (vocoder=tetra-acelp). Healthy looks liketetra cc locked freq=… mcc=… mnc=… la=…and calls in the browser; the classic failure signature (locked but silent,bsch_okhigh withsch_pdus=0) now self-heals via the payload watchdog.
Key takeaways
- TETRA is a different physical layer, same recipe. π/4-DQPSK at 18000 symbols/s in a 25 kHz channel — nothing like P25/DMR’s FSK — but the config shape you learned in Parts 1–3 carries over unchanged.
- The equalizer is on because real networks needed it. Field captures showed marginal control channels (~10 dB in-channel SNR) locking but decoding ~22% of their sync bursts; the default-on CMA equalizer recovers them. You configure nothing.
- One carrier can be the whole cell. A single-carrier TETRA site keeps voice on the control carrier’s other TDMA slots, so the same-carrier taps give you up to four concurrent calls with no voice radio.
- Encrypted networks still yield the map. TEA-encrypted voice is not decryptable, but the control channel is clear — talkgroups, identities and activity all decode. Know what your local law permits before you camp one.
Cheat sheet
| Concern | What it does | Where it lives |
|---|---|---|
| System definition | trunked TETRA cell, camped CC | protocol: tetra, control_channels |
| Cell identity | MCC / MNC / location area, decoded + displayed | tetra cc locked log line; Systems panel — MNI |
| CC equalizer | blind CMA, default-on for the TETRA CC path | nothing to configure; the equalizer story |
| Voice | four same-carrier slot taps → ACELP | automatic; ACELP, clean-room vocoder |
| Decode health | per-interval counters at debug | tetra: decode status (bsch_ok, sch_pdus, grants); cadence via tetra_status_interval_secs |
| Talkgroup names | GSSIs in the same CSV format | talkgroup_file |
| Is TETRA for you? | region + hardware reality check | TETRA scanner guide |
In this post
- What you’re building, and where it works — region and legality, briefly.
- The shopping list — Part 1’s again, with one frequency-band caveat.
- The config — two meaningful lines.
- First run — what healthy looks like — lock, identity, slots, voice.
- When it doesn’t work — sync loss, carrier offset, encryption.
- Variations — multi-carrier cells, status cadence, capture taps.
What you’re building, and where it works
TETRA is the trunked-radio standard of most of the world outside North America: public safety across Europe, plus transit, utilities, ports and airports on several continents. If you’re in the US, your local public-safety system is not TETRA — but TETRA gear does show up in commercial and industrial use, and Part 5’s direct-mode radios travel everywhere. One sentence on law, kept factual: countries differ sharply on whether receiving public-safety traffic is legal at all, and some prohibit even possession of a capable receiver — check your jurisdiction before you camp a government network. The TETRA scanner guide covers the landscape, including why almost no consumer hardware scanner decodes TETRA voice — which is precisely why this recipe exists.
Technically, a TETRA cell’s main carrier is this build’s target: a 25 kHz channel whose downlink transmits continuously, four TDMA slots per frame, control signalling on one and — on single-carrier sites — voice calls on the others. GopherTrunk channelizes it to 144 kHz (8 samples per symbol), demodulates the π/4-DQPSK, and decodes everything from the sync bursts up. The physics of that carrier fills its own series opener, TETRA End to End Part 1; the cookbook just points a dongle at it.
The shopping list
The Part 1 hardware transfers whole: one ~$35 RTL-SDR, an antenna, a computer. Two caveats. TETRA lives mostly at 380–430 MHz (public safety and commercial), so extend the kit whip accordingly, and expect ACELP + equalizer + four potential slot decodes to want more CPU than a P25 rig — any recent laptop is fine; the smallest Pis will feel it. If your target network sits at the weak end, the Analog Edge antenna and feedline parts are the upgrade path; the numbers below tell you whether you need them.
The config
Every key verified against config.example.yaml:
storage:
path: "../data/calls.db"
recordings:
dir: "../recordings"
sdr:
sample_rate: 2_400_000
devices:
- serial: "00000001"
role: control
gain: "auto"
trunking:
systems:
- name: "Harbour-TETRA"
protocol: tetra
control_channels:
- 419_512_500 # the cell's main carrier
talkgroup_file: "../config/talkgroups-tetra.csv" # optional (GSSIs)
That’s the whole rig. Things you might expect to configure and don’t: the
144 kHz channel rate (per-protocol, automatic), the equalizer (default-on for
the TETRA CC path), the colour code for descrambling (recovered from the
BSCH), and voice devices (four same-carrier slot taps are registered for
every TETRA system automatically). The optional
tetra_status_interval_secs sets how often the decode-status counters
accumulate before logging — leave it defaulted until the troubleshooting
table sends you there. Talkgroups in TETRA are GSSIs; the CSV format is the
same Decimal/Alpha Tag shape as every other protocol’s.
First run — what healthy looks like
Start the daemon with log.level: debug for the first session — TETRA’s
continuous downlink makes the debug stream genuinely informative rather than
spammy. The lock line carries the decoded network identity:
INF tetra cc locked freq=419512500 mcc=262 mnc=1023 la=2101 system=Harbour-TETRA
mcc/mnc are the Mobile Network Identity
— country code and network — and they appear in the web Systems panel too;
check them against what you expect for the network, because a strong adjacent
cell is a real possibility (see the table). Then the five-second heartbeat:
DBG tetra: decode status system=Harbour-TETRA locked=true carrier_off_hz=-412.3 baud=18000 sb_bursts=38 bsch_ok=37 bsch_fail=1 sysinfo=37 sch_pdus=214 sch_pdus_fail=6 grants=3 colour_code=27
Read it like a meter: bsch_ok near sb_bursts means sync is healthy;
sch_pdus climbing means the signalling payload is decoding (this is the
number that matters — more below); grants means traffic. When a call goes
out, the composer announces the slot-tap chain and the recorder does its
usual thing with a TETRA-specific vocoder name:
INF composer: tetra voice follow started — TCH/S decode + ACELP vocoder serial=cc:same-carrier:2 group=61432 timeslot=2 ...
INF recorder: call started ... tg=61432 ... vocoder=tetra-acelp
The voice you hear came through a clean-room ACELP implementation validated bit-identical against the ETSI EN 300 395-2 reference codec — the conformance story is worth reading once, because it’s why “TETRA voice sounds wrong” almost never means the vocoder.
When it doesn’t work
TETRA’s troubleshooting rows are unusually well-grounded: most came from multi-hour operator field logs, diagnosed in the sync-loss deep dive.
| Symptom | Likely cause | Fix |
|---|---|---|
| No lock at all | wrong frequency, or the carrier isn’t TETRA | gophertrunk hunt -candidates identifies it; TETRA carriers are 25 kHz and continuously keyed — easy to spot on the Spectrum panel |
Locks, drops, tetra: dsp resync (signal-time decode drought…), re-hunts |
weak signal — the marginal regime. Field captures split cleanly: ~18 dB in-channel SNR decodes ~100% of BSCH, ~10 dB locks but decodes ~22% and storms resyncs | The equalizer already mitigates this; the rest is RF — antenna, feedline, siting. Raising software knobs will not help; raising the signal will |
bsch_ok healthy, sch_pdus=0, no traffic ever |
the “locked but deaf” signature — historically the AFC latching a wrong ±4.5 kHz alias bucket | Self-healing now: the payload watchdog forces a resync after 12 s of lock without payload and escalates to a re-hunt. If you still see it persist, that’s a capture worth reporting |
ccdecoder: control carrier offset far from configured frequency (issue #815) |
you’ve locked an adjacent cell’s stronger carrier, or the tuner is badly mistuned | Check the reported MCC/MNC/LA against your target; set the device ppm. The WARN requires the offset to persist 10 s, so it isn’t estimator jitter |
| MCC/MNC in the lock line looks absurd | one corrupted sync burst can’t rewrite identity — changes need two consecutive agreeing decodes — so a stable absurd value means you’re genuinely on a different network | Believe the decoder; re-check which carrier you camped |
| Calls appear, recordings are silent or missing, talkgroups decode fine | the network encrypts voice (TEA air-interface encryption) | Nothing to fix: GopherTrunk decodes all clear signalling — talkgroups, identities, activity — but does not decrypt TETRA. Per-system encrypted_calls: mode: metadata keeps voice taps free |
| Garbled voice on an otherwise-strong signal | multipath/ISI on the traffic slots | The voice path runs its own equalizer; if it persists, capture IQ — the soft-decision + equalizer work roughly doubled yield on exactly such captures |
The second row carries this series’ standing RF sermon. The equalizer, the soft-decision decoding, the watchdogs — all of them widen the marginal band, none of them repeal it. The decoder can only be as good as the samples, and the 10-vs-18 dB split above is what that abstraction costs in concrete numbers on a real network.
How this recipe shapes operator practice
sch_pdusis the truth;bsch_okis just the handshake. Sync bursts survive conditions that kill payload. A rig is healthy when payload counters climb, not when lock flags are green.- Identity is a checksum for your config. MCC/MNC/LA in the lock line either match your intended network or you’re camped somewhere else — check it on day one, before you name a single talkgroup.
- Defaults encode field experience. Every default this recipe leans on — equalizer, watchdogs, confirmation thresholds — exists because an operator’s log demanded it. Overriding them is for A/B tests, not setup.
Variations
- Multi-carrier cells. Bigger sites put voice on secondary carriers the
same-carrier taps can’t reach. Add a second dongle as
role: voiceand off-carrier grants bind it automatically — the same spill-over pattern as Parts 1–2. - Status cadence.
tetra_status_interval_secs: 30calms the debug heartbeat for long soak sessions; the counters accumulate over the window either way. - Capture-on-failure.
baseband.auto_recordwithon_cc_sync_loss: trueandtap: ddcwrites a small 144 kHz IQ capture at the exact moment a locked CC loses sync — the feature the sync-loss investigation was solved with, and the single best thing you can attach to a TETRA bug report. - Pre-stripped fixtures. The
tetra_channel/tetra_channel_coding/tetra_colour_codeopt-out keys exist for feeding pre-decoded capture files, not live air — leave them alone on a real rig.
Where this goes next
TMO assumed infrastructure: a base station, a cell, a downlink that never stops. Part 5 scans TETRA radios talking directly to each other — Direct Mode, where there is no control channel, no talkgroup in the grant, and a colour-code recovery problem interesting enough that the rig solves it statistically. It’s the newest and most honestly-caveated recipe in the series.
FAQ
Can an RTL-SDR really decode TETRA voice? Yes — a 25 kHz π/4-DQPSK carrier is comfortably within an RTL-SDR’s bandwidth and dynamic range, and GopherTrunk’s TETRA path (144 kHz channel rate, CMA equalizer, soft-decision channel decoding, clean-room ACELP) was developed and validated on exactly such captures. Voice quality tracks signal quality; the equalizer buys real margin but not miracles.
What do MCC and MNC mean in the TETRA lock line? Mobile Country Code and Mobile Network Code — together the Mobile Network Identity that names the network, like a cellular PLMN. GopherTrunk decodes them from the broadcast sync channel and shows them in the log and the Systems panel; they’re your first check that you locked the network you intended.
Can GopherTrunk decode encrypted TETRA networks?
It decodes everything that’s clear — and on TEA-encrypted networks that’s
the entire control channel: talkgroups, subscriber activity, cell topology.
Voice on such networks is not recoverable, and GopherTrunk performs no
key recovery. The encrypted_calls per-system policy stops encrypted calls
from tying up voice taps.
Why does GopherTrunk process TETRA at 144 kHz when the channel is 25 kHz? 144 kHz is a processing rate — 8 samples per symbol at 18000 symbols/s — not a bandwidth. Every protocol gets channelized to its own designed rate (the C4FM family gets 48 kHz); the receiver’s filters and timing loops are sized from that constant, which is what makes the decode rate-invariant to whatever your dongle captured at.
Do I need a separate voice SDR for TETRA?
Not for a single-carrier cell: voice occupies the control carrier’s other
TDMA slots, and GopherTrunk registers four same-carrier slot taps per TETRA
system automatically. Only multi-carrier sites — where grants point at
secondary carriers — need a role: voice dongle for the overflow.
Series navigation
Part 4 of 14 · ← Part 3: One Repeater, Two Conversations — Conventional DMR · Next → Part 5: TETRA Direct Mode — Scanning DMO