From 93b19531ee0942d4a589bb3e7345e28f1f578759 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 5 Sep 2017 16:42:55 -0400 Subject: big patch from Graham - many thx --- op25/gr-op25_repeater/apps/README | 37 +++++++-------- op25/gr-op25_repeater/apps/gr_gnuplot.py | 53 +++++++++++++++------- op25/gr-op25_repeater/apps/rx.py | 47 ++++++++++++------- op25/gr-op25_repeater/apps/trunking.py | 29 +++++++++--- .../lib/p25_frame_assembler_impl.cc | 1 + op25/gr-op25_repeater/lib/p25p1_fdma.cc | 7 +++ op25/gr-op25_repeater/lib/p25p1_fdma.h | 1 + op25/gr-op25_repeater/lib/p25p2_tdma.cc | 13 +++++- 8 files changed, 129 insertions(+), 59 deletions(-) diff --git a/op25/gr-op25_repeater/apps/README b/op25/gr-op25_repeater/apps/README index cb98191..f02f687 100644 --- a/op25/gr-op25_repeater/apps/README +++ b/op25/gr-op25_repeater/apps/README @@ -54,28 +54,29 @@ the mail list. EXTERNAL UDP AUDIO SERVER ========================= -Because the GR block no longer outputs audio samples the audio is routed -via UDP instead. After starting rx.py in a separate terminal window run +Starting rx.py with the "-w -W host" options directs udp audio data to +be sent over the network to the specified remote host. It can then be +received and played back with either of the following methods: +1. Execute ./audio.sh on a remote machine equipped with python2.7, + libasound.so.2 and the sockaudio.py file. +-or- +2. Execute the command: nc -kluvw 1 127.0.0.1 23456 | aplay -c1 -f S16_LE -r 8000 -Notes: -1. Each time rx.py is restarted you must also restart the audio server -(change to the terminal window where the server is running and hit Ctrl-C, -then up-arrow, then Enter). +NOTE: audio underruns are to be expected when using nc | aplay as the +pcm stream is interrupted every time a radio transmission ends. The +sockaudio player is designed to handle this more gracefully, and generally +only underruns due to high cpu utilization or reception/decoding errors. -2. When doing audio output it is no longer necessary to specify the -"-V" option in rx.py. However for now as a hack it's necessary to -give the "-w" (wireshark) rx.py option. If/when the hack is removed and -wireshark is fixed it will no longer be necessary to use "-w". The "-2" -option is still required when using phase 2/TDMA. +INTERNAL AUDIO SERVER +===================== +Starting rx.py with the "-U" command line option enables an internal udp +audio server which will play received audio through the default ALSA +device. Optionally you may specify which ALSA device to use by setting +the "-O audio_out" option along with "-U". -3. If the use of "aplay" in this manner causes no problems (including -running in a VM, etc), the command will eventually be rolled into rx.py -and it will no longer be necessary to run the server manually in this -way. Reports are needed from VM users both with and without pulse, and -Phase II/TDMA users - -4. "aplay" is in package "alsa-utils" and "nc" in "netcat-openbsd" +As of this writing (Aug 2017) it is still necessary to specify the "-w" +(wireshark) option if using either the internal or external audio server. PLOT MODES ========== diff --git a/op25/gr-op25_repeater/apps/gr_gnuplot.py b/op25/gr-op25_repeater/apps/gr_gnuplot.py index f4e4942..58ccc72 100644 --- a/op25/gr-op25_repeater/apps/gr_gnuplot.py +++ b/op25/gr-op25_repeater/apps/gr_gnuplot.py @@ -33,11 +33,18 @@ _def_sps = 10 GNUPLOT = '/usr/bin/gnuplot' +FFT_AVG = 0.25 +FFT_BINS = 512 + class wrap_gp(object): def __init__(self, sps=_def_sps): self.sps = sps self.center_freq = None + self.relative_freq = 0.0 self.width = None + self.ffts = () + self.freqs = () + self.avg_pwr = np.zeros(FFT_BINS) self.buf = [] self.attach_gp() @@ -82,49 +89,57 @@ class wrap_gp(object): self.buf = [] plots.append('"-" with dots') elif mode == 'fft': - ffbuf = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ) - ffbuf = np.fft.fftshift(ffbuf) - for i in xrange(len(ffbuf)): - if self.center_freq and self.width: - f = (self.center_freq - self.width / 2.0) / 1e6 - w = self.width / 1e6 - s += '%f\t%f\n' % (f + i*(w/BUFSZ), 20 * np.log10(np.abs(ffbuf[i]))) - else: - s += '%f\n' % (20 * np.log10(np.abs(ffbuf[i]))) + self.ffts = np.fft.fft(self.buf * np.blackman(BUFSZ)) / (0.42 * BUFSZ) + self.ffts = np.fft.fftshift(self.ffts) + self.freqs = np.fft.fftfreq(len(self.ffts)) + self.freqs = np.fft.fftshift(self.freqs) + if self.center_freq and self.width: + self.freqs = ((self.freqs * self.width) + self.center_freq) / 1e6 + for i in xrange(len(self.ffts)): + self.avg_pwr[i] = ((1.0 - FFT_AVG) * self.avg_pwr[i]) + (FFT_AVG * np.abs(self.ffts[i])) + s += '%f\t%f\n' % (self.freqs[i], 20 * np.log10(self.avg_pwr[i])) s += 'e\n' self.buf = [] plots.append('"-" with lines') self.buf = [] h= 'set terminal x11 noraise\n' - background = 'set object 1 circle from screen 0,0 to screen 1,1 fillcolor rgb"black"\n' + #background = 'set object 1 circle at screen 0,0 size screen 1 fillcolor rgb"black"\n' #FIXME! + background = '' h+= 'set key off\n' if mode == 'constellation': - h += background + h+= background h+= 'set size square\n' h+= 'set xrange [-1:1]\n' h+= 'set yrange [-1:1]\n' elif mode == 'eye': - h += background + h+= background h+= 'set yrange [-4:4]\n' elif mode == 'symbol': - h += background + h+= background h+= 'set yrange [-4:4]\n' elif mode == 'fft': + h+= 'unset arrow; unset title\n' + h+= 'set xrange [%f:%f]\n' % (self.freqs[0], self.freqs[len(self.freqs)-1]) h+= 'set yrange [-100:0]\n' + h+= 'set xlabel "Frequency"\n' + h+= 'set ylabel "Power(dB)"\n' h+= 'set grid\n' if self.center_freq: - h += 'set title "%f"\n' % (self.center_freq / 1e6) + arrow_pos = (self.center_freq - self.relative_freq) / 1e6 + h+= 'set arrow from %f, graph 0 to %f, graph 1 nohead\n' % (arrow_pos, arrow_pos) + h+= 'set title "Tuned to %f Mhz"\n' % ((self.center_freq - self.relative_freq) / 1e6) dat = '%splot %s\n%s' % (h, ','.join(plots), s) self.gp.stdin.write(dat) return consumed def set_center_freq(self, f): - sys.stderr.write('set_center_freq: %s\n' % f) self.center_freq = f + def set_relative_freq(self, f): + self.relative_freq = f + def set_width(self, w): - sys.stderr.write('set_width: %f\n' % w) self.width = w class eye_sink_f(gr.sync_block): @@ -183,7 +198,7 @@ class fft_sink_c(gr.sync_block): if self.skip == 50: self.skip = 0 in0 = input_items[0] - self.gnuplot.plot(in0, 512, mode='fft') + self.gnuplot.plot(in0, FFT_BINS, mode='fft') return len(input_items[0]) def kill(self): @@ -191,6 +206,10 @@ class fft_sink_c(gr.sync_block): def set_center_freq(self, f): self.gnuplot.set_center_freq(f) + self.gnuplot.set_relative_freq(0.0) + + def set_relative_freq(self, f): + self.gnuplot.set_relative_freq(f) def set_width(self, w): self.gnuplot.set_width(w) diff --git a/op25/gr-op25_repeater/apps/rx.py b/op25/gr-op25_repeater/apps/rx.py index 4adb19b..95f7cfe 100755 --- a/op25/gr-op25_repeater/apps/rx.py +++ b/op25/gr-op25_repeater/apps/rx.py @@ -65,7 +65,8 @@ from gr_gnuplot import fft_sink_c from gr_gnuplot import symbol_sink_f from gr_gnuplot import eye_sink_f -from terminal import curses_terminal +from terminal import curses_terminal +from sockaudio import socket_audio #speeds = [300, 600, 900, 1200, 1440, 1800, 1920, 2400, 2880, 3200, 3600, 3840, 4000, 4800, 6000, 6400, 7200, 8000, 9600, 14400, 19200] speeds = [4800, 6000] @@ -120,7 +121,9 @@ class p25_rx_block (gr.top_block): parser.add_option("-G", "--gain-mu", type="eng_float", default=0.025, help="gardner gain") parser.add_option("-N", "--gains", type="string", default=None, help="gain settings") parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name") + parser.add_option("-U", "--udp-player", action="store_true", default=False, help="enable built-in udp audio player") parser.add_option("-q", "--freq-corr", type="eng_float", default=0.0, help="frequency correction") + parser.add_option("-d", "--fine-tune", type="eng_float", default=0.0, help="fine tuning") parser.add_option("-2", "--phase2-tdma", action="store_true", default=False, help="enable phase2 tdma decode") parser.add_option("-Z", "--decim-amt", type="int", default=1, help="spectrum decimation") (options, args) = parser.parse_args() @@ -227,6 +230,12 @@ class p25_rx_block (gr.top_block): # attach terminal thread self.terminal = curses_terminal(self.input_q, self.output_q) + # attach audio thread + if self.options.udp_player: + self.audio = socket_audio("127.0.0.1", WIRESHARK_PORT, self.options.audio_output) + else: + self.audio = None + # setup common flow graph elements # def __build_graph(self, source, capture_rate): @@ -236,7 +245,8 @@ class p25_rx_block (gr.top_block): self.rx_q = gr.msg_queue(100) udp_port = 0 - if self.options.wireshark: + + if self.options.udp_player or self.options.wireshark or (self.options.wireshark_host != "127.0.0.1"): udp_port = WIRESHARK_PORT self.tdma_state = False @@ -256,7 +266,7 @@ class p25_rx_block (gr.top_block): self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate) else: # complex input # local osc - self.lo_freq = self.options.offset + self.lo_freq = self.options.offset + self.options.fine_tune if self.options.audio_if or self.options.ifile or self.options.input: self.lo_freq += self.options.calibration self.demod = p25_demodulator.p25_demod_cb( input_rate = capture_rate, @@ -268,10 +278,6 @@ class p25_rx_block (gr.top_block): costas_alpha = self.options.costas_alpha, symbol_rate = self.symbol_rate) - udp_port = 0 - if self.options.wireshark: - udp_port = WIRESHARK_PORT - num_ambe = 0 if self.options.phase2_tdma: num_ambe = 1 @@ -377,20 +383,26 @@ class p25_rx_block (gr.top_block): freq = params['freq'] offset = params['offset'] center_freq = params['center_frequency'] + fine_tune = self.options.fine_tune if self.options.hamlib_model: self.hamlib.set_freq(freq) elif params['center_frequency']: relative_freq = center_freq - freq if abs(relative_freq + self.options.offset) > self.channel_rate / 2: - print '***unable to tune Local Oscillator to offset %d Hz' % (relative_freq + self.options.offset) - print '***limit is one half of sample-rate %d = %d' % (self.channel_rate, self.channel_rate / 2) - print '***request for frequency %d rejected' % freq - - self.lo_freq = self.options.offset + relative_freq - self.demod.set_relative_frequency(self.lo_freq) - self.set_freq(center_freq + offset) - #self.spectrum.set_baseband_freq(center_freq) + self.lo_freq = self.options.offset + self.options.fine_tune # relative tune not possible + self.demod.set_relative_frequency(self.lo_freq) # reset demod relative freq + self.set_freq(freq + offset) # direct tune instead + else: + self.lo_freq = self.options.offset + relative_freq + fine_tune + if self.demod.set_relative_frequency(self.lo_freq): # relative tune successful + self.set_freq(center_freq + offset) + if self.fft_sink: + self.fft_sink.set_relative_freq(self.lo_freq) + else: + self.lo_freq = self.options.offset + self.options.fine_tune # relative tune unsuccessful + self.demod.set_relative_frequency(self.lo_freq) # reset demod relative freq + self.set_freq(freq + offset) # direct tune instead else: self.set_freq(freq + offset) @@ -648,7 +660,10 @@ if __name__ == "__main__": except: sys.stderr.write('main: exception occurred\n') sys.stderr.write('main: exception:\n%s\n' % traceback.format_exc()) - tb.terminal.end_curses() + if tb.terminal: + tb.terminal.end_curses() + if tb.audio: + tb.audio.stop() tb.stop() if tb.kill_sink: tb.kill_sink.kill() diff --git a/op25/gr-op25_repeater/apps/trunking.py b/op25/gr-op25_repeater/apps/trunking.py index a38194b..aaf28b7 100644 --- a/op25/gr-op25_repeater/apps/trunking.py +++ b/op25/gr-op25_repeater/apps/trunking.py @@ -520,7 +520,7 @@ class rx_ctl (object): VC = 3 self.states = _states - self.state = self.states.CC + self.current_state = self.states.CC self.trunked_systems = {} self.frequency_set = frequency_set self.debug = debug @@ -531,6 +531,8 @@ class rx_ctl (object): self.TGID_SKIP_TIME = 1.0 # TODO: make more configurable self.current_nac = None self.current_id = 0 + self.current_tgid = None + self.current_slot = None self.TSYS_HOLD_TIME = 3.0 # TODO: make more configurable self.wait_until = time.time() self.configs = {} @@ -728,7 +730,7 @@ class rx_ctl (object): return s = s[2:] if self.debug > 10: - print "nac %x type %d at %f state %d len %d" %(nac, type, time.time(), self.state, len(s)) + print "nac %x type %d at %f state %d len %d" %(nac, type, time.time(), self.current_state, len(s)) if (type == 7 or type == 12) and nac not in self.trunked_systems: if not self.configs: # TODO: allow whitelist/blacklist rather than blind automatic-add @@ -750,7 +752,7 @@ class rx_ctl (object): mbt_data = (mbt_data << 8) + ord(c) opcode = (header >> 16) & 0x3f if self.debug > 10: - print "type %d at %f state %d len %d/%d opcode %x [%x/%x]" %(type, time.time(), self.state, len(s1), len(s2), opcode, header,mbt_data) + print "type %d at %f state %d len %d/%d opcode %x [%x/%x]" %(type, time.time(), self.current_state, len(s1), len(s2), opcode, header,mbt_data) updated += self.trunked_systems[nac].decode_mbt_data(opcode, header << 16, mbt_data << 32) if nac != self.current_nac: @@ -882,10 +884,14 @@ class rx_ctl (object): new_nac = None new_slot = None - if command == 'timeout' or command == 'duid15': + if command == 'timeout': if self.current_state == self.states.CC: + if self.debug > 0: + sys.stderr.write("[%f] control channel timeout\n" % time.time()) tsys.cc_timeouts += 1 elif self.current_state != self.states.CC and curr_time - self.last_tdma_vf > self.P2_GRACE_TIME: + if self.debug > 0: + sys.stderr.write("[%f] voice timeout\n" % time.time()) new_state = self.states.CC new_frequency = tsys.trunk_cc elif command == 'update': @@ -895,15 +901,23 @@ class rx_ctl (object): desired_tgid = self.tgid_hold new_frequency, new_tgid, tdma_slot = tsys.find_talkgroup(curr_time, tgid=desired_tgid) if new_frequency: + if self.debug > 0: + sys.stderr.write("[%f] voice update: tg(%s), freq(%s), slot(%s)\n" % (time.time(), new_tgid, new_frequency, tdma_slot)) new_state = self.states.TO_VC self.current_tgid = new_tgid new_slot = tdma_slot - elif command == 'duid3' or command == 'tdma_duid3': + elif command == 'tdma_duid3': # tdma termination, no channel release (MAC_HANGTIME) + if self.current_state != self.states.CC: + self.tgid_hold = self.current_tgid + self.tgid_hold_until = max(curr_time + self.TGID_HOLD_TIME, self.tgid_hold_until) + self.wait_until = curr_time + self.TSYS_HOLD_TIME + self.last_tdma_vf = curr_time + elif command == 'duid3' or command == 'duid15' or command == 'tdma_duid15': # fdma/tdma termination with channel release if self.current_state != self.states.CC: new_state = self.states.CC new_frequency = tsys.trunk_cc elif command == 'duid0' or command == 'duid5' or command == 'duid10' or command == 'tdma_duid5': - if self.state == self.states.TO_VC: + if self.current_state == self.states.TO_VC: new_state = self.states.VC self.tgid_hold = self.current_tgid self.tgid_hold_until = max(curr_time + self.TGID_HOLD_TIME, self.tgid_hold_until) @@ -917,7 +931,8 @@ class rx_ctl (object): self.tgid_hold = self.current_tgid self.tgid_hold_until = curr_time + 86400 * 10000 self.hold_mode = True - sys.stderr.write ('set hold until %f tgid %s\n' % (self.tgid_hold_until, self.current_tgid)) + if self.debug > 0: + sys.stderr.write ('set hold until %f tgid %s\n' % (self.tgid_hold_until, self.current_tgid)) elif self.hold_mode is True: self.current_tgid = None self.tgid_hold = None diff --git a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc index 27d0d68..1c1acf7 100644 --- a/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc +++ b/op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc @@ -132,6 +132,7 @@ p25_frame_assembler_impl::general_work (int noutput_items, int rc = p2tdma.handle_frame(); if (rc > -1) p25p2_queue_msg(rc); + p1fdma.reset_timer(); // prevent P1 timeouts due to long TDMA transmissions } } } diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.cc b/op25/gr-op25_repeater/lib/p25p1_fdma.cc index 51ce715..336e0e8 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.cc +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.cc @@ -239,6 +239,13 @@ p25p1_fdma::process_duid(uint32_t const duid, uint32_t const nac, uint8_t const // msg.reset(); } +void +p25p1_fdma::reset_timer() +{ + //update last_qtime with current time + gettimeofday(&last_qtime, 0); +} + void p25p1_fdma::rx_sym (const uint8_t *syms, int nsyms) { diff --git a/op25/gr-op25_repeater/lib/p25p1_fdma.h b/op25/gr-op25_repeater/lib/p25p1_fdma.h index 671e781..3595699 100644 --- a/op25/gr-op25_repeater/lib/p25p1_fdma.h +++ b/op25/gr-op25_repeater/lib/p25p1_fdma.h @@ -64,6 +64,7 @@ namespace gr { p25p1_voice_decode p1voice_decode; public: + void reset_timer(); void rx_sym (const uint8_t *syms, int nsyms); p25p1_fdma(const char* udp_host, int port, int debug, bool do_imbe, bool do_output, bool do_msgq, gr::msg_queue::sptr queue, std::deque &output_queue, bool do_audio_output); ~p25p1_fdma(); diff --git a/op25/gr-op25_repeater/lib/p25p2_tdma.cc b/op25/gr-op25_repeater/lib/p25p2_tdma.cc index c3c2970..8bc1afe 100644 --- a/op25/gr-op25_repeater/lib/p25p2_tdma.cc +++ b/op25/gr-op25_repeater/lib/p25p2_tdma.cc @@ -118,7 +118,13 @@ int p25p2_tdma::process_mac_pdu(const uint8_t byte_buf[], unsigned int len) unsigned int opcode = (byte_buf[0] >> 5) & 0x7; unsigned int offset = (byte_buf[0] >> 2) & 0x7; // maps sacch opcodes into phase I duid values - static const int opcode_map[8] = {3, 5, 3, 3, 5, 3, 3, 3}; + // 0, 5, 7 - Reserved + // 1 - MAC_PTT + // 2 - MAC_END_PTT + // 3 - MAC_IDLE + // 4 - MAC_ACTIVE + // 6 - MAC_HANGTIME + static const int opcode_map[8] = {3, 5, 15, 15, 5, 3, 3, 3}; return opcode_map[opcode]; // TODO: decode MAC PDU's } @@ -174,6 +180,11 @@ int p25p2_tdma::handle_acch_frame(const uint8_t dibits[], bool fast) } else { crc_errors++; } + // write a zero audio sample (2 bytes) at end of voice to trigger pcm drain + if (((rc == 3) || (rc == 15)) && (write_sock > 0)) { + memset(write_buf, 0, 2); + sendto(write_sock, write_buf, 2, 0, (struct sockaddr *)&write_sock_addr, sizeof(write_sock_addr)); + } return rc; } -- cgit v1.2.3