UPDATE: POCSAG on a Cheap Yellow Display
POCSAG is still alive. Your county EMS is still broadcasting dispatches in plaintext over frequencies anyone can receive. I built a scanner that listens to all of it.
The display still works. It still shows pager traffic in green on black. That part is unchanged.
Everything behind it is different.
If you missed the original build, it is here.
What Changed
The original setup was a single container: rtl_fm piped into multimon-ng, Python parsing the output, MQTT to the display. One mode, one frequency at a time, no persistence, no remote access.
server/radio/ replaces all of that. It is a Python scheduler that manages one RTL-SDR across a list of tasks. Each task has a mode, a frequency list, a signal threshold, and a priority. The loop scans in priority order, measures RMS audio level on each frequency, and locks onto anything above threshold. When it locks, a mode-specific worker takes over.
Workers:
- Voice: records audio until silence, sends it to a local Whisper instance for transcription
- POCSAG: pipes audio into multimon-ng and parses pager traffic
- APRS: same, different decoder mode
- CW: multimon-ng MORSE_CW. Tested against synthetic tones, untested on real over-the-air signals.
- SSTV: slow-scan TV, untested
Every decoded event goes to MQTT and to a remote web feed simultaneously. The CYD gets what it always got. Everything else goes to the feed.
Voice Transcription
When the scanner locks on a voice frequency, the voice worker records until silence, then sends the clip to a containerized Whisper instance.
whisper-stt:
environment:
- WHISPER_MODEL=tiny.en
- WHISPER_DEVICE=cpu
- WHISPER_COMPUTE=int8
tiny.en with int8 quantization transcribes a 15-second clip in about four seconds on a modest x86 box. The first transcription I got back from NOAA weather radio was And with locally heavy depth. The sentence made no sense. The antenna is a 6m dipole receiving 162 MHz, which it has no business doing. The pipeline worked, which was the point.
The Web Feed
RadioFeed is a separate server that receives events over a persistent WebSocket connection and stores them in SQLite. It serves a dark-theme web UI with live updates pushed to any connected browser.

Events show mode, frequency, timestamp, and message. Tap any row to expand it and see the full text plus metadata. Filter by mode. The feed is public. No login required to read it. Admin login is required to send control commands back to the scanner.
The scanner authenticates to the feed with a shared secret. Sessions use HMAC-SHA256 tokens. RadioFeed is its own container, deployable anywhere.
Signal Thresholds
The scanner locks onto any frequency with RMS audio level above a configured threshold. I got this wrong at first. Spent all my time locked onto noise.
POCSAG frequencies in my area read RMS 2000 to 5000 on noise alone. A threshold of 1200 locks onto every one. The scan loop waits 30 seconds for traffic that never comes, releases, locks again immediately. Everything else starves.
The repeater at 145.450 MHz has the same problem in the opposite direction. The idle carrier reads around 2500 RMS. A threshold of 800 locks on every carrier key, burns a Whisper cycle, gets nothing back. The working threshold is 3500. Above the carrier floor, below actual voice.
There is no universal number. Measure first, then commit.
The USB Problem
The RTL-SDR goes into a bad state if rtl_fm is killed mid-operation with SIGKILL, which is what Docker sends when it stops a container. The device claims successfully on the next start but streams zero bytes. It looks like it is working. It isn't.
The only fix is a physical USB unplug and replug. Software cannot recover it.
The container entrypoint runs rmmod dvb_usb_rtl28xxu rtl2832 rtl2830 and sleeps three seconds before starting. This prevents most cases. Kill the container hard mid-sample and you are back to unplugging.
I know this from experience. More than once.
Code
server/radio/ is the scheduler. RadioFeed/ is the feed server. Both have .env.example files.