Receivers 11/11
P25 Locked
Streams 3/3 live
Calls/hr
Uptime
Live
Build log · KI4GIP

The Beast

An ex-datacenter Xeon, eleven thirty-dollar SDR dongles, and a small pile of bash scripts. The honest build log behind listennowhou.com — what worked, what didn't, and what I tried first.

This started in early 2025 with a Uniden SDS200 and an honest question: could I monitor Houston-area public safety better than what already existed? Eleven months and four hardware iterations later, the answer is "kind of, depending on what 'better' means." The system runs 24/7, captures around 75,000 calls per day, and quietly survives most things that go wrong with it. Here's how it got there.


The four hardware experiments

Before "the Beast" was a server, it was a series of failed setups. Each one taught me something specific.

1 · Uniden SDS200

The SDS200 is the gold-standard consumer scanner — and the audio is genuinely beautiful. Crisp, full bandwidth, the kind of sound you wish every Broadcastify feed had. But it scans like a scanner: one channel at a time, sequentially. On a busy P25 trunked system with dozens of active talkgroups, sequential scanning means missed calls. By the time you've cycled back to HPD-3 North Patrol, two units already keyed up and nobody captured them. It sounded great and missed half the action.

2 · Raspberry Pi 5 + OP25

Next attempt: a Pi 5 running OP25, the venerable open-source P25 decoder. Software-defined radio means you can decode multiple channels in parallel — exactly the problem the Uniden couldn't solve. OP25 is well-regarded and the audio quality is solid. But the Pi 5's CPU couldn't keep up with simultaneous multi-channel decoding on a busy trunked system. Calls dropped. The decoder fell behind during dispatch surges and stayed behind. Right architecture, wrong hardware.

3 · SDRTrunk on a video-editing PC

I borrowed CPU horsepower from the desktop I use for video editing and ran SDRTrunk on it. SDRTrunk is the popular Java-based alternative to OP25 — easier UI, active community, great for casual scanning. It kept up with the trunked system. But the audio wasn't quite as clean as OP25, and I needed my video PC back. It worked, but it wasn't sustainable.

4 · A dedicated server with trunk-recorder

The fourth try is the one that stuck: a former-datacenter Xeon, eleven RTL-SDR dongles, one AirSpy, and trunk-recorder. Trunk-recorder takes a different philosophical approach than the others: record everything in parallel, sort it out downstream. Instead of "tune to channel X, decode it," it dedicates SDRs to the control channel and voice channels, captures every call as it happens, and writes each one to disk as a separate file. Filtering, routing, and playback all happen after the audio is safely on disk. Nothing gets missed because nothing is being skipped.

That paradigm shift — capture-everything-then-filter, not tune-and-listen — is the single most important thing I learned across all four attempts. Once I made that mental switch, everything else fell into place.


The hardware

Most homelab writeups feature glossy rack photos. This isn't going to. The Beast lives in an open-frame case behind a couch in my house. Cable management is "embraced entropy." The reason it's open-frame is that fitting eleven USB SDR dongles into a closed case is its own kind of optimization problem and I didn't want to solve it. The cooling is better this way anyway.

CPU
Intel Xeon E5-2699 v4 — 22 cores / 44 threads, 2.2 GHz base / 3.6 GHz turbo. Bought used; runs at ~55% of rated clock under current load.
RAM
64 GB DDR4 ECC. Most of it is unused; trunk-recorder is CPU-bound, not memory-bound.
SDRs
10 × NooElec NESDR SMArt v5 + 1 × original Realtek RTL2838 + 1 × AirSpy. The AirSpy covers the 800 MHz voice plane; the RTLs handle control + the rest of the voice channels.
Storage
~900 GB NVMe. Audio archive holds 7 days at ~70k calls/day = ~16 GB live. Plenty of headroom.
OS
DragonOS — Ubuntu-based, pre-loaded with SDR tooling. Saved me a week of dependency hell.
Network
Comcast/Xfinity residential. Tailscale for remote admin, Cloudflare Tunnel for the public scanner UI. No port forwarding, no public services on residential IP.
Total cost
Roughly $400 in hardware. The CPU and motherboard combo was about $90 used. SDRs were $30 each. Most expensive part was the AirSpy.

Yes, an E5-2699 v4 is overkill. Yes, I know. The original plan was an i7 in the same socket family — the Xeon came up cheap and I figured the headroom would be useful. It is.


The software

The audio pipeline runs in stages. Each stage does one job. The whole thing is held together by systemd units and a watchdog process that knows what "healthy" looks like.

Capture

trunk-recorder reads the TXWARN P25 Phase II control channel from one dedicated SDR and follows voice grants to the remaining receivers. Every captured transmission lands on disk as an .m4a file, named with its talkgroup ID and timestamp. rdio-scanner ingests these files into a SQLite database and serves them through a web UI for searchable playback.

DSP cleanup

Raw trunk-recorder output sounds like raw trunk-recorder output: variable levels, hiss, low-frequency rumble, dead air on either end of each call. Most Broadcastify feeds stream that as-is. This one doesn't. An inotify watcher fires sox on every new file with a five-stage chain: high-pass at 300 Hz to cut hum, low-pass at 3 kHz to cut hiss, peak normalization to −3 dB so loud and quiet calls land at the same volume, silence trimming on both ends, and a small pad at the end so back-to-back calls don't feel mashed together.

Filter

Three Broadcastify feeds run from this single server: HPD + Constables, Fort Bend, and Harris County Sheriff. Each feed gets a per-talkgroup whitelist. When ezstream asks for the next file to play, a script checks the talkgroup ID prefix against the whitelist and skips anything that doesn't match. Encrypted talkgroups are filtered out earlier, at the trunk-recorder layer — they have no decodable payload, so they never become files.

Label

The slick part: when ezstream is about to play a file, the script looks up the talkgroup ID in radioreference.csv and creates a symlink with a human-readable name. Listeners on the Broadcastify mobile app see HPD-3 NORTH PATROL scroll across their screen instead of 563-1778091538. It's a small detail. It makes the feed feel professional.

Stream

ezstream serves newest-first from the whitelisted pool, keeping listeners current with active dispatch instead of replaying old calls. When there's nothing whitelisted to play, a sox-generated silence file fills the gap so the Broadcastify mount never times out. The connection is continuous; the audio only plays when there's something worth playing.

Watch

The watchdog is the part of the system I'm most proud of. Every two minutes it checks: are all SDRs detected? Is the AirSpy detected? Is trunk-recorder running? Is rdio-scanner serving? Are all three ezstream services connected? Is fresh audio being captured? And critically: is each feed actually streaming whitelisted content, or is it silently feeding silence while its source dir has busy traffic on other talkgroups?

That last check is the one most monitoring scripts miss. A feed can be running, the service can be "active," and the listener can be hearing dead air — because the talkgroups it's filtering for happen to be quiet while everything else is busy. I had to add this check the hard way; more on that below.


Lessons learned

01

Capture-everything beats tune-and-listen

The Uniden, OP25, and SDRTrunk were all variations on "tune to a channel, decode it, move on." That model is fundamentally lossy on a busy trunked system. trunk-recorder's "record every call as a file, sort downstream" architecture is the right paradigm. It cost me three hardware experiments to learn that.

→ Move filtering as far downstream as you can.

02

Used enterprise hardware is the cheat code

The CPU and motherboard cost less than a single new SDR. A nine-year-old Xeon E5-2699 v4 has more cores than most current consumer chips and idles at half clock under steady-state load. Buying retail parts for a homelab in 2026 is a tax you don't have to pay.

→ eBay searches: "xeon e5-2680 v4 motherboard combo" and equivalents.

03

"Service is active" doesn't mean "service is working"

Early versions of the watchdog only checked systemctl is-active. Then a feed went silent during a real bomb-threat callout at a Houston-area high school — the service was running, ezstream was connected, listeners were hearing nothing because the script was filtering out every busy talkgroup that didn't match its whitelist. Coverage stayed live thanks to redundancy on other feeds, but it was a wake-up call. The watchdog now cross-references whitelist activity against feed output. If the source dir is busy and a feed is quiet on talkgroups it should be playing, it gets restarted.

→ Health checks must validate output, not just process state.

04

One missing dongle isn't a crisis

Eleven RTL-SDR dongles is a lot of USB. Statistically, one of them is going to misbehave at any given moment — flaky cables, USB power dips, thermal drift. Earlier watchdog versions would Pushover-alert me at 3am because dongle #7 had dropped, and an hour later it was back. Now: ten of eleven counts as "degraded but acceptable" with one daily summary alert. Two missing triggers escalation. Restraint took me longer to learn than escalation.

→ Build redundancy in, then trust it.

05

DragonOS saved me a week

SDR software is a dependency-graph nightmare. GNU Radio versions, OOT modules, boost, OSMocom drivers, kernel-level USB tweaks — a fresh Ubuntu install takes days to get right. DragonOS ships with all of it pre-configured. Picking the right base image is sometimes the highest-leverage decision in the whole build.

→ Don't fight environment problems you don't have to.

06

Talkgroup IDs in filenames is the unsung hero

Trunk-recorder writes each call with the talkgroup ID embedded in the filename. That single design choice means every downstream filter is a string match against the prefix — no audio decode, no metadata lookup, no database join. The whitelist filter, the per-feed router, the cleanup scripts, the wedge detector: all of them are basically grep. Cheap operations stay cheap when the upstream emits the right metadata in the right place.

→ Embed metadata in names, not just content.


Live, right now

The numbers below pull from the system in real time. They update every minute.

RECEIVERS CALLS / 24H CALLS / HR UPTIME
If the receiver count says 10/11, that's normal — one of the dongles drops out periodically. The system runs fine on ten and the watchdog accepts it without complaint.

What's next

The build is never really "done." A few things on the working list:

If you build something similar and want to compare notes, the contact form is on the main page. I'm @doorman812 on X. I read everything.