From the Issue Tracker is a postmortem series mined from GopherTrunk’s closed GitHub issues. Every post is a true story with receipts: the symptom as a user actually reported it, the plausible explanations that turned out to be wrong, the diagnostic that finally cracked it, and the fix — linked back to the original issue thread so you can check the work.
Where the other series on this blog explain how GopherTrunk works, this one explains how it broke, and what each breakage taught us. The recurring villains: round-trip tests that validate their own bugs, success-only log lines whose silence means nothing, hardware that lies politely, and “smoking gun” evidence that was an artifact of the instrument. The series closes with three meta-lesson posts that pull those threads together.
Every post reads three ways: a TL;DR + cheat-sheet for skimmers, bold headers and tables for the medium read, and the full investigation narrative for the deep read.
Reference companions to this series live in the Field Guide’s Field Notes domain — the same knowledge, condensed into look-up form: hardware quirks, configuration gotchas, diagnostic signatures, and protocol facts the specs hide.<ol class="post-list series-list"><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 1: The First P25 Lock — Eleven Fixes Between ‘Trying’ and ‘Locked’</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A P25 control channel that SDRTrunk and OP25 decoded in five seconds took GopherTrunk eleven fixes over five days to lock — and the last two bugs were a wrong BCH generator polynomial and a wrong CRC variant that every synthetic round-trip test had happily agreed with.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 2: The Talker-Alias Hunt — Three Wrong Transports and an Architectural Gate</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A month of chasing blank P25 talker aliases through three wrong transport theories, until a per-opcode census and SDRTrunk ground truth revealed the aliases were riding traffic-channel signalling — and GopherTrunk’s decoder was architecturally gated behind winning a voice tuner.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 3: Encrypted, Says Who — Four Layers Between a Flag and Its Metadata</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A P25 Phase 2 system flagged every encrypted call correctly but never reported which algorithm or key — because a fictional MAC opcode, a missing carrier loop, a 48-bit sync constant in a 40-bit field, and a swapped dibit map were stacked on top of each other.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 4: The Dongle That Heard Nothing — One Line in a Register Table</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">An RTL-SDR Blog V4 that only ever received noise survived four confident wrong diagnoses, a crystal set wrong and then wrong again, and a missing input-routing block — until the reporter read the register tables and found the one value that had to be 1.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 5: Ten Megasamples — When the Bug Is in the Samples Themselves</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Raising an Airspy R2 from 2.5 to 10 MS/s made every wideband tap go dark, including the strong one — two real bugs got fixed, an alarming AGC number turned out to be normal, and an independent-resampler A/B proved the remaining 10 dB deficit was recorded into the capture itself.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 6: CQPSK in Four Acts — Fixing the Linear Path One Layer at a Time</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A P25 control channel that decoded perfectly in C4FM produced almost nothing in CQPSK mode. The fix took four rounds — a missing carrier-recovery stage, a multipath-poisoned frequency seed, a fractionally-spaced equalizer, and a BCH hot path that only appeared once locking finally worked — and the most convincing piece of evidence in the thread was a broken instrument.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 7: The LSM Myth — When Your Own Docs Are the Bug</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">GopherTrunk’s docs, config comments, and UI labels all taught that simulcast P25 sites need the CQPSK demodulator. A user proved a genuine three-tower simulcast system decodes fine in C4FM — and that forcing CQPSK kills it. The real fault was a gain value, and the real fix was rewriting our own guidance across every surface it had leaked into.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 8: Nineteen Dibits — A Perfect Hypothesis Meets a Rail-Pinned ADC</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A meticulously argued bug report proved that GopherTrunk’s wideband DDC starved the P25 demodulator with chunks too short to hold a frame sync word — except the sync detector buffers across chunks, the incriminating log line only fires while unlocked, and the raw capture was 50% pinned to the ADC rails. The fix was turning the gain down.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 9: Broken Pipe — Six Rounds of Traces for One USB Write</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Two NESDR SMArt v5 dongles that worked in every other SDR app failed GopherTrunk’s tuner init with one EPIPE. Five parity fixes didn’t take, one of them shipped a regression of our own making, and paired USB traces ruled out everything until a 5 ms sleep closed it.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 10: Faster Than libusb — When the Second Write Outruns the First</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A Windows 10 dongle accepted a control write and then rejected its byte-identical twin with ERROR_GEN_FAILURE. The retry-envelope fix couldn’t work by construction — the real bug was that a pure-Go WinUSB path is thinner than libusb’s stack, and libusb’s overhead had been hiding a firmware race.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 11: Detected but Not Present — One Hex Code from a Fix That Already Existed</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">macOS enumerated the dongle, then the daemon insisted the configured SDR wasn’t on the bus. One probe line surfaced IOKit kern_return 0xe000404f — a pipe stall GopherTrunk already knew how to recover from on Linux and Windows, except the macOS backend never translated the error.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 12: Seventy-Eight Degrees — The Phase Angle That Named the Bug</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">An Airspy R2 that refused to open because its opcode table was systematically shifted, then opened and decoded nothing at all. One diagnostic triplet — EVM 22.7%, phase imbalance +78.1°, image rejection 3.3 dB — read as a sentence, named the bug: a real-sampling stream misread as interleaved I/Q.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 13: The SoapyRemote Handshake — Three Wrong Root Causes and a Server That Says Nothing First</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A SoapySDRServer fronting a USRP X310 segfaulted the moment GopherTrunk connected. The reporter filed three confident root-cause analyses; all three were disproved by the code. The real bugs were a two-phase, two-socket SETUP_STREAM handshake the client half-implemented — and a TCP flow-control ACK without which the server sends zero samples, forever.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 14: The Recorder Is the Decoder — Perfect Recordings, Silent Speakers</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Live audio in the web UI did nothing while recordings came out flawless. Two plausible fixes shipped before a 44-byte curl probe exposed the real cause — only the recorder ever decoded vocoder frames to PCM, so every live path was silently skipped.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 15: The Silent MP3 — Three Encoder Bugs and a Test That Checked One Frame</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Rdio Scanner accepted every upload and played silence. Behind it sat three independent MP3 encoder bugs — an illegal bitrate corrupting headers via putBits(-1), bit-reservoir stuffing desync, and a mono stride that skipped half the audio — none caught by a test that only checked the first frame’s sync word.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 16: The Channel That Was Its Own Voice Channel — Conventional FM and the IQ Broker</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Conventional analog FM channels lit up with activity and recorded nothing. Fixing the composer’s digital-only gate revealed a second bug hiding behind it — in conventional scanning the monitored channel is the recorded channel, and a single-consumer device API can’t serve both.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 17: Placeholder Constants — The TETRA Sync That Never Existed</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">TETRA never locked because the training-sequence constants were placeholders — 76 bits declared into a 64-bit variable, bits conflated with dibits, and a linear dibit mapping where the air uses Gray — and every test passed because the fixtures were built from the same wrong constants.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 18: The Stall That Wasn’t — A Dongle Off the Bus and an Opcode Off the Books</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A decoder that “stalled” after a clean control-channel lock turned out to be a dongle leaving the USB bus, a retry loop that matched only one error sentinel and left the daemon half-dead, and — found by running p25_survey as an oracle — a TDMA IDEN_UP opcode the dispatcher never handled.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 19: One Render Loop — A Blank UI, a Host-less URL, and React Error #185</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">A blank web UI, a WebSocket URL missing its host, sockets closing before the handshake, and React’s maximum-update-depth error all traced to a single unstable selector in an effect dependency array — plus a separate go:embed trap that shipped a binary whose web assets were one .gitkeep file.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 20: The Self-Consistent Trap — Round-Trip Tests That Validate Their Own Bugs</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Seven GopherTrunk bugs shipped behind green test suites because the encoder and decoder shared the same wrong constant, table, or convention — a wrong sync word, a wrong BCH polynomial, a skipped descramble — and a round-trip test cannot see a mistake it makes twice.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 21: Census Everything — The Silence of a Success-Only Log Line Carries No Information</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">Five GopherTrunk investigations stalled or went sideways because the logs only spoke on success — zero lines from a dead decoder looks identical at every failure stage. The fix each time was a census — an unconditional per-unit count with denominators — and this post distills when and how to add one.</p></li><li class="post-card"> <h2 class="post-card__title">From the Issue Tracker, Part 22: Two Pipelines, One Symptom — When Parallel Code Paths Drift</h2> <p class="post-card__meta"> Solution Postmortem </p><p class="post-card__desc">GopherTrunk grew parallel implementations of the same contract — daemon vs replay, single-channel vs wideband, C4FM vs CQPSK — and five issues trace to one path getting a fix, a warning, or a config knob the other never received. The series finale, with the rules that keep twins honest.</p></li></ol><p class="blog-feed-link"> See all solution postmortems or subscribe via RSS. </p>