big patch from Graham - many thx

This commit is contained in:
Max 2017-09-05 16:42:55 -04:00
parent 187f5d180c
commit 93b19531ee
8 changed files with 129 additions and 59 deletions

View File

@ -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
==========

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)
{

View File

@ -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<int16_t> &output_queue, bool do_audio_output);
~p25p1_fdma();

View File

@ -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;
}