Also known as: adjacent-channel lock, wrong-site lock, offset_hz fingerprint
Adjacent-channel lock is the failure mode where GopherTrunk decodes a neighbouring channel’s signal while reporting it under the configured frequency. The receiver tunes its down-converter so the configured frequency sits at 0 Hz, then locks whatever compatible C4FM carrier the matched filter finds inside the roughly ±24 kHz channel passband — and every downstream event, site record, and log line carries the configured frequency, not the carrier actually decoded. At 12.5 kHz channel spacing, a strong neighbour one step away sits comfortably inside that passband. The result is silent wrong-site data: internally consistent, repeatable, and wrong.
The fingerprint
The reported carrier offset (offset_hz) sitting near the channel spacing —
±12.5 kHz for narrowband P25 — is the fingerprint. Crystal error on a
reasonable dongle is 1–3 kHz; a TCXO front end after autotune leaves a sub-kHz
residual. Nothing legitimate parks at exactly one channel step.
In #815, a control
channel configured for Geelong at 420.0750 MHz confidently and repeatably decoded
RFSS 2 / Site 7 — Mt Anakie, whose control channel is 420.0875 MHz, one 12.5 kHz
step up. Off-pipeline capture + spectrum (rung 3 of the
diagnostic playbook) showed no carrier at
all at 0 Hz offset and a dominant carrier at +12.2 kHz; once instrumented, the
live receiver reported offset_hz between 12,436 and 12,699 Hz. The receiver had
been measuring the true offset all along via its
AFC — the value simply was never
inspected unless autotune was enabled.
Detection and configuration
sdr.carrier_offset_warn_hz— while a control channel is locked, GopherTrunk warns when the total carrier offset (autotune correction plus receiver residual) exceeds this magnitude. The default of 4000 Hz is chosen to sit in the gap: a good front end never trips it, a 1–3 kHz crystal error stays under it, and a 6.25 or 12.5 kHz adjacent-channel step always trips it. Raise it for a high-drift dongle. The warning is advisory only — it never changes tuning or decoding.control_channel_carrier_offset_hzonGET /api/v1/sites— the same total offset, published per site for dashboards and scripted checks.
A subtle trap from the follow-up (#866): the API field was initially wired to the residual offset only, while the warning computed the total (applied autotune correction + residual). With autotune enabled, the API field would have hidden exactly the lock the warning flags. Both now report the total. Autotune itself can never mask an adjacent lock either way — its plausibility bound (~1.5 kHz at 420 MHz) is far below a 12.5 kHz step.
Two verification cautions:
- The warning lives in the live daemon path only.
gophertrunk replaydrives the receiver directly and does not construct the component that emits the warning, so replaying a capture of an adjacent lock stays silent. Judge the symptom in the path where it lives. - The decoder cannot know which site you meant. The lock in #815 was only caught because the user cross-referenced the decoded site identity against RadioReference and spectrum-licensing records and noticed it was the wrong site. When decoded identity matters, verify it against an external database — the fix is to tell the operator, not to change the DSP.
Carrier offset is not a quality metric
The companion lesson from
#858: a well-locked
carrier (near-zero offset) can still decode badly at range, so carrier offset is
a poor proxy for signal quality. Use the decode-quality fields on
GET /api/v1/sites instead:
| Field | Meaning |
|---|---|
control_channel_tsbk_error_rate |
Percentage of TSBK blocks that failed Viterbi + CRC — a frame-error rate, not a pre-FEC bit-error rate |
control_channel_tsbk_count |
How many TSBKs the rate was measured over — a confidence weight; discard readings with count < 100 |
control_channel_decode_quality |
The rate bucketed: clean ≤ 1%, marginal ≤ 5%, poor above |
All three are only populated once TSBKs actually decode. Read the two families of
fields together: a large control_channel_carrier_offset_hz says you may be
decoding the wrong carrier; a high control_channel_tsbk_error_rate says the
carrier you are decoding is marginal. Neither substitutes for the other — see
signal-quality signatures for the wider
symptom table.
Provenance
- #815 — adjacent-channel bleed-through at 12.5 kHz spacing; the offset fingerprint,
carrier_offset_warn_hz, and the API field. - #866 — follow-up pull request aligning the published offset field with the warning’s total-offset computation.
- #858 — per-site decode-quality fields and why carrier offset is a bad quality proxy.