Part 3 of Signal Lab, a 10-part series on GopherTrunk’s offline signal-analysis workbench. The terminal app is fast; the browser console is where you actually *see a signal.*
TL;DR:
gophertrunk siglab servestarts a fully offline web console athttp://127.0.0.1:8099/. It bundles the entire RF-analysis UI into the binary — no Node.js at runtime, no CDN — and organizes work into five tabs: Captures, Synthesize, Identify, Wideband, and Compare. The core loop is upload → configure the engine → run with a live SSE event stream → read the dashboard. Live capture from an SDR only exists when the console is served by a daemon with a radio; the standalone server is purely offline analysis.
Key takeaways
- One command, a local web app.
siglab servebinds127.0.0.1:8099and serves the whole console from the binary — nothing fetched from the internet. - Five tabs cover the workbench: Captures, Synthesize, Identify, Wideband, Compare.
- The loop is upload → configure → run → dashboard, with decode events streaming live over Server-Sent Events.
-open,-addr,-tmp-dir,-max-uploadshape where it listens and how big an upload it accepts.- Live SDR capture is daemon-only. The standalone server analyzes files; it doesn’t touch hardware.
Cheat sheet
| Command / flag | What it does |
|---|---|
gophertrunk siglab serve |
Serve the console on 127.0.0.1:8099 |
-open |
Open it in the system browser once it’s up |
-addr host:port |
Change the listen address (e.g. 0.0.0.0:9000) |
-tmp-dir <dir> |
Directory for staged capture uploads |
-max-upload <bytes> |
Max upload size (0 = default 512 MiB) |
-verbose-errors |
Full error chain + stack on failure |
In this post
- Starting the console — the one command and its flags.
- The five tabs — what each surface is for.
- The core loop — upload, configure, run, read.
- The live event stream — what SSE gives you that a batch run doesn’t.
- Where live capture fits — and why the standalone server stays offline.
Starting the console
The whole thing is one subcommand:
gophertrunk siglab serve # serve on 127.0.0.1:8099
gophertrunk siglab serve -open # ...and open it in your browser
gophertrunk siglab serve -addr 0.0.0.0:9000
It prints its address to stderr and waits:
siglab serve: listening on http://127.0.0.1:8099/
The server is deliberately minimal — it runs the API server with only the Signal Lab subsystem wired in, no SDR, no daemon, no storage:
// cmd/gophertrunk/siglab_serve.go
opts := api.ServerOptions{
Addr: *addr,
Siglab: api.SiglabOptions{
Enabled: true,
TempDir: *tmpDir,
MaxUploadBytes: *maxUpload,
},
}
Everything the browser needs — the SPA, the fonts, the analysis code — is embedded in the binary, so the console works on an air-gapped machine. On Windows the installer drops a Signal Lab console shortcut in the Start Menu that runs exactly this command, so operators who never touch a shell still get the full workbench.
The flags are few and practical. -addr moves the listener (bind 0.0.0.0 to
reach it from another machine — mind that it’s unauthenticated, so keep it on
trusted networks). -open launches your browser once the server is up.
-tmp-dir chooses where uploaded captures are staged (default: a fresh temp
dir). -max-upload caps upload size in bytes, where 0 means the default of
512 MiB — raise it if you routinely analyze multi-gigabyte wideband files.
The five tabs
The console organizes the workbench into five tabs, each a front-end onto part of the engine you’ve already met or will meet soon:
| Tab | What it’s for | Covered in |
|---|---|---|
| Captures | Upload a file, configure the engine, run a decode, read the dashboard | this post |
| Synthesize | Generate an ideal capture and inject impairments | Part 6 |
| Identify | Rank protocol candidates for an unknown capture | Part 8 |
| Wideband | Survey a wide capture for carriers and name them | Part 8 |
| Compare | Overlay spectra and diff metrics across captures | Parts 6 & 10 |
The core loop
A typical session on the Captures tab is four steps:
- Upload a capture (or hop to Synthesize and generate one). The file is
staged into the temp directory
-tmp-dirpoints at, subject to the-max-uploadlimit. - Configure the engine. Choose the protocol, set the sample rate, and toggle
the same knobs the CLI exposes plus a few deeper ones: tune, auto-tune,
conjugate (mirror I/Q), IQ-correct, and the P25 deep knobs. These map onto the
same engine
Configthe TUI builds — the console is a richer front panel on identical machinery. - Run. The decode runs and streams events back live (next section).
- Read the dashboard, then optionally Compare against another capture or
a synthesized ideal, and Export the structured result. The console offers
the engine’s JSON / JSONL / YAML / CSV serializers — the same output the
terminal app writes when you press
e.
Because it’s the same engine, the 2% baud-deviation rule from Part 2 applies here verbatim: if the run reports a large baud deviation, fix the sample rate in the config panel and re-run before trusting anything else.
The live event stream
The console’s signature feature is that a run isn’t a black box that returns an answer — it narrates. Decode events stream to the browser over Server-Sent Events (SSE) as they happen: the moment of lock with its residual frequency, each grant, stage transitions, errors. For a five-second capture that’s over in a blink, but for a long recording it’s the difference between staring at a spinner and watching a control channel wake up, grant a call, and settle.
The same event records feed the terminal app’s live log; the console just renders them in a scrolling panel next to the results. When something goes wrong mid-decode, you see where in the capture it happened rather than only a final “not locked” — which is often the clue that turns a mysterious failure into an obvious one (a burst of errors right at a known interference spike, say).
Where live capture fits
One boundary matters. The standalone siglab serve is purely offline: it
analyzes uploaded and synthesized files and never touches hardware. The
live-capture surface — record a fixed-length raw-IQ file from a tuner, then
analyze it immediately and download the .cfile — only exists when the console
is served by a running daemon with an SDR.
That’s not an accident of packaging; it’s the architecture. The daemon mounts the
identical /api/v1/siglab/* routes the standalone server does:
The running daemon mounts the same
/api/v1/siglab/*routes, so if you already have a daemon up you can reach the console (and the live-capture surface) there instead of starting a separate server.
So there are two ways to reach the same console. Start siglab serve for
offline, file-only analysis on any machine; or open the console on a daemon
that’s attached to a radio to also get live capture. Either way the analysis
half — every tab in this post — behaves identically, because it’s the same
bundled UI over the same engine.
Where this goes next
Now that you can drive the console, the next four parts fill its visualization tabs with meaning. Part 4 opens the constellation and eye diagram — the plots that show modulation quality directly — so the EVM and SNR numbers from Part 2 get a picture to go with them. The SigLab docs page lists every serve flag if you need the reference.
FAQ
Does the console need internet or Node.js? No. The entire SPA and its analysis code are embedded in the binary. It runs on an air-gapped machine; nothing is fetched from a CDN at runtime.
Can I capture live from an SDR in the console?
Only when the console is served by a running daemon that has an SDR attached. The
standalone siglab serve is offline analysis of uploaded and synthesized files
only.
My capture upload was rejected as too large. What do I raise?
-max-upload, in bytes. The default is 512 MiB (0 selects that default). For
multi-gigabyte wideband files, pass a larger value.
How do I reach the console from another machine?
Bind a non-loopback address with -addr 0.0.0.0:9000. It’s unauthenticated, so
only do this on a trusted network.
Series navigation
Part 3 of 10 · ←Part 2 · Next → Part 4: Constellations & Eye Diagrams