├── 88-dvb-t.rules ├── Instructions.txt ├── LICENSE ├── README.md ├── blacklist-dvb-t.conf ├── run_jamdet.sh └── src ├── audio_tones.py ├── jammer_detect_main.py └── jammer_detect_no_ui.py /88-dvb-t.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2832", MODE:="0666" 2 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0bda", ATTRS{idProduct}=="2838", MODE:="0666" 3 | 4 | ACTION=="remove", SUBSYSTEM=="usb", ENV{ID_VENDOR_ID}=="0bda", ENV{ID_MODEL_ID}=="2838", RUN+="/sbin/poweroff" 5 | ACTION=="remove", SUBSYSTEM=="usb", ENV{ID_VENDOR_ID}=="0bda", ENV{ID_MODEL_ID}=="2832", RUN+="/sbin/poweroff" 6 | -------------------------------------------------------------------------------- /Instructions.txt: -------------------------------------------------------------------------------- 1 | Based on installing into the June 2017 version of Raspbian Jessie Lite. Your Raspberry Pi will need an Internet connection! 2 | 3 | 4 | 5 | Build and install PortAudio and its Python bindings: 6 | 7 | sudo apt-get install libasound-dev 8 | wget http://www.portaudio.com/archives/pa_stable_v190600_20161030.tgz 9 | tar -xvf pa_stable_v190600_20161030.tgz 10 | cd portaudio 11 | ./configure 12 | make && sudo make install && sudo ldconfig 13 | cd .. 14 | sudo apt install python3 python3-pip 15 | sudo pip3 install pyaudio 16 | 17 | Now download the SDR code: 18 | mkdir jamdet 19 | cd jamdet 20 | sudo apt install git automake shtool libtool libusb-1.0 21 | git clone https://github.com/mikeh69/librtlsdr 22 | git clone https://github.com/mikeh69/pyrtlsdr 23 | git clone https://github.com/mikeh69/JammerDetect 24 | 25 | Build and install the RTLSDR library: 26 | cd librtlsdr 27 | autoreconf -i 28 | ./configure 29 | sudo rm /usr/lib/librtlsdr.* (remove any previous version of librtlsdr) 30 | make && sudo make install (our custom version installs to /usr/local/lib) 31 | sudo ldconfig 32 | cd .. 33 | 34 | Now install the Python bindings for librtlsdr: 35 | cd pyrtlsdr (our customised version, to go with our customised librtlsdr) 36 | sudo pip3 install --upgrade wheel pip setuptools 37 | sudo python3 setup.py install 38 | sudo ldconfig 39 | cd .. 40 | 41 | Lastly, install a Udev rule and driver-blacklist that allows user-mode access to the DVB-T dongle: 42 | cd JammerDetect 43 | sudo cp 88-dvb-t.rules /etc/udev/rules.d/ 44 | sudo cp blacklist-dvb-t.conf /etc/modprobe.d/ 45 | sudo reboot 46 | 47 | Log in again (pi - raspberry). Set the audio output volume to 100%: 48 | sudo amixer cset numid=1 100% 49 | 50 | And finally the program is ready to run: 51 | cd jamdet/JammerDetect/src 52 | python3 jammer_detect_main.py 53 | 54 | (Press Ctrl – C to break out). 55 | 56 | 57 | To auto-run the program when the Pi powers up: 58 | 59 | sudo crontab -e, then Enter to select nano editor, 60 | Add a final line to the file: @reboot /usr/bin/python3 /home/pi/jammer_det/jammer_detect_no_ui.py 61 | 62 | The Udev rules file 88-dvb-t-rules also contains a setting to make the Raspberry Pi shut down cleanly if the DVB-T dongle is unplugged. 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mikeh69 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JammerDetect 2 | Detect jamming on ISM bands using an RTLSDR dongle and a Raspberry Pi or laptop PC. 3 | 4 | The primary motivation was to detect (and possibly track down!) car-keyfob jammers being used by criminals, but it could equally well 5 | be used to detect continuous transmissions in any band that the dongle can receive, including the 868MHz and 915MHz ISM bands. 6 | 7 | It uses a fork of Steve Markgraf's librtlsdr library, with a simple-as-possible function added to measure RF power in the receiver's bandwidth. 8 | (In order to run it on a battery-powered Raspberry Pi, we want to minimise the processing load). 9 | 10 | The main program is written in Python to make it easy to modify, so we also use the pyrtlsdr package (Python bindings to librtlsdr). 11 | 12 | Mike H. June 2017 13 | -------------------------------------------------------------------------------- /blacklist-dvb-t.conf: -------------------------------------------------------------------------------- 1 | blacklist dvb_usb_rtl28xxu 2 | -------------------------------------------------------------------------------- /run_jamdet.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /home/pi/jamdet/JammerDetect/src 3 | while : 4 | do 5 | python3 jammer_detect_no_ui.py 6 | done 7 | -------------------------------------------------------------------------------- /src/audio_tones.py: -------------------------------------------------------------------------------- 1 | import math #import needed modules 2 | import pyaudio #sudo apt-get install python-pyaudio 3 | import struct 4 | import pickle 5 | from time import sleep 6 | 7 | PyAudio = pyaudio.PyAudio #initialize pyaudio 8 | 9 | #See http://en.wikipedia.org/wiki/Bit_rate#Audio 10 | BITRATE = 48000 #number of frames per second - 44.1kHz does not work properly on RPi BCM2538! 11 | LENGTH = 0.2 #seconds to play sound 12 | CHUNKSIZE = int(BITRATE * LENGTH) 13 | WAVEDATA_FILE = "/home/pi/wavedata.pickled" 14 | 15 | class AudioTones: 16 | 17 | def init(self): 18 | 19 | self.player = PyAudio() 20 | defaultCapability = self.player.get_default_host_api_info() 21 | print("Player defaults:") 22 | print(defaultCapability) 23 | # fmt = self.player.get_format_from_width(2) 24 | # fmt = pyaudio.paInt8 # supposedly 8-bit signed-integer 25 | fmt = pyaudio.paInt16 # 16-bit signed-integer 26 | print(self.player.is_format_supported(output_format = fmt, output_channels = 1, 27 | rate = BITRATE, output_device = 3)) 28 | self.stream = self.player.open(format = fmt, channels = 1, rate = BITRATE, output = True, frames_per_buffer = CHUNKSIZE) 29 | 30 | try: 31 | print("Trying to load wavedata from file...") 32 | f = open(WAVEDATA_FILE, "rb") 33 | print(" File opened OK") 34 | self.WAVEDATA = pickle.load(f) 35 | print(" Wavedata read from file OK") 36 | f.close() 37 | return 38 | except Exception as ex: 39 | print(ex) 40 | print("Failed to load wavedata from file, re-generating") 41 | 42 | frequency = 200.0 # start frequency 200Hz 43 | self.WAVEDATA = [] 44 | 45 | for index in range(0, 46): # valid index range 0 - 45, ~10log(32768) 46 | num_fadein_frames = int(BITRATE * LENGTH * 0.05) 47 | num_loud_frames = int(BITRATE * LENGTH * 0.7) 48 | num_fadeout_frames = CHUNKSIZE - (num_loud_frames + num_fadein_frames) 49 | self.WAVEDATA.append(struct.pack( " 10: 54 | stdscr.addch(3, 3,'*') 55 | else: 56 | stdscr.addch(3, 3,' ') 57 | avg_rssi = ((15 * avg_rssi) + rssi) / 16 58 | stdscr.addstr(0, 0, "{0:4.0f} : {1:4.0f}".format(rssi, avg_rssi)) 59 | stdscr.addstr(20, 0, "{0:4.1f}".format(rssi)) 60 | 61 | # select the bargraph's colour: 62 | if rssi > 30: 63 | bargraph_color = 4 64 | elif rssi > 20: 65 | bargraph_color = 3 66 | elif rssi > 15: 67 | bargraph_color = 2 68 | else: 69 | bargraph_color = 1 70 | # draw the bargraph: 71 | bargraph_idx = min(int(78 - rssi), 80) 72 | stdscr.addstr(7, 0, bargraph_str[bargraph_idx:bargraph_idx+80], curses.color_pair(bargraph_color)) 73 | stdscr.addstr(8, 0, bargraph_str[bargraph_idx:bargraph_idx+80], curses.color_pair(bargraph_color)) 74 | 75 | stdscr.refresh() 76 | 77 | max_rssi = max(max_rssi, rssi) 78 | counter += 1 79 | if counter & 0x1F == 0: 80 | tone_idx = max_rssi 81 | tone_idx = max(0, tone_idx) 82 | tone_idx = min(40, tone_idx) 83 | tones.play(tone_idx) 84 | max_rssi = rssi 85 | stdscr.addstr(21, 0, "{0:4.1f}".format(max_rssi)) 86 | 87 | # First attempt: using floating-point complex samples 88 | def MeasureRSSI_1(sdr): 89 | samples = sdr.read_samples(NUM_SAMPLES) 90 | power = 0.0 91 | for sample in samples: 92 | power += (sample.real * sample.real) + (sample.imag * sample.imag) 93 | return 10 * (math.log(power) - math.log(NUM_SAMPLES)) 94 | 95 | # Second go: read raw bytes, square and add those 96 | def MeasureRSSI_2(sdr): 97 | data_bytes = sdr.read_bytes(NUM_SAMPLES * 2) 98 | power = 0 99 | for next_byte in data_bytes: 100 | signed_byte = next_byte + next_byte - 255 101 | power += signed_byte * signed_byte 102 | return 10 * (math.log(power) - math.log(NUM_SAMPLES) - math.log(127)) - 70 103 | 104 | # Third go: modify librtlsdr, do the square-and-add calculation in C 105 | def MeasureRSSI_3(sdr): 106 | while(True): 107 | try: 108 | return sdr.read_power_dB(NUM_SAMPLES) - 112 109 | except OSError: # e.g. SDR unplugged... 110 | pass # go round and try again, SDR will be replugged sometime... 111 | 112 | # Select the desired implementation here: 113 | def MeasureRSSI(sdr): 114 | return MeasureRSSI_3(sdr) 115 | # return sdr.read_offset_I(NUM_SAMPLES) / (NUM_SAMPLES / 2) 116 | # return sdr.read_offset_Q(NUM_SAMPLES) / (NUM_SAMPLES / 2) 117 | 118 | def redirect_stderr(): 119 | import os, sys 120 | sys.stderr.flush() 121 | err = open('/dev/null', 'a+') 122 | os.dup2(err.fileno(), sys.stderr.fileno()) # send ALSA underrun error messages to /dev/null 123 | 124 | 125 | if __name__ == "__main__": 126 | import os, sys 127 | wrapper(main) 128 | -------------------------------------------------------------------------------- /src/jammer_detect_no_ui.py: -------------------------------------------------------------------------------- 1 | from rtlsdr import RtlSdr 2 | import math 3 | from threading import Timer # for watchdog timer 4 | from audio_tones import AudioTones 5 | 6 | NUM_SAMPLES = 32768 7 | BARGRAPH = "################################################################################" \ 8 | + " " 9 | 10 | def main(): 11 | 12 | sdr = RtlSdr() 13 | 14 | # configure device 15 | sdr.sample_rate = 1.024e6 # Hz 16 | sdr.center_freq = 433.9e6 # Hz 17 | sdr.freq_correction = 20 # PPM 18 | sdr.gain = 'auto' 19 | 20 | tones = AudioTones() 21 | tones.init() 22 | 23 | for i in range(0, 10): 24 | rssi = MeasureRSSI(sdr) 25 | 26 | # Measure minimum RSSI over a few readings, auto-adjust for dongle gain 27 | min_rssi = 1000 28 | avg_rssi = 0 29 | for i in range(0, 10): 30 | rssi = MeasureRSSI(sdr) 31 | min_rssi = min(min_rssi, rssi) 32 | avg_rssi += rssi 33 | avg_rssi /= 10 34 | ampl_offset = avg_rssi 35 | max_rssi = MeasureRSSI(sdr) - ampl_offset 36 | avg_rssi = max_rssi + 20; 37 | counter = 0 38 | # redirect_stderr() 39 | 40 | 41 | while(True): 42 | wdt = Timer(3.0, watchdog_timeout) 43 | wdt.start() 44 | rssi = MeasureRSSI(sdr) - ampl_offset 45 | wdt.cancel() 46 | avg_rssi = ((15 * avg_rssi) + rssi) / 16 47 | 48 | max_rssi = max(max_rssi, rssi) 49 | counter += 1 50 | if counter & 0x1F == 0: 51 | tone_idx = int(max_rssi) 52 | tone_idx = max(0, tone_idx) 53 | tone_idx = min(45, tone_idx) 54 | tones.play(tone_idx) 55 | max_rssi = rssi 56 | 57 | # First attempt: using floating-point complex samples 58 | def MeasureRSSI_1(sdr): 59 | samples = sdr.read_samples(NUM_SAMPLES) 60 | power = 0.0 61 | for sample in samples: 62 | power += (sample.real * sample.real) + (sample.imag * sample.imag) 63 | return 10 * (math.log(power) - math.log(NUM_SAMPLES)) 64 | 65 | # Second go: read raw bytes, square and add those 66 | def MeasureRSSI_2(sdr): 67 | data_bytes = sdr.read_bytes(NUM_SAMPLES * 2) 68 | power = 0 69 | for next_byte in data_bytes: 70 | signed_byte = next_byte + next_byte - 255 71 | power += signed_byte * signed_byte 72 | return 10 * (math.log(power) - math.log(NUM_SAMPLES) - math.log(127)) - 70 73 | 74 | # Third go: modify librtlsdr, do the square-and-add calculation in C 75 | def MeasureRSSI_3(sdr): 76 | while(True): 77 | try: 78 | return sdr.read_power_dB(NUM_SAMPLES) - 112 79 | except OSError: # e.g. SDR unplugged... 80 | pass # go round and try again, SDR will be replugged sometime... 81 | 82 | # Select the desired implementation here: 83 | def MeasureRSSI(sdr): 84 | return MeasureRSSI_3(sdr) 85 | # return sdr.read_offset_I(NUM_SAMPLES) / (NUM_SAMPLES / 2) 86 | # return sdr.read_offset_Q(NUM_SAMPLES) / (NUM_SAMPLES / 2) 87 | 88 | def redirect_stderr(): 89 | import os, sys 90 | sys.stderr.flush() 91 | err = open('/dev/null', 'a+') 92 | os.dup2(err.fileno(), sys.stderr.fileno()) # send ALSA underrun error messages to /dev/null 93 | 94 | def watchdog_timeout(): 95 | print("**** WATCHDOG TIMEOUT ****") 96 | exit() 97 | 98 | 99 | if __name__ == "__main__": 100 | import os, sys 101 | main() 102 | --------------------------------------------------------------------------------