Digital-protocol feature parity
A review of the advanced decode features GopherTrunk has accumulated —
mostly on the TETRA path, some on P25 — and where each one now stands
across the other digital protocols. This page records three things: the
capability matrix as of the parity series, the ports that landed (with
their A/B recipes), and the ports that were deliberately not made and
why. The governing rule throughout is the #764/#771 discipline from
CLAUDE.md: a green synthetic decode is never proof of an on-air fix, so
every decode-affecting port here landed opt-in/off-by-default (or with an
explicit no-harm argument), pinned by failing-first synthetic tests, with a
capture A/B recipe an operator can run.
Capability matrix
Receiver / pipeline capabilities per protocol after the parity series (“opt-in” = off by default behind a per-system config key):
| Capability | TETRA TMO/DMO | P25 P1 C4FM | P25 P1 CQPSK | P25 P2 | DMR T3 | DMR T1/T2 | NXDN |
|---|---|---|---|---|---|---|---|
| Complex equalizer | SnapshotCMA on (rx) + SnapshotLMS opt-in (tetra_traffic_lms) |
n/a (nonlinear path) | FSE always-on | CMA opt-in (p25_phase2_equalizer) |
n/a | n/a | n/a |
| Soft-decision FEC | full (soft Viterbi RCPC/RM/TCH/S) | opt-in (p25_phase1_soft_decision) |
— | opt-in (p25_phase2_soft_decision) |
— | — | opt-in (nxdn_soft_decision) |
| Carrier recovery | carrierAFC (alias-reject, re-prime) | CoarseAFC (+opt-in DDA) | CV-gated seed + Costas | seed now CV-gated + Costas | CoarseAFC (post-clock) | CoarseAFC (post-clock) | opt-in (nxdn_afc, post-clock CoarseAFC) |
| DC block | voice paths | voice path | voice path | opt-in (p25_phase2_dc_block) |
— | — | — |
| Decode-drought resync watchdog | checkResync + payload escalation | yes (resyncGuard) | yes | yes | yes | n/a (conventional) | yes |
| Diagnostic taps (soft/eye) | soft (complex diffs) | full | constellation | soft diffs | full | full | full |
| Symbol-scope panel | yes | yes | yes | yes (p25-phase2) |
yes | yes | yes (nxdn) |
| Replay harness / GT_ knobs | richest (TMO+DMO) | fixtures + metrics test | — | GT_915_* | GT_DMR_* | GT_DMR_* | GT_NXDN_* |
Bold entries landed in the parity series. TETRA-only engine behaviours
(radio/talkgroup reclassification, discovery corroboration, notification
hold, channel folding, per-transmission split) are intentionally not in
this table — they rest on TETRA’s GSSI/ISSI disjointness and are gated to
protocol == "tetra" on purpose (see “Not ported”).
What landed
Zero-risk parity
tetra-dmo.rawsidecar — DMO voice calls carry the same post-FEC TCH/S speech frames as TMO, so they now get the same always-on.rawsidecar (internal/voice/recorder.go).- NXDN diagnostic taps + symbol scope — the NXDN receiver gained the
same
SoftSink/EyeSinktaps the DMR/P25 receivers carry, and the web symbol panels gained annxdnreceiver, so an NXDN rig’s panels no longer open a wrong-protocol receiver (thesymbol_protolesson). - NXDN replay harness —
TestReplayNXDNRealCapture(cmd/gophertrunk/nxdn_realcapture_test.go):GT_NXDN_IQ(cs16),GT_NXDN_IQ_RATE(default 48000),GT_NXDN_ALLOW_EMPTY=1for weak-signal baselines,GT_NXDN_SOFT=1for the soft-decision A/B,GT_NXDN_AFC=1for the opt-in carrier-AFC A/B. Reports FSW hits, CAC channel-decode/CRC yields (hard and soft), lock and grant counts. NXDN real-air captures remain the blocker for the whole NXDN voice path — seedocs/decoder-capture-needs.md. - P25 Phase 2 carrier-seed multipath gate — the Phase 1 CQPSK
seedModulusCVgate (issue #492) is now applied to the Phase 2 coarse carrier seed. A simulcast-like two-ray channel biases the seed by ~1 kHz on a 0 Hz carrier and mis-tunes the NCO past the Costas pull-in for good; the gate detects the ISI a wrong de-rotation leaves and lets the Costas loop acquire instead (measured on the synthetic fixture: tail SER 0.56 → 0.01 with the #915 CMA equalizer). Default-on: rejection falls back to the pre-existing weak-coherence behaviour, and a genuine 1500 Hz seed still fires (pinned no-harm).
Pipeline hygiene
- Signal-time decode-drought watchdog (
resyncGuard,internal/scanner/ccdecoder/resyncguard.go) — the TETRAcheckResyncdesign generalised to the P25 Phase 1/2, DMR Tier III and NXDN CC pipelines and their widebandt2 taps: a full window of PROCESSED signal (not wall clock — starvation-immune) with no CRC-clean decode forces a fast reacquire from centre. Each control channel gained aLastActivityNanoheartbeat stamped on its own decode unit (TSBK / MAC PDU / CSBK+MBC / CAC). DMR T3 and NXDN also gainedResyncReset: their Process adapters key on absolute dibit indices, and a receiver reset without dropping that state would silently discard every post-reset sync match (T3’sbufStartwedge). Not wired on conventional/camped channels (DMR Tier I/II, TETRA DMO) where inter-transmission silence is normal; TETRA’scheckPayloadResyncescalation stays TETRA-only. p25_phase2_dc_block(opt-in) — the P1/TETRA voice-receiver DC-removal high-pass, ported to the Phase 2 traffic-channel receiver and threaded through the grant (P25Phase2Decode.DCBlock) to the composer, both pipeline factories, and every config surface. CC path stays DC-untouched, per the TETRA/P1 rule.
Opt-in soft-decision FEC (the TETRA yield lever, ported)
nxdn_soft_decision— the NXDN receiver derives two per-bit LLRs per dibit from the 4-level soft symbols (sign axis / magnitude threshold) and the spec CAC decode runs a true per-bit soft Viterbi (framing.ViterbiK5Soft,nxdn.DecodeCACChannelSoft,ControlChannel.ProcessSoft). Measured on the seeded synthetic channel: CAC CRC yield 26/200 hard → 151/200 soft at σ=0.7; at the level where hard decodes nothing the soft CC still locks. A/B a capture withGT_NXDN_IQ=… GT_NXDN_SOFT=1 go test ./cmd/gophertrunk -run TestReplayNXDNRealCapture -vand comparecac_crc_okvscac_crc_ok_soft.p25_phase1_soft_decision— the C4FM receiver emits the same per-bit LLRs (BitLLRSink), the control channel keeps them in lockstep with its dibit buffer (StashSoft, the TETRA contract — dropped to a hard fallback on any misalignment), and the TSBK trellis runsframing.DecodeP25TrellisSoftthrough the same framing, status-symbol stripping, rotation handling and CRC gate. Measured: TSBK yield 81/300 hard → 212/300 soft at σ=0.75; 0 → 12/12 on the end-to-end CC fixture at σ=0.85. C4FM only (a CQPSK site decodes hard regardless). A/B a capture offline via siglab metadata (p25_phase1_soft_decision: on) against theTestReplayP25RealCaptureMetricsfixture flow, comparing TSBK CRC-ok counts.
Both stay off by default until an operator capture A/B confirms the
gain on air — the same posture the TETRA SnapshotLMS and P25 Phase 2
#915 knobs shipped under.
Follow-ups from the parity review (second pass)
- P25 Phase 2 symbol-scope panel — the web symbol panels gained a
p25-phase2receiver (internal/scanner/symbolscope/scope.go,symbolProtoFor, and the five SPA panel lists), driving the true π/8-DQPSK complex constellation from the Phase 2 receiver’sSoftDibitSink. Diagnostic-only; a P25 Phase 2 rig’s panels no longer fall back to the wrongp25-c4fmreceiver. - NXDN post-clock CoarseAFC (
nxdn_afc, opt-in) — the DMR #836 design copied exactly (internal/radio/nxdn/receiver/receiver.go): applied post-clock/pre-AGC, eye recentred by the tracked offset, ahead of the soft-LLR derivation. Pinned failing-first byTestReceiverIsCarrierOffsetInvariant(400 Hz offset: decode agreement collapses without the AFC, ≥0.95 with it). Opt-in unlike DMR/P25, and the reason is a finding of this pass, not caution for its own sake: NXDN’s CAC carries no air-interface whitening, so a zero-heavy L3 payload convolutionally encodes to long constant-dibit runs, and the plain coarse tracker drifts onto that data mean (the documented issue #402 mode) — an always-on port collapsed CAC CRC yield to zero on the repo’s own synthetic SITE_INFO round-trip (the siglab fixture, which now pins the default-off path). Any default change needs a real mistuned NXDN capture and likely the P25-style decision-directed pairing. - NXDN widebandt2 channel — the multi-tap wideband engine can now
build an
nxdncontrol-channel tap (widebandt2/engine.go), with the same knobs as the single-channel pipeline (Viterbi mode, band plan, deviation,nxdn_soft_decision) plus the drought guard and aDecodedFramesactivity counter on the NXDN control channel. Config validation acceptsnxdnin the wideband role’s CC-required list. tetra_traffic_lms(opt-in) — the traffic-burstSnapshotLMSmidamble equalizer, production-wired: per-system config → both pipeline factories →tetra.Options.TrafficLMS→Grant.TETRATrafficLMS→ both composer receiver-build sites (EnableLMSEqualizer+ theSymbolSink → StashSymbolsstash the GT_TETRA_LMS harness uses). Off ⇒ byte-identical (no symbols stashed). Default-on stays gated on the operator’s capture A/B:GT_TETRA_LMS=1inTestTETRAMultiSlotReplay, comparingtraffic_marked_crc_soft.- P25 Phase 2 carrier-offset reporter —
p25Phase2Pipelinenow satisfies the decoder’safcReporter, so Phase 2 CCs get thecontrol_channel_carrier_offsethealth stamp and the #815 wrong-site/mistune WARN like P1/TETRA. Deliberately does not carry theappliesAutotunemarker (autotune sampling stays P1-only); pinned byTestP25Phase2PipelineReportsAFCOffset.
Deliberately NOT ported
- Complex equalizers (CMA/LMS/FSE/Snapshot*) onto the C4FM family (P25 P1 C4FM, DMR, NXDN). The FM discriminator is nonlinear: after it, a multipath channel is no longer a linear complex convolution, so the complex linear equalizers that ~2×’d TETRA yield do not model the channel. A real-valued post-discriminator DFE is new algorithm research, not a port. The portable lever for this family is soft-decision FEC — which is what landed.
- P25 P1 soft IMBE (voice) FEC.
CLAUDE.md’s weak-signal P25 note is explicit: no voice-quality change lands without thesamples/p25/weak-signal voice capture. Soft Golay(23,12)/Hamming decoding is also new algorithm work (Chase-style), not a port. Deferred until that capture lands; the TSBK soft path above is the control-channel half of the same lever. - TETRA engine niceties onto DMR (radio/TG reclassification,
discovery corroboration, channel fold, notification hold). DMR shares
one 24-bit ID space between talkgroups and radios, so the TETRA
invariant these rest on does not hold — the gating is deliberate and
pinned by
engine_dmr_classify_test.go. checkPayloadResync/MarkLost escalation to other protocols. It was tuned against a specific observed TETRA field failure (the 19 Aug AFC alias latch). Porting the destructive escalation without an equivalent observed symptom is speculation; the first-tier drought reacquire is what generalises.- DMR EMB QR(16,7) and RC(32,11) FEC. The in-code posture notes
(
dmr/emb.go,dmr/rc.go) are explicit: no off-air capture and no open reference decoder implements these codes, so implementing spec-derived tables would be unvalidatable — the self-consistent synthetic trap (#764/#771; the TETRA class-2 CRC lesson). The parity is preserved raw; the embedded-LC BPTC+CRC downstream remains the integrity gate. (The “unapplied AMBE FEC” reading was stale: the composer’s DMR voice chain FEC-decodes every AMBE frame viadmrvoice.DecodeAMBEFramebefore the vocoder.) - NXDN voice deinterleave / scramble / CAC structural changes. All
flagged “UNVERIFIED ON AIR” placeholders; capture-gated. The new
GT_NXDN_*harness is how a contributed capture gets baselined. - Backlog (new features, not ports): DMR Tier I / TETRA DMO
widebandt2 channels (DMO’s grant/colour machinery is coupled to the
daemon path — a refactor, not a port, and DMO is itself on-air-gated);
the NXDN 4800-baud BFSK receiver (a second demod variant against a
frame layout still unverified on air); a DMR pre-clock/freeze AFC
stage (the acknowledged follow-up in
dmr/receiver/receiver.go— needs a grossly-mistuned capture to validate the freeze design;sdr.ppmremains the documented answer). The P25 Phase 2 symbol scope, NXDN AFC, NXDN widebandt2 channel, TETRA traffic-LMS wiring and Phase 2 offset reporter formerly listed here landed in the second-pass follow-ups above.
Verifying
make vet test— everything above is pinned by unit tests, including the failing-first ones named in each section.make integration— daemon-level CC decode for the pipelines the watchdog touched.- Capture A/Bs (operator-run, the gates for turning any opt-in on by
default): the
GT_NXDN_*recipe above; the siglabp25_phase1_soft_decisionA/B;GT_915_*for the Phase 2 seed gate on a real simulcast capture.