Monsoon Ears

A scanner that notices when the washes come up.

A Raspberry Pi and a $40 radio dongle sit in a window and listen to Tucson fire and EMS traffic all day. Whisper transcribes it on the Pi itself, so no audio ever leaves the network. When it rains, the Pi checks what dispatchers are saying against backyard weather stations, stream gauges and NWS warnings, and says whether a flash flood is actually underway.

A Raspberry Pi 5 with its active cooler sits open on a windowsill beside the printed case lid and an RTL-SDR dongle. A dipole antenna on a small tripod stands behind it, with monsoon clouds over Tucson through the window.
The whole build: a Pi 5, an RTL-SDR, and a dipole on a camera tripod in the window.
~$155Total hardware, including the SDR
5 WAverage draw, running continuously
3Decode paths: analog FM, P25 trunked, APRS
0Audio that leaves the LAN

The demo

Fifty seconds through the dashboard

Recorded 29 September 2026, with a Flash Flood Warning up for Pima County. Threads, the raw feed, the map, the day's activity. No narration.

Three spots are blurred on purpose, not broken: two medical calls with street addresses in the hourly rollup, an EMS popup on the map, one injury call in the feed.

The problem

The washes come up fast

Most of the year Tucson's washes are just sand. In monsoon season they can go from dry to moving in a few minutes, fed by rain that fell miles off where you never saw it. The NWS warnings are good, but they cover whole counties. The radio usually knows about one specific low-water crossing well before anything else does.

Monsoon Ears listens to that traffic and checks it against real sensors. A dispatcher naming a wash, plus a gauge on that same wash climbing, is worth a lot more than either one alone.

How it works

Six stages, one dongle

One RTL-SDR can only hear one frequency at a time, and a Pi can only transcribe so fast. Most of the design is rationing those two things.

Scan and hold

The scanner probes each priority frequency for about a second, holds when a signal appears, and resumes after a hangover window. Probe audio is prepended to the first held chunk so short transmissions don't slip between cycles.

Squelch

A cheap RMS gate throws out the noise-only chunks before anything expensive touches them. That's most of the day gone for almost nothing.

Voice detection

WebRTC VAD finds where the speech actually starts and stops and trims the dead air around it. Feed Whisper silence and it invents words, so this matters more than it sounds.

Transcribe

Whisper runs on the Pi's own CPU. It reports per-segment confidence as it goes: no-speech probability, average log-probability, compression ratio. Those numbers catch most of the ghost text it produces on a quiet channel.

Classify and extract

A separate worker polls the database and runs a small agent graph: classify, extract structured fields against a local gazetteer, geocode. Keeping it separate means an API outage backs up the queue instead of taking capture down with it.

Correlate

Every fifteen minutes a bigger model reads the recent voice traffic, the APRS weather packets, the gauges and the live NWS warnings, and decides whether they add up to a flood.

"…Engine 4-1, respond, water rescue, vehicle in the wash at the 200 block…" type flood   severity high   units ['Engine 41']
wash — matched to gazetteer   road_closure true

Made up, to show the format. The real captures on this page are flood traffic with house numbers masked; see the footer.

All six run end to end in the walkthrough above, if you'd rather watch it than read about it.

The showcase

It shows its work

An NWS warning counts for more than a rising gauge, and a rising gauge counts for more than somebody's backyard rain total. Whatever the digest concludes, it lists the event ids and readings it leaned on. Most days it concludes nothing is happening, which took far longer to get right than the alarms did.

Real output, 29 September 2026, 11:23 MST: a Flash Flood Warning and a Flood Watch both up, Sabino Creek running, and the ids behind the call.

Perennial reaches are flagged. A few gauges sit downstream of wastewater plants and run steady water all year. Until I marked them, an ordinary Tuesday looked like a flood on every single run. That one doesn't turn up in testing.

The archive

Ask it in plain English

Months of extracted incidents pile up in a SQLite file. Type a question, get rows back. The generated query has to be one read-only SELECT over the allowed tables or it never runs.

"which washes came up most this week?" SELECT wash_name, count(*) FROM transcription_events
WHERE wash_name IS NOT NULL AND timestamp > date('now','-7 day')
GROUP BY wash_name ORDER BY 2 DESC

Also made up. It's the shape of the answer, not a real one.

The case

The assembled Pi in its black printed snap-fit case, vents visible over the cooler, with the RTL-SDR dongle plugged into a USB port and an antenna cable attached.

Snap-fit, no screws

It lives in a printed case built around the official Pi 5 active cooler: an intake vent for the fan, exhaust at the back, vents over the radiator. Two parts, no screws.

The design isn't mine. It's Raspberry Pi 5 Case (snap fit) by pyrho // 25.wf, free on Printables, and that's where to get it. The print settings below are the designer's.

SettingValue
MaterialPLA
Nozzle0.4 mm
Layer height0.2 mm
Orientationflat side down
Supportsnone
Parts2

Design by pyrho // 25.wf, licensed CC BY-SA. Multiple top variants with different port cutouts are included, so you can pick the one that matches your build.

It runs cool, which matters here. The designer measured 61.5 °C after ten minutes of all-core stress. Whisper pins the CPU every time a transmission comes in, all day long, and a throttled Pi just falls further behind the radio.

Build one

None of this is specific to Tucson

The capture path, the gating, the transcription and the agent graph don't care where they're pointed. USGS gauges, NWS alerts and APRS all work anywhere in the US and just want a latitude and longitude. What you bring is local knowledge: your frequencies, your gauges, your street names.

WhatWhereEffort
Frequencies and talkgroupsconfig/frequencies.pyAn evening on RadioReference
Location scalarsconfig/locale.pySix values, one file
Stream and rain gaugesconfig/gauges.pyA USGS site query
Local place namesconfig/gazetteer.pyAn hour with a map
A different hazardagents/alert.pyOne prompt, same structure

Wildfire lines up almost one-to-one. Red Flag Warnings do the job Flash Flood Warnings do here, and fuel moisture and wind stand in for stream discharge. The canyons are your washes.