I wanted to play with an software-defined radio for a while now. A simple one can be had for about $10 by repurposing a USB TV adapter: http://sdr.osmocom.org/trac/wiki/rtl-sdr. I bought one, and looked into using it as a police scanner. Suprisingly to me, there's quite a bit of information out there about the protocols and frequencies used by LAPD: http://harrymarnell.net/lapd-freqs.htm. So they're using P25-encoded digital signals, which are unencrypted, apparently.
There are some guides out there on how to decode these with an RTL-SDR, but they're all highly Windows-centric (and look like a pain in the butt, to be honest). This post is a set of notes on getting this working on a Debian box.
Obtaining the tools
The core RTL-SDR libraries, GNU Radio and UI tools such as GQRX are already in
Debian, so getting them is trivial. There is a tool for decoding P25, dsd
;
it's not in Debian, so we have to build it. First we get and build mbelib
, a
library it uses. We check out the code, roll bakc to the latest tag and build:
dima@shorty:/tmp$ git clone https://github.com/szechyjs/mbelib ... dima@shorty:/tmp$ cd mbelib dima@shorty:/tmp/mbelib$ git tag -l v1.2.1 v1.2.3 v1.2.4 v1.2.5 dima@shorty:/tmp/mbelib$ git reset --hard v1.2.5 HEAD is now at 316bab6 Bump version to v1.2.5 dima@shorty:/tmp/mbelib$ mkdir build dima@shorty:/tmp/mbelib$ cd build dima@shorty:/tmp/mbelib/build$ cmake .. ... dima@shorty:/tmp/mbelib/build$ make ... Linking C static library libmbe.a
OK. We built mbelib
, now we can build dsd
. Same as before, except we tweak
the Makefile
to find and use the library we just built, and to use the
statically-linked version so that we don't need to mess with RPATHs.
dima@shorty:/tmp$ git clone https://github.com/szechyjs/dsd ... dima@shorty:/tmp$ cd dsd dima@shorty:/tmp/dsd$ git tag -l v1.3 v1.4.1 v1.6.0 dima@shorty:/tmp/dsd$ git reset --hard v1.6.0 HEAD is now at 5d147c9 version 1.6.0 dima@shorty:/tmp/dsd$ perl -p -i -e 's{/usr/local/include}{/tmp/mbelib/}g; s{-lmbe}{/tmp/mbelib/build/libmbe.a}' Makefile dima@shorty:/tmp/dsd$ make ... gcc -O2 -Wall -o dsd dsd_main.o dsd_symbol.o dsd_dibit.o dsd_frame_sync.o dsd_file.o dsd_audio.o dsd_serial.o dsd_frame.o dsd_mbe.o dsd_upsample.o p25p1_hdu.o p25p1_ldu1.o p25p1_ldu2.o p25p1_tdulc.o p25_lcw.o x2tdma_voice.o x2tdma_data.o dstar.o nxdn_voice.o nxdn_data.o dmr_voice.o dmr_data.o provoice.o -L/usr/local/lib -lm /tmp/mbelib/build/libmbe.a
Decoding the stream
Now we can think about listening in. The overall data flow is
- Tune in, demodulate the narrow-band FM signal into a 48KHz-sampled signal
- Use
dsd
to decode this 48KHz-sampled signal to produce 8KHz-sampled audio
FM
There are several basic tools one can use for this. I'd prefer to use the
commandline rtl_sdr
or rtl_fm
from the rtl-sdr Debian package. The issue I
ran into was that we're tuning into a relatively narrow-band signal, so the
tuning is sensitive, and small tuning errors make you miss the signal you want
entirely. RTL-SDR is a cheap device, and its tuning inaccuracy alone is enough
to break this. There exists an RTL-SDR calibration tool to compensate for the
hardware inaccuracy, but I still wasn't able to successfully tune into the
frequencies, as defined in the LAPD channel list linked above. I didn't push on
this very hard, so this could very well be my fault.
So instead of the commandline tools, I ended up GQRX. Pretty much all the LAPD
frequencies are in the 484MHz range or the 506MHz range. I set the tuner into
the right neighborhood, then the FFT waterfall plot in GQRX visually shows you
which frequencies are active. You can roughly tune in simply by looking at the
plot, and you can fine-tune by listening to the demodulated signal, trying to
find the characteristic digital buzz and no static. There are multiple
digital-sounding channels and multiple types of encoding are present (sound
different). You can play around to find a signal that dsd
knows how to decode.
Note that since we're now looking for channels empirically, we compensate for
tuning inaccuracies, but the LAPD frequency list becomes useless, and we don't
even know what specifically we're listening to.
The GQRX window looks like this:
We're clearly listening to an active transmission: we're tuned to the channel indicated by the red line, and the waterfall plot shows intermittent activity there. The signal is intermittent because the transmitter is only active when there's data to send, i.e. when the human talking into the radio is pressing the button.
P25
We now need to get the data out of GQRX and into dsd
. dsd
wants to get its
input from (and send its output to) /dev/audio
. Even if my input was coming
from a sound device, it wouldn't be /dev/audio
on my box. That's a holdover
from some ancient system that ALSA doesn't provide by default, and I want to
avoid it if possible. Turns out dsd
just looks at raw samples, so we can
simply send it appropriately-formatted bits (16 bits per sample, little endian,
48KHz sample rate). GQRX has several export capabilities, one of them being raw
UDP output. This is perfect for this application, and I turn on that GQRX mode
by pressing the appropriate button (bottom of the screenshot; two computers are
pictured).
We now have raw 48KHz samples coming out on UDP port 7355. We can make a named
pipe, or better yet, we can pass the data to dsd
on standard input:
socat UDP-RECV:7355 - | ./dsd -i /dev/stdin
Almost done. We can now tune interactively with GQRX and decode the demodulated
FM data on the fly with dsd
. dsd
says lots of stuff about signals it's
receiving. When successfully decoding audio, I see things like this:
Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 180359 tg: 1 LDU1 e:======================== Sync: +P25p1 mod: C4FM inlvl: 7% nac: 467 src: 180359 tg: 1 LDU2 e:========= Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 180359 tg: 1 LDU1 e:===== Sync: +P25p1 mod: GFSK inlvl: 7% nac: 466 src: 180359 tg: 1 LDU2 e:======R================R========= Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 180359 tg: 1 LDU1 e:================== Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 180359 tg: 1 LDU2 e:=== Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 180359 tg: 1 LDU1 e:===================== Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 1228951 tg: 1 LDU2 e:======== Sync: +P25p1 mod: C4FM inlvl: 7% nac: 466 src: 1228951 tg: 1 LDU1 e:==== Sync: +P25p1 mod: GFSK inlvl: 7% nac: 466 src: 180359 tg: 1 LDU2 e:==================
We still can't hear the results because dsd
doesn't write them anywhere
useful. We can send those to ALSA with a named pipe and aplay
:
# In one shell mkfifo /tmp/pipe socat UDP-RECV:7355 - | ./dsd -i /dev/stdin -o /tmp/pipe # In another shell, in parallel aplay -r 8000 -f S16_LE -t raw -c 1 < /tmp/pipe
So this setup works for me. Now one can go back, and fix stuff; stuff like inaccurate tuning. It'd be nice to automatically tune into valid channels, of better yet to follow the trunking signals, but that's more work than I'm willing to put into this.
Commandline-only listening (no GQRX)
Once we find a channel we like, using GQRX to interactively tune the radio (as described above), we can run the whole pipeline with one command (possibly zsh-only):
./dsd -f1 -mc -i <(rtl_fm -F1 -o4 -g32 -f 484.7918M -s48000 -) -o >(aplay -r 8000 -f S16_LE -t raw -c 1)
Here 484.7918Mhz is the frequency I found by poking around with GQRX. -F1 and
-o4
are tuning parameters (possibly highly suboptimal). The -f1 -mc
options
to dsd
indicate what signal should be expected; this would vary if listening
to something other than LAPD. Seems to work. And the total CPU comsumption is
about 1/3 of what gqrx requires.