git clone git://github.com/bistromath/gr-smartnet.git cd gr-smartnet git reset --hard b3335c1441ba5d2e9266e39f7c85faf65d546ba8 git apply <<"END_PATCH" diff --git a/Makefile.common b/Makefile.common index 4b80811..299d1a9 100644 --- a/Makefile.common +++ b/Makefile.common @@ -35,7 +35,8 @@ AM_CPPFLAGS = \ STD_DEFINES_AND_INCLUDES = \ $(DEFINES) \ -I$(GNURADIO_CORE_INCLUDEDIR) \ - -I$(GNURADIO_CORE_INCLUDEDIR)/swig + -I$(GNURADIO_CORE_INCLUDEDIR)/swig \ + -I$(GNURADIO_CORE_INCLUDEDIR)/../gruel/swig # includes grincludedir = $(includedir)/gnuradio diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am index 702a898..1e9dc21 100644 --- a/src/lib/Makefile.am +++ b/src/lib/Makefile.am @@ -31,7 +31,8 @@ grinclude_HEADERS = \ smartnet_parse.h \ smartnet_types.h \ smartnet_subchannel_framer.h \ - smartnet_wavsink.h + smartnet_wavsink.h \ + smartnet_wavfile.h ################################### # SWIG Python interface and library @@ -59,6 +60,7 @@ smartnet_la_swig_sources = \ smartnet_packetize.cc \ smartnet_parse.cc \ smartnet_subchannel_framer.cc \ + smartnet_wavfile.cc \ smartnet_wavsink.cc # additional arguments to the SWIG command diff --git a/src/lib/smartnet_wavfile.cc b/src/lib/smartnet_wavfile.cc new file mode 100644 index 0000000..a782390 --- /dev/null +++ b/src/lib/smartnet_wavfile.cc @@ -0,0 +1,313 @@ +/* -*- c++ -*- */ +/* + * Copyright 2004,2008 Free Software Foundation, Inc. + * + * This file is part of GNU Radio + * + * GNU Radio is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3, or (at your option) + * any later version. + * + * GNU Radio is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Radio; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include + +# define VALID_COMPRESSION_TYPE 0x0001 + +// WAV files are always little-endian, so we need some byte switching macros + +// FIXME: Use libgruel versions + +#ifdef WORDS_BIGENDIAN + +#ifdef HAVE_BYTESWAP_H +#include +#else +#warning Using non-portable code (likely wrong other than ILP32). + +static inline short int +bswap_16 (unsigned short int x) +{ + return ((((x) >> 8) & 0xff) | (((x) & 0xff) << 8)); +} + +static inline unsigned int +bswap_32 (unsigned int x) +{ + return ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >> 8) \ + | (((x) & 0x0000ff00) << 8) | (((x) & 0x000000ff) << 24)); +} +#endif // HAVE_BYTESWAP_H + +static inline uint32_t +host_to_wav(uint32_t x) +{ + return bswap_32(x); +} + +static inline uint16_t +host_to_wav(uint16_t x) +{ + return bswap_16(x); +} + +static inline int16_t +host_to_wav(int16_t x) +{ + return bswap_16(x); +} + +static inline uint32_t +wav_to_host(uint32_t x) +{ + return bswap_32(x); +} + +static inline uint16_t +wav_to_host(uint16_t x) +{ + return bswap_16(x); +} + +static inline int16_t +wav_to_host(int16_t x) +{ + return bswap_16(x); +} + +#else + +static inline uint32_t +host_to_wav(uint32_t x) +{ + return x; +} + +static inline uint16_t +host_to_wav(uint16_t x) +{ + return x; +} + +static inline int16_t +host_to_wav(int16_t x) +{ + return x; +} + +static inline uint32_t +wav_to_host(uint32_t x) +{ + return x; +} + +static inline uint16_t +wav_to_host(uint16_t x) +{ + return x; +} + +static inline int16_t +wav_to_host(int16_t x) +{ + return x; +} + +#endif // WORDS_BIGENDIAN + + +bool +smartnet_wavheader_parse(FILE *fp, + unsigned int &sample_rate_o, + int &nchans_o, + int &bytes_per_sample_o, + int &first_sample_pos_o, + unsigned int &samples_per_chan_o) +{ + // _o variables take return values + char str_buf[8] = {0}; + + uint32_t file_size; + uint32_t fmt_hdr_skip; + uint16_t compression_type; + uint16_t nchans; + uint32_t sample_rate; + uint32_t avg_bytes_per_sec; + uint16_t block_align; + uint16_t bits_per_sample; + uint32_t chunk_size; + + size_t fresult; + + fresult = fread(str_buf, 1, 4, fp); + if (fresult != 4 || strncmp(str_buf, "RIFF", 4) || feof(fp)) { + return false; + } + + fresult = fread(&file_size, 1, 4, fp); + + fresult = fread(str_buf, 1, 8, fp); + if (fresult != 8 || strncmp(str_buf, "WAVEfmt ", 8) || feof(fp)) { + return false; + } + + fresult = fread(&fmt_hdr_skip, 1, 4, fp); + + fresult = fread(&compression_type, 1, 2, fp); + if (wav_to_host(compression_type) != VALID_COMPRESSION_TYPE) { + return false; + } + + fresult = fread(&nchans, 1, 2, fp); + fresult = fread(&sample_rate, 1, 4, fp); + fresult = fread(&avg_bytes_per_sec, 1, 4, fp); + fresult = fread(&block_align, 1, 2, fp); + fresult = fread(&bits_per_sample, 1, 2, fp); + + if (ferror(fp)) { + return false; + } + + fmt_hdr_skip = wav_to_host(fmt_hdr_skip); + nchans = wav_to_host(nchans); + sample_rate = wav_to_host(sample_rate); + bits_per_sample = wav_to_host(bits_per_sample); + + if (bits_per_sample != 8 && bits_per_sample != 16) { + return false; + } + + fmt_hdr_skip -= 16; + if (fmt_hdr_skip) { + fseek(fp, fmt_hdr_skip, SEEK_CUR); + } + + // data chunk + fresult = fread(str_buf, 1, 4, fp); + if (strncmp(str_buf, "data", 4)) { + return false; + } + + fresult = fread(&chunk_size, 1, 4, fp); + if (ferror(fp)) { + return false; + } + + // More byte swapping + chunk_size = wav_to_host(chunk_size); + + // Output values + sample_rate_o = (unsigned) sample_rate; + nchans_o = (int) nchans; + bytes_per_sample_o = (int) (bits_per_sample / 8); + first_sample_pos_o = (int) ftell(fp); + samples_per_chan_o = (unsigned) (chunk_size / (bytes_per_sample_o * nchans)); + return true; +} + + +short int +smartnet_wav_read_sample(FILE *fp, int bytes_per_sample) +{ + int16_t buf = 0; + size_t fresult; + + fresult = fread(&buf, bytes_per_sample, 1, fp); + + return (short) wav_to_host(buf); +} + + +bool +smartnet_wavheader_write(FILE *fp, + unsigned int sample_rate, + int nchans, + int bytes_per_sample) +{ + const int header_len = 44; + char wav_hdr[header_len] = "RIFF\0\0\0\0WAVEfmt \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0data\0\0\0"; + uint16_t nchans_f = (uint16_t) nchans; + uint32_t sample_rate_f = (uint32_t) sample_rate; + uint16_t block_align = bytes_per_sample * nchans; + uint32_t avg_bytes = sample_rate * block_align; + uint16_t bits_per_sample = bytes_per_sample * 8; + + nchans_f = host_to_wav(nchans_f); + sample_rate_f = host_to_wav(sample_rate_f); + block_align = host_to_wav(block_align); + avg_bytes = host_to_wav(avg_bytes); + bits_per_sample = host_to_wav(bits_per_sample); + + wav_hdr[16] = 0x10; // no extra bytes + wav_hdr[20] = 0x01; // no compression + memcpy((void *) (wav_hdr + 22), (void *) &nchans_f, 2); + memcpy((void *) (wav_hdr + 24), (void *) &sample_rate_f, 4); + memcpy((void *) (wav_hdr + 28), (void *) &avg_bytes, 4); + memcpy((void *) (wav_hdr + 32), (void *) &block_align, 2); + memcpy((void *) (wav_hdr + 34), (void *) &bits_per_sample, 2); + + fwrite(&wav_hdr, 1, header_len, fp); + if (ferror(fp)) { + return false; + } + + return true; +} + + +void +smartnet_wav_write_sample(FILE *fp, short int sample, int bytes_per_sample) +{ + void *data_ptr; + unsigned char buf_8bit; + int16_t buf_16bit; + + if (bytes_per_sample == 1) { + buf_8bit = (unsigned char) sample; + data_ptr = (void *) &buf_8bit; + } else { + buf_16bit = host_to_wav((int16_t) sample); + data_ptr = (void *) &buf_16bit; + } + + fwrite(data_ptr, 1, bytes_per_sample, fp); +} + + +bool +smartnet_wavheader_complete(FILE *fp, unsigned int byte_count) +{ + uint32_t chunk_size = (uint32_t) byte_count; + chunk_size = host_to_wav(chunk_size); + + fseek(fp, 40, SEEK_SET); + fwrite(&chunk_size, 1, 4, fp); + + chunk_size = (uint32_t) byte_count + 36; // fmt chunk and data header + chunk_size = host_to_wav(chunk_size); + fseek(fp, 4, SEEK_SET); + + fwrite(&chunk_size, 1, 4, fp); + + if (ferror(fp)) { + return false; + } + + return true; +} diff --git a/src/lib/smartnet_wavfile.h b/src/lib/smartnet_wavfile.h new file mode 100644 index 0000000..2c70d0b --- /dev/null +++ b/src/lib/smartnet_wavfile.h @@ -0,0 +1,101 @@ +/* -*- c++ -*- */ +/* + * Copyright 2008 Free Software Foundation, Inc. + * + * This file is part of GNU Radio + * + * GNU Radio is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3, or (at your option) + * any later version. + * + * GNU Radio is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Radio; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +// This file stores all the RIFF file type knowledge for the gr_wavfile_* +// blocks. + +//#include +#include + +/*! + * \brief Read signal information from a given WAV file. + * + * \p fp File pointer to an opened, empty file. + * \p sample_rate Stores the sample rate [S/s] + * \p nchans Number of channels + * \p bytes_per_sample Bytes per sample, can either be 1 or 2 (corresponding to + * 8 or 16 bit samples, respectively) + * \p first_sample_pos Number of the first byte containing a sample. Use this + * with fseek() to jump from the end of the file to the first sample + * when in repeat mode. + * \p samples_per_chan Number of samples per channel + * \p normalize_fac The normalization factor with which you need to divide the + * integer values of the samples to get them within [-1;1] + * \p normalize_shift The value by which the sample values need to be shifted + * after normalization (reason being, 8-bit WAV files store samples as + * unsigned char and 16-bit as signed short int) + * \return True on a successful read, false if the file could not be read or is + * not a valid WAV file. + */ +bool +smartnet_wavheader_parse(FILE *fp, + unsigned int &sample_rate, + int &nchans, + int &bytes_per_sample, + int &first_sample_pos, + unsigned int &samples_per_chan); + + +/*! + * \brief Read one sample from an open WAV file at the current position. + * + * Takes care of endianness. + */ +short int +smartnet_wav_read_sample(FILE *fp, int bytes_per_sample); + + +/*! + * \brief Write a valid RIFF file header + * + * Note: Some header values are kept blank because they're usually not known + * a-priori (file and chunk lengths). Use smartnet_wavheader_complete() to fill + * these in. + */ +bool +smartnet_wavheader_write(FILE *fp, + unsigned int sample_rate, + int nchans, + int bytes_per_sample); + +/*! + * \brief Write one sample to an open WAV file at the current position. + * + * Takes care of endianness. + */ +void +smartnet_wav_write_sample(FILE *fp, short int sample, int bytes_per_sample); + + +/*! + * \brief Complete a WAV header + * + * Note: The stream position is changed during this function. If anything + * needs to be written to the WAV file after calling this function (which + * shouldn't happen), you need to fseek() to the end of the file (or + * whereever). + * + * \p fp File pointer to an open WAV file with a blank header + * \p byte_count Length of all samples written to the file in bytes. + */ +bool +smartnet_wavheader_complete(FILE *fp, unsigned int byte_count); diff --git a/src/lib/smartnet_wavsink.cc b/src/lib/smartnet_wavsink.cc index bc75e0c..d795fdf 100644 --- a/src/lib/smartnet_wavsink.cc +++ b/src/lib/smartnet_wavsink.cc @@ -26,7 +26,7 @@ #include #include -#include +#include #include #include #include @@ -140,7 +140,7 @@ smartnet_wavsink::open(const char* filename) int new_nchans, new_bytes_per_sample, new_first_sample_pos; //validate the data, be sure it's set up the same as the current block (sample rate, mono/stereo, etc) and barf if it isn't - if (!gri_wavheader_parse(d_new_fp, + if (!smartnet_wavheader_parse(d_new_fp, new_sample_rate, new_nchans, new_bytes_per_sample, @@ -180,7 +180,7 @@ smartnet_wavsink::open(const char* filename) } d_updated = true; - if (!gri_wavheader_write(d_new_fp, + if (!smartnet_wavheader_write(d_new_fp, d_sample_rate, d_nchans, d_bytes_per_sample_new)) { @@ -211,7 +211,7 @@ void smartnet_wavsink::close_wav() //printf("Writing wav header with %f seconds of audio\n", ((float(d_sample_count)/d_sample_rate)/d_nchans)/d_bytes_per_sample); - if(!gri_wavheader_complete(d_fp, byte_count)) { + if(!smartnet_wavheader_complete(d_fp, byte_count)) { throw std::runtime_error("Error writing wav header\n"); } @@ -256,7 +256,7 @@ smartnet_wavsink::work (int noutput_items, sample_buf_s = 0; } - gri_wav_write_sample(d_fp, sample_buf_s, d_bytes_per_sample); + smartnet_wav_write_sample(d_fp, sample_buf_s, d_bytes_per_sample); if (feof(d_fp) || ferror(d_fp)) { fprintf(stderr, "[%s] file i/o error\n", __FILE__); diff --git a/src/python/smartnet2logging_rtl.py b/src/python/smartnet2logging_rtl.py new file mode 100755 index 0000000..7ef2f42 --- /dev/null +++ b/src/python/smartnet2logging_rtl.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python +""" + This program decodes the Motorola SmartNet II trunking protocol from the control channel + Tune it to the control channel center freq, and it'll spit out the decoded packets. + In what format? Who knows. + + This program does not include audio output support. It logs channels to disk by talkgroup name. If you don't specify what talkgroups to log, it logs EVERYTHING. +""" + +from gnuradio import gr, gru, blks2, optfir, digital +from grc_gnuradio import blks2 as grc_blks2 +from gnuradio import audio +from gnuradio import eng_notation +#from gnuradio import uhd +from fsk_demod import fsk_demod +from logging_receiver import logging_receiver +from optparse import OptionParser +from gnuradio.eng_option import eng_option +from gnuradio import smartnet +#from gnuradio.wxgui import slider +#from gnuradio.wxgui import stdgui2, fftsink2, form +import osmosdr +from gnuradio import digital + +#from pkt import * +import time +import gnuradio.gr.gr_threading as _threading +import csv +import os + +class top_block_runner(_threading.Thread): + def __init__(self, tb): + _threading.Thread.__init__(self) + self.setDaemon(1) + self.tb = tb + self.done = False + self.start() + + def run(self): + self.tb.run() + self.done = True + +class my_top_block(gr.top_block): + def __init__(self, options, queue): + gr.top_block.__init__(self) + + self.rtl = osmosdr.source_c( args="nchan=" + str(1) + " " + "" ) + self.rtl.set_sample_rate(options.rate) + self.rtl.set_center_freq(options.centerfreq, 0) + self.rtl.set_freq_corr(options.ppm, 0) + self.rtl.set_gain_mode(1, 0) + + self.centerfreq = options.centerfreq + print "Tuning to: %fMHz" % (self.centerfreq - options.error) + if not(self.tune(options.centerfreq - options.error)): + print "Failed to set initial frequency" + + if options.gain is None: #set to halfway +# g = self.u.get_gain_range() +# options.gain = (g.start()+g.stop()) / 2.0 + options.gain = 10 + # TODO FIX^ + + print "Setting gain to %i" % options.gain + self.rtl.set_gain(options.gain, 0) + + self.rate = options.rate + + print "Samples per second is %i" % self.rate + + self._syms_per_sec = 3600; + + options.audiorate = 11025 + options.rate = self.rate + + options.samples_per_second = self.rate #yeah i know it's on the list + options.syms_per_sec = self._syms_per_sec + options.gain_mu = 0.01 + options.mu=0.5 + options.omega_relative_limit = 0.3 + options.syms_per_sec = self._syms_per_sec + options.offset = options.centerfreq - options.freq + print "Control channel offset: %f" % options.offset + + self.demod = fsk_demod(options) + self.start_correlator = digital.correlate_access_code_bb("10101100",0) #should mark start of packet + self.smartnet_sync = smartnet.sync() + self.smartnet_deinterleave = smartnet.deinterleave() + self.smartnet_parity = smartnet.parity() + self.smartnet_crc = smartnet.crc() + self.smartnet_packetize = smartnet.packetize() + self.parse = smartnet.parse(queue) #packet-based. this simply posts lightly-formatted messages to the queue. + + self.connect(self.rtl, self.demod) + + self.connect(self.demod, self.start_correlator, self.smartnet_sync, self.smartnet_deinterleave, self.smartnet_parity, self.smartnet_crc, self.smartnet_packetize, self.parse) + + + def tune(self, freq): + result = self.rtl.set_center_freq(freq) + return True + +def getfreq(chanlist, cmd): + if chanlist is None: #if no chanlist file, make a guess. there are four extant bandplan schemes, and i believe this one is the most common. +# if cmd < 0x2d0: +# Changed for rebanding + if cmd < 0x1b8: + freq = float(cmd * 0.025 + 851.0125) + elif cmd < 0x230: + freq = float(cmd * 0.025 + 851.0125 - 10.9875) + else: + freq = None + else: #program your channel listings, get the right freqs. + if chanlist.get(str(cmd), None) is not None: + freq = float(chanlist[str(cmd)]) + else: + freq = None + + return freq + +def parsefreq(s, chanlist): + retfreq = None + [address, groupflag, command] = s.split(",") + command = int(command) + address = int(address) & 0xFFF0 + groupflag = bool(groupflag) + + if chanlist is None: + if command < 0x2d0: + retfreq = getfreq(chanlist, command) + + else: + if chanlist.get(str(command), None) is not None: #if it falls into the channel somewhere + retfreq = getfreq(chanlist, command) + return [retfreq, address] # mask so the squelch opens up on the entire group + + + +def main(): + # Create Options Parser: + parser = OptionParser (option_class=eng_option, conflict_handler="resolve") + expert_grp = parser.add_option_group("Expert") + + parser.add_option("-f", "--freq", type="eng_float", default=866.9625e6, + help="set control channel frequency to MHz [default=%default]", metavar="FREQ") + parser.add_option("-c", "--centerfreq", type="eng_float", default=867.5e6, + help="set center receive frequency to MHz [default=%default]. Set to center of 800MHz band for best results") + parser.add_option("-g", "--gain", type="int", default=None, + help="set RF gain", metavar="dB") + parser.add_option("-r", "--rate", type="eng_float", default=64e6/18, + help="set sample rate [default=%default]") +# parser.add_option("-b", "--bandwidth", type="eng_float", default=3e6, +# help="set bandwidth of DBS RX frond end [default=%default]") + parser.add_option("-C", "--chanlistfile", type="string", default=None, + help="read in list of Motorola channel frequencies (improves accuracy of frequency decoding) [default=%default]") + parser.add_option("-E", "--error", type="eng_float", default=0, + help="enter an offset error to compensate for USRP clock inaccuracy") + parser.add_option("-m", "--monitor", type="int", default=None, + help="monitor a specific talkgroup") + parser.add_option("-v", "--volume", type="eng_float", default=3.0, + help="set volume gain for audio output [default=%default]") + parser.add_option("-s", "--squelch", type="eng_float", default=28, + help="set audio squelch level (default=%default, play with it)") + parser.add_option("-D", "--directory", type="string", default="./log", + help="choose a directory in which to save log data [default=%default]") +# parser.add_option("-a", "--addr", type="string", default="", +# help="address options to pass to UHD") +# parser.add_option("-s", "--subdev", type="string", +# help="UHD subdev spec", default=None) +# parser.add_option("-A", "--antenna", type="string", default=None, +# help="select Rx Antenna where appropriate") +# FOR RTL + parser.add_option("-p", "--ppm", type="eng_float", default=0, + help="set RTL PPM frequency adjustment [default=%default]") + + #receive_path.add_options(parser, expert_grp) + + (options, args) = parser.parse_args () + + if len(args) != 0: + parser.print_help(sys.stderr) + sys.exit(1) + + + if options.chanlistfile is not None: + clreader=csv.DictReader(open(options.chanlistfile), quotechar='"') + chanlist={"0": 0} + for record in clreader: + chanlist[record['channel']] = record['frequency'] + else: + chanlist = None + + # build the graph + queue = gr.msg_queue() + tb = my_top_block(options, queue) + + runner = top_block_runner(tb) + + updaterate = 10 #main loop rate in Hz + audiologgers = [] #this is the list of active audio sinks. + rxfound = False #a flag to indicate whether or not an audio sink was found with the correct talkgroup ID; see below + + + try: + while 1: + if not queue.empty_p(): + msg = queue.delete_head() # Blocking read + sentence = msg.to_string() + + [newfreq, newaddr] = parsefreq(sentence, chanlist) + + monaddr = newaddr & 0xFFF0 #last 8 bits are status flags for a given talkgroup + + if newfreq is not None and int(newfreq*1e6) == int(options.freq): + newfreq = None #don't log the audio from the trunk itself + + #we have a new frequency assignment. look through the list of audio logger objects and find if any of them have been allocated to it + rxfound = False + + for rx in audiologgers: + + #print "Logger info: %i @ %f idle for %fs" % (rx.talkgroup, rx.getfreq(options.centerfreq), rx.timeout()) #TODO: debug + + #first look through the list to find out if there is a receiver assigned to this talkgroup + if rx.talkgroup == monaddr: #here we've got one + if newfreq != rx.getfreq(options.centerfreq) and newfreq is not None: #we're on a new channel, though + rx.tuneoffset(newfreq, options.centerfreq) + + rx.unmute() #this should be unnecessary but it does update the timestamp + rxfound = True + #print "New transmission on TG %i, updating timestamp" % rx.talkgroup + + else: + if rx.getfreq(options.centerfreq) == newfreq: #a different talkgroup, but a new assignment on that freq! time to mute. + rx.mute() + + if rxfound is False and newfreq is not None: #no existing receiver for this talkgroup. time to create one. + #lock the flowgraph + tb.lock() + audiologgers.append( logging_receiver(newaddr, options) ) #create it + audiologgers[-1].tuneoffset(newfreq, options.centerfreq) #tune it + tb.connect(tb.rtl, audiologgers[-1]) #connect to the flowgraph + tb.unlock() + audiologgers[-1].unmute() #unmute it + + if newfreq is not None: + print "TG %i @ %f, %i active loggers" % (newaddr, newfreq, len(audiologgers)) + + + else: + time.sleep(1.0/updaterate) + + for rx in audiologgers: + if rx.timeout() >= 5.0: #if this receiver has been muted more than 3 seconds + rx.close() #close the .wav file that the logger has been writing to + tb.lock() + tb.disconnect(rx) + tb.unlock() + audiologgers.remove(rx) #delete the audio logger object from the list + + except KeyboardInterrupt: + #perform cleanup: time to get out of Dodge + for rx in audiologgers: #you probably don't need to lock, disconnect, unlock, remove. but you might as well. + rx.close() + #tb.lock() + #tb.disconnect(rx) + #tb.unlock() + audiologgers.remove(rx) + + tb.stop() + + runner = None + +if __name__ == '__main__': + main() + + END_PATCH