Part 1 of Signal Lab, a 10-part series on GopherTrunk’s offline signal-analysis workbench. Signal Lab is one leg of the Lab Bench trilogy — three sister series (Signal Lab, RF Scope, and Crypto Lab) that follow the same recorded captures from raw IQ to decoded meaning to broken cipher. A single mystery signal, Mercury, threads through all thirty posts; you’ll meet it soon.
TL;DR:
gophertrunk siglabis an offline workbench that replays a recorded IQ capture through the same decode pipeline the live daemon uses — so what you see in the lab is what you’d get on the air. No SDR, no daemon, no network. It ships in the default install (no build tag) and has three front-ends: a terminal app, a browser console (siglab serve), and a demod benchmark (siglab sweep). Point it at a.cfile, pick a protocol, read the dashboard.
Key takeaways
- Signal Lab is an analysis workbench, not a scanner. It works on recorded IQ files, so you can diagnose a marginal capture on a laptop on a plane.
- Same pipeline as the daemon. Every metric comes from the production decoder, so lab results transfer directly to live behavior.
- Three front-ends, one binary —
siglab,siglab serve,siglab sweep— chosen by how much you want to see versus script. - Format matters. GopherTrunk’s own
capturewrites interleavedf32;rtl_sdr-style.cu8/.cfilefiles areu8. Set-formatto match or the bytes are garbage.
Cheat sheet
| Command / flag | What it does |
|---|---|
gophertrunk siglab -in capture.cfile -sample-rate 2400000 |
Replay a capture in the terminal app |
-protocol p25p1 |
Skip the picker and decode a named protocol |
-format u8 \| f32 |
Sample format of the capture (default f32) |
-auto-tune |
Estimate carrier offset and tune to 0 Hz before demod |
gophertrunk siglab serve |
Launch the browser console at 127.0.0.1:8099 |
gophertrunk siglab sweep |
Benchmark demod quality across an SNR ladder |
In this post
- What Signal Lab actually is — an offline workbench sharing the daemon’s decode path.
- The three front-ends and when to reach for each.
- Your first replay — the one command, the protocol picker, the dashboard.
- IQ formats — why
f32versusu8is the mistake everyone makes once. - The cast and the trilogy — Ada, Reese, and the Mercury thread.
What Signal Lab is
Signal Lab is GopherTrunk’s standalone signal-analysis workbench. It runs entirely offline against recorded IQ captures — no radio attached, no daemon running, no network — which makes it the natural home for three jobs: diagnosing a marginal capture that won’t lock, proving out a demodulator change, and building regression fixtures you can check into a repo.
The single most important thing to understand is this: everything Signal Lab shows comes from the same decode pipeline the live daemon uses. It is not a re-implementation, not a simplified teaching model, not a separate “analysis” decoder that drifts out of sync with production. When the lab says a P25 control channel locked at 19.7 dB SNR with 7.4% EVM, that is the production receiver’s own verdict on those exact samples. So the lab is a microscope on production behavior, and the numbers you learn to read here are the numbers that decide whether the daemon hears a system on the air.
That equivalence is the whole reason the workbench is useful. A recorded capture is deterministic — the same bytes, every run — so you can replay a failure a hundred times, change one knob, and see exactly what moved. On a live radio you can never step in the same river twice.
The three front-ends
Signal Lab is one binary with three faces. They share the engine; they differ in how much they let you see and how easily you can script them.
| Surface | Command | Best for |
|---|---|---|
| Terminal app (TUI) | gophertrunk siglab |
quick one-capture replay + dashboard in a shell |
| Browser console | gophertrunk siglab serve |
rich visuals, synthesis, comparing captures |
| Demod benchmark | gophertrunk siglab sweep |
measuring demod quality across an SNR ladder |
The terminal app is where you start: one capture, one command, a live event
log, and a signal-quality dashboard. The browser console (Part 3) is the
richest surface — constellations, eye diagrams, spectrograms, a synthesis
engine, and side-by-side capture comparison — served locally at
http://127.0.0.1:8099/. The demod benchmark (Part 10) is the lab-coat
instrument: it synthesizes P25 across an SNR ladder on both demod paths and
prints measured quality against theory, so you can answer “did my change
actually help?” with a number.
None of them needs an opt-in build tag; Signal Lab is part of the default install. On Windows the installer even drops a Signal Lab console shortcut in the Start Menu next to the standard consoles.
Your first replay
Here is the entire getting-started experience. Point the terminal app at a capture and give it the sample rate the file was recorded at:
gophertrunk siglab -in capture.cfile -sample-rate 2400000
If you don’t pass -protocol, the TUI opens on a protocol picker — the list
of every protocol the decoder knows, driven by the same registry the daemon
uses. Move with ↑/↓ (or k/j), press enter, and the engine runs. You’ll
watch decode events stream past — NAC, color code, grants — and then land on the
dashboard, the signal-quality readout we take apart field by field in Part 2.
Prefer to skip the picker? Name the protocol and the format up front:
gophertrunk siglab -in capture.cfile -protocol p25p1 -format u8 -auto-tune
-auto-tune estimates the capture’s carrier offset and tunes it to 0 Hz before
demodulation — handy when you recorded a little off-center. Under the hood, the
TUI is a small self-contained program that launches the engine in a goroutine
and pumps its events into the dashboard:
// cmd/gophertrunk/siglab_tui.go
res, err := siglab.RunStream(capture, cfg, func(ev siglab.EventRecord) {
select {
case eventCh <- ev:
default: // drop on a slow UI rather than block the engine
}
})
That RunStream call is the same entry point the daemon’s replay path leans on —
which is exactly why the lab and the air agree.
IQ formats: f32 versus u8
The one mistake nearly everyone makes once is feeding the lab a file in the wrong sample format. An IQ capture is just interleaved I/Q samples on disk, but the width and encoding of each sample varies by tool:
f32— interleaved 32-bit floats, two per sample (I then Q). This is what GopherTrunk’s owngophertrunk capturewrites, and it’s the lab’s default.u8— interleaved unsigned 8-bit bytes, the classicrtl_sdrformat. Files named.cu8(and many.cfiles from the RTL ecosystem) areu8.
The extension is a hint, not a contract. If you set -format wrong, the decoder
reads the bytes at the wrong stride and sees noise — no lock, garbage
histogram, nothing to diagnose. When a capture that should decode produces
pure noise, check the format before anything else. For the deeper story on what
IQ samples are, see the reference on
IQ data and
software-defined radio.
The second most common surprise is the sample rate. The lab defaults to
-sample-rate 2400000, but if the file was recorded at, say, 2.5 MS/s and you
leave the default, the effective baud will read a few percent off and the
decoder may never settle. Part 2 turns that deviation into a diagnosis you can
trust.
Meet the cast
Two people will walk through this series with you.
Ada’s first replay locks on the third try — the first two were the wrong
-format, then the wrong -sample-rate. That’s the normal path, and it’s why we
spend Part 2 on reading the dashboard before anything fancier.
The Mercury thread
Somewhere in Ada’s shoebox is a capture near 453 MHz — UHF business band — that doesn’t behave. Short, intermittent bursts on a roughly 12.5 kHz channel, here and gone, and when she replays it nothing named decodes. Reese calls it Mercury, half joking, and the name sticks.
Mercury is the thread that ties the whole Lab Bench trilogy together. In this series it stays stubbornly unnamed until Part 8, where blind signal-ID gives a best guess but no lock and Ada hands the wideband capture off to RF Scope. RF Scope triages it as an unknown, apparently-encrypted emitter and passes its frames to Crypto Lab, where the twist finally lands. Keep it in the back of your mind; we’ll pick it up piece by piece.
The series map
| Part | Topic | What you’ll do |
|---|---|---|
| 1 | Your first capture (this post) | Replay a file with no radio |
| 2 | Reading the dashboard | Read lock, baud, EVM, SNR |
| 3 | The browser console | Drive siglab serve |
| 4 | Constellations & eye diagrams | See modulation quality |
| 5 | PSD, spectrogram, occupancy | Measure spectral shape |
| 6 | Synthesize references | Build ideal/impaired fixtures |
| 7 | VSA & EVM | Lab-grade modulation quality |
| 8 | Naming the unknown | Blind ID + wideband survey |
| 9 | Dissecting P25 PDUs | Field-level TSBK dissection |
| 10 | The demod bench | Sweep, regress, export |
Where this goes next
Part 2 takes the signal-quality dashboard apart field by field — protocol, symbols, effective baud versus expected, lock status and latency, the symbol histogram, IQ imbalance, decode-error rate, EVM, and the SNR estimate — and turns the “~2% baud deviation” rule into a reliable wrong-sample-rate diagnosis. If you’d rather see the whole workbench first, the SigLab docs page is the canonical reference for every flag. And when you’re ready to follow a capture past the decoder into segmentation and protocol hierarchy, that’s the sister series, RF Scope.
FAQ
Do I need an SDR to use Signal Lab? No. That’s the point. Signal Lab works entirely on recorded IQ files with no radio, no daemon, and no network. You only need hardware to make a capture (or grab one from a live daemon’s console); analyzing it is fully offline.
Is the lab decoder the same as the live one? Yes. Everything Signal Lab reports comes from the same production decode pipeline the daemon runs. The lab just feeds it from a file instead of an antenna, so lab results transfer directly to on-air behavior.
What protocols can it decode?
Every protocol the engine knows — P25 Phase 1/2, DMR, NXDN, dPMR, YSF, TETRA,
EDACS, MPT-1327, D-STAR, and LTR. Pass the name to -protocol or let the picker
rank candidates for you.
Why did my capture decode to noise?
Almost always the wrong -format (an f32 default against a u8/.cu8 file)
or the wrong -sample-rate. Fix the format first, then the rate, then re-run.
Series navigation
Part 1 of 10 · Next → Part 2: Reading the Dashboard