December 25 2018 merge

max
Max 4 years ago
parent 582ba4395a
commit c4fe5610ba
  1. 1
      CMakeLists.txt
  2. 2
      install.sh
  3. 15
      op25/gr-op25/lib/op25_hamming.h
  4. 44
      op25/gr-op25/lib/op25_imbe_frame.h
  5. 53
      op25/gr-op25_repeater/apps/audio.py
  6. 2
      op25/gr-op25_repeater/apps/cfg.json
  7. 186
      op25/gr-op25_repeater/apps/gr_gnuplot.py
  8. 62
      op25/gr-op25_repeater/apps/http.py
  9. 56
      op25/gr-op25_repeater/apps/multi_rx.py
  10. 14
      op25/gr-op25_repeater/apps/p25_decoder.py
  11. 26
      op25/gr-op25_repeater/apps/p25_demodulator.py
  12. 172
      op25/gr-op25_repeater/apps/rx.py
  13. 117
      op25/gr-op25_repeater/apps/sockaudio.py
  14. 139
      op25/gr-op25_repeater/apps/terminal.py
  15. 368
      op25/gr-op25_repeater/apps/trunking.py
  16. 45
      op25/gr-op25_repeater/apps/tsvfile.py
  17. 7
      op25/gr-op25_repeater/apps/tx/dv_tx.py
  18. 2
      op25/gr-op25_repeater/apps/tx/ysf-cfg.dat
  19. 1
      op25/gr-op25_repeater/include/op25_repeater/frame_assembler.h
  20. 1
      op25/gr-op25_repeater/include/op25_repeater/gardner_costas_cc.h
  21. 1
      op25/gr-op25_repeater/include/op25_repeater/p25_frame_assembler.h
  22. 1
      op25/gr-op25_repeater/lib/CMakeLists.txt
  23. 3
      op25/gr-op25_repeater/lib/frame_assembler_impl.cc
  24. 1
      op25/gr-op25_repeater/lib/frame_assembler_impl.h
  25. 64
      op25/gr-op25_repeater/lib/gardner_costas_cc_impl.cc
  26. 10
      op25/gr-op25_repeater/lib/gardner_costas_cc_impl.h
  27. 44
      op25/gr-op25_repeater/lib/log_ts.h
  28. 15
      op25/gr-op25_repeater/lib/op25_audio.cc
  29. 25
      op25/gr-op25_repeater/lib/p25_frame_assembler_impl.cc
  30. 5
      op25/gr-op25_repeater/lib/p25_frame_assembler_impl.h
  31. 45
      op25/gr-op25_repeater/lib/p25_framer.cc
  32. 8
      op25/gr-op25_repeater/lib/p25_framer.h
  33. 562
      op25/gr-op25_repeater/lib/p25p1_fdma.cc
  34. 52
      op25/gr-op25_repeater/lib/p25p1_fdma.h
  35. 34
      op25/gr-op25_repeater/lib/p25p1_voice_decode.cc
  36. 9
      op25/gr-op25_repeater/lib/p25p1_voice_decode.h
  37. 40
      op25/gr-op25_repeater/lib/p25p1_voice_encode.cc
  38. 9
      op25/gr-op25_repeater/lib/p25p1_voice_encode.h
  39. 459
      op25/gr-op25_repeater/lib/p25p2_tdma.cc
  40. 50
      op25/gr-op25_repeater/lib/p25p2_tdma.h
  41. 415
      op25/gr-op25_repeater/lib/rs.cc
  42. 4
      op25/gr-op25_repeater/lib/rs.h
  43. 7
      op25/gr-op25_repeater/lib/vocoder_impl.cc
  44. 2
      op25/gr-op25_repeater/lib/vocoder_impl.h
  45. 42
      op25/gr-op25_repeater/www/www-static/index.html
  46. 10
      op25/gr-op25_repeater/www/www-static/main.css
  47. 133
      op25/gr-op25_repeater/www/www-static/main.js

@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 2.6)
project(gr-op25 CXX C)
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_FLAGS "-std=c++11")
add_subdirectory(op25/gr-op25)
add_subdirectory(op25/gr-op25_repeater)

@ -12,7 +12,7 @@ fi
sudo apt-get update
sudo apt-get build-dep gnuradio
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen
sudo apt-get install gnuradio gnuradio-dev gr-osmosdr librtlsdr-dev libuhd-dev libhackrf-dev libitpp-dev libpcap-dev cmake git swig build-essential pkg-config doxygen python-numpy python-waitress python-requests
mkdir build
cd build

@ -3,6 +3,7 @@
#include <cstddef>
#include <stdint.h>
#include <assert.h>
/*
* APCO Hamming(15,11,3) ecoder.
@ -183,4 +184,18 @@ hamming_15_decode(uint16_t& cw)
return errs;
}
static const uint32_t hmg1063EncTbl[64] = {
0, 12, 3, 15, 7, 11, 4, 8, 11, 7, 8, 4, 12, 0, 15, 3,
13, 1, 14, 2, 10, 6, 9, 5, 6, 10, 5, 9, 1, 13, 2, 14,
14, 2, 13, 1, 9, 5, 10, 6, 5, 9, 6, 10, 2, 14, 1, 13,
3, 15, 0, 12, 4, 8, 7, 11, 8, 4, 11, 7, 15, 3, 12, 0 };
static const uint32_t hmg1063DecTbl[16] = {
0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 8, 1, 16, 32, 0 };
static inline int hmg1063Dec (uint32_t Dat, uint32_t Par) {
assert ((Dat < 64) && (Par < 16));
return Dat ^ hmg1063DecTbl[hmg1063EncTbl[Dat] ^ Par];
}
#endif /* INCLUDED_OP25_HAMMING_H */

@ -13,6 +13,50 @@ typedef std::vector<bool> voice_codeword;
typedef const std::vector<bool> const_bit_vector;
typedef std::vector<bool> bit_vector;
static const uint16_t hdu_codeword_bits[658] = { // 329 symbols = 324 + 5 pad
114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129,
130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 144, 145, 146, 147,
148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163,
164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195,
196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211,
212, 213, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245,
246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261,
262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277,
278, 279, 280, 281, 282, 283, 284, 285, 288, 289, 290, 291, 292, 293, 294, 295,
296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311,
312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327,
328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343,
344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 360, 361,
362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377,
378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393,
394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409,
410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425,
426, 427, 428, 429, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443,
444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459,
460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475,
476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491,
492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 504, 505, 506, 507, 508, 509,
510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525,
526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541,
542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557,
558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573,
576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591,
592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607,
608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623,
624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639,
640, 641, 642, 643, 644, 645, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657,
658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673,
674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705,
706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 720, 721, 722, 723,
724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739,
740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755,
756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771,
772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787,
788, 789 };
static const size_t nof_voice_codewords = 9, voice_codeword_sz = 144;
static const uint16_t imbe_ldu_NID_bits[] = {

@ -0,0 +1,53 @@
#!/usr/bin/env python
# Copyright 2017, 2018 Graham Norbury
#
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
#
# This file is part of OP25 and part of GNU Radio
#
# OP25 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.
#
# OP25 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 OP25; see the file COPYING. If not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Boston, MA
# 02110-1301, USA.
import signal
import sys
import time
from optparse import OptionParser
from sockaudio import socket_audio
def signal_handler(signal, frame):
audiothread.stop()
sys.exit(0)
parser = OptionParser()
parser.add_option("-O", "--audio-output", type="string", default="default", help="audio output device name")
parser.add_option("-H", "--host-ip", type="string", default="0.0.0.0", help="IP address to bind to")
parser.add_option("-u", "--wireshark-port", type="int", default=23456, help="Wireshark port")
parser.add_option("-2", "--two-channel", action="store_true", default=False, help="single or two channel audio")
parser.add_option("-x", "--audio-gain", type="float", default="1.0", help="audio gain (default = 1.0)")
(options, args) = parser.parse_args()
if len(args) != 0:
parser.print_help()
sys.exit(1)
audiothread = socket_audio(options.host_ip, options.wireshark_port, options.audio_output, options.two_channel, options.audio_gain)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
while True:
time.sleep(1)

@ -36,7 +36,7 @@
],
"devices": [
{
"args": "rtl:0",
"args": "rtl=0",
"frequency": 460100000,
"gains": "lna:49",
"name": "rtl0",

@ -29,6 +29,7 @@ from gnuradio import blocks, audio
from gnuradio.eng_option import eng_option
import numpy as np
from gnuradio import gr
from math import pi
_def_debug = 0
_def_sps = 10
@ -36,17 +37,34 @@ _def_sps = 10
GNUPLOT = '/usr/bin/gnuplot'
FFT_AVG = 0.25
MIX_AVG = 0.15
BAL_AVG = 0.05
FFT_BINS = 512
def degrees(r):
d = 360 * r / (2*pi)
while d <0:
d += 360
while d > 360:
d -= 360
return d
def limit(a,lim):
if a > lim:
return lim
return a
class wrap_gp(object):
def __init__(self, sps=_def_sps):
self.sps = sps
self.center_freq = None
self.center_freq = 0.0
self.relative_freq = 0.0
self.offset_freq = 0.0
self.width = None
self.ffts = ()
self.freqs = ()
self.avg_pwr = np.zeros(FFT_BINS)
self.avg_sum_pwr = 0.0
self.buf = []
self.plot_count = 0
self.last_plot = 0
@ -63,8 +81,23 @@ class wrap_gp(object):
self.gp = subprocess.Popen(args, executable=exe, stdin=subprocess.PIPE)
def kill(self):
self.gp.kill()
self.gp.wait()
try:
self.gp.stdin.close() # closing pipe should cause subprocess to exit
except IOError:
pass
sleep_count = 0
while True: # wait politely, but only for so long
self.gp.poll()
if self.gp.returncode is not None:
break
time.sleep(0.1)
if self.gp.returncode is not None:
break
sleep_count += 1
if (sleep_count & 1) == 0:
self.gp.kill()
if sleep_count >= 3:
break
def set_interval(self, v):
self.plot_interval = v
@ -85,12 +118,9 @@ class wrap_gp(object):
self.buf = []
return consumed
if self.plot_interval and self.last_plot + self.plot_interval > time.time():
return consumed
self.last_plot = time.time()
plots = []
s = ''
plot_size = (320,240)
while(len(self.buf)):
if mode == 'eye':
if len(self.buf) < self.sps:
@ -100,72 +130,132 @@ class wrap_gp(object):
s += 'e\n'
self.buf=self.buf[self.sps:]
plots.append('"-" with lines')
elif mode == 'constellation':
elif mode == 'constellation':
plot_size = (240,240)
self.buf = self.buf[:100]
for b in self.buf:
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
plots.append('"-" with points')
for b in self.buf:
s += '%f\t%f\n' % (b.real, b.imag)
#s += '%f\t%f\n' % (b.real, b.imag)
s += '%f\t%f\n' % (degrees(np.angle(b)), limit(np.abs(b),1.0))
s += 'e\n'
self.buf = []
plots.append('"-" with points')
plots.append('"-" with lines')
elif mode == 'symbol':
for b in self.buf:
s += '%f\n' % (b)
s += 'e\n'
self.buf = []
plots.append('"-" with points')
elif mode == 'fft':
elif mode == 'fft' or mode == 'mixer':
sum_pwr = 0.0
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)
tune_freq = (self.center_freq - self.relative_freq) / 1e6
if self.center_freq and self.width:
self.freqs = ((self.freqs * self.width) + self.center_freq) / 1e6
self.freqs = ((self.freqs * self.width) + self.center_freq + self.offset_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]))
if mode == 'fft':
self.avg_pwr[i] = ((1.0 - FFT_AVG) * self.avg_pwr[i]) + (FFT_AVG * np.abs(self.ffts[i]))
else:
self.avg_pwr[i] = ((1.0 - MIX_AVG) * self.avg_pwr[i]) + (MIX_AVG * np.abs(self.ffts[i]))
s += '%f\t%f\n' % (self.freqs[i], 20 * np.log10(self.avg_pwr[i]))
if (mode == 'mixer') and (self.avg_pwr[i] > 1e-5):
if (self.freqs[i] - self.center_freq) < 0:
sum_pwr -= self.avg_pwr[i]
elif (self.freqs[i] - self.center_freq) > 0:
sum_pwr += self.avg_pwr[i]
self.avg_sum_pwr = ((1.0 - BAL_AVG) * self.avg_sum_pwr) + (BAL_AVG * sum_pwr)
s += 'e\n'
self.buf = []
plots.append('"-" with lines')
elif mode == 'float':
for b in self.buf:
s += '%f\n' % (b)
s += 'e\n'
self.buf = []
plots.append('"-" with lines')
self.buf = []
# FFT processing needs to be completed to maintain the weighted average buckets
# regardless of whether we actually produce a new plot or not.
if self.plot_interval and self.last_plot + self.plot_interval > time.time():
return consumed
self.last_plot = time.time()
filename = None
if self.output_dir:
if self.sequence >= 2:
delete_pathname = '%s/plot-%s-%d.png' % (self.output_dir, mode, self.sequence-2)
if os.access(delete_pathname, os.W_OK):
os.remove(delete_pathname)
h= 'set terminal png\n'
h0= 'set terminal png size %d, %d\n' % (plot_size)
filename = 'plot-%s-%d.png' % (mode, self.sequence)
h0 += 'set output "%s/%s"\n' % (self.output_dir, filename)
self.sequence += 1
h += 'set output "%s/%s"\n' % (self.output_dir, filename)
else:
h= 'set terminal x11 noraise\n'
#background = 'set object 1 circle at screen 0,0 size screen 1 fillcolor rgb"black"\n' #FIXME!
h0= 'set terminal x11 noraise\n'
background = ''
h+= 'set key off\n'
h = 'set key off\n'
if mode == 'constellation':
h+= background
h+= 'set size square\n'
h+= 'set xrange [-1:1]\n'
h+= 'set yrange [-1:1]\n'
h += 'unset border\n'
h += 'set polar\n'
h += 'set angles degrees\n'
h += 'unset raxis\n'
h += 'set object circle at 0,0 size 1 fillcolor rgb 0x0f01 fillstyle solid behind\n'
h += 'set style line 10 lt 1 lc rgb 0x404040 lw 0.1\n'
h += 'set grid polar 45\n'
h += 'set grid ls 10\n'
h += 'set xtics axis\n'
h += 'set ytics axis\n'
h += 'set xtics scale 0\n'
h += 'set xtics ("" 0.2, "" 0.4, "" 0.6, "" 0.8, "" 1)\n'
h += 'set ytics 0, 0.2, 1\n'
h += 'set format ""\n'
h += 'set style line 11 lt 1 lw 2 pt 2 ps 2\n'
h+= 'set title "Constellation"\n'
elif mode == 'eye':
h+= background
h+= 'set yrange [-4:4]\n'
h+= 'set title "Datascope"\n'
elif mode == 'symbol':
h+= background
h+= 'set yrange [-4:4]\n'
elif mode == 'fft':
h+= 'set title "Symbol"\n'
elif mode == 'fft' or mode == 'mixer':
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:
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)
h+= 'set yrange [-100:0]\n'
if mode == 'mixer': # mixer
h+= 'set title "Mixer: balance %3.0f (smaller is better)"\n' % (np.abs(self.avg_sum_pwr * 1000))
else: # fft
h+= 'set title "Spectrum"\n'
if self.center_freq:
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 "Spectrum: tuned to %f Mhz"\n' % arrow_pos
elif mode == 'float':
h+= 'set yrange [-2:2]\n'
h+= 'set title "Oscilloscope"\n'
dat = '%s%splot %s\n%s' % (h0, h, ','.join(plots), s)
self.gp.poll()
if self.gp.returncode is None: # make sure gnuplot is still running
try:
self.gp.stdin.write(dat)
except (IOError, ValueError):
pass
if filename:
self.filename = filename
return consumed
@ -176,6 +266,9 @@ class wrap_gp(object):
def set_relative_freq(self, f):
self.relative_freq = f
def set_offset(self, f):
self.offset_freq = f
def set_width(self, w):
self.width = w
@ -248,9 +341,31 @@ class fft_sink_c(gr.sync_block):
def set_relative_freq(self, f):
self.gnuplot.set_relative_freq(f)
def set_offset(self, f):
self.gnuplot.set_offset(f)
def set_width(self, w):
self.gnuplot.set_width(w)
class mixer_sink_c(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="mixer_sink_c",
in_sig=[np.complex64],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
self.gnuplot.plot(in0, FFT_BINS, mode='mixer')
return len(input_items[0])
def kill(self):
self.gnuplot.kill()
class symbol_sink_f(gr.sync_block):
"""
"""
@ -269,3 +384,22 @@ class symbol_sink_f(gr.sync_block):
def kill(self):
self.gnuplot.kill()
class float_sink_f(gr.sync_block):
"""
"""
def __init__(self, debug = _def_debug):
gr.sync_block.__init__(self,
name="float_sink_f",
in_sig=[np.float32],
out_sig=None)
self.debug = debug
self.gnuplot = wrap_gp()
def work(self, input_items, output_items):
in0 = input_items[0]
self.gnuplot.plot(in0, 2000, mode='float')
return len(input_items[0])
def kill(self):
self.gnuplot.kill()

@ -161,7 +161,10 @@ def post_req(environ, start_response, postdata):
if resp:
resp_msg.append(resp)
continue
msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0)
if d['command'].startswith('settings-'):
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
else:
msg = gr.message().make_from_string(str(d['command']), -2, d['data'], 0)
if my_output_q.full_p():
my_output_q.delete_head_nowait() # ignores result
if not my_output_q.full_p():
@ -245,7 +248,7 @@ class queue_watcher(threading.Thread):
self.callback(msg)
class Backend(threading.Thread):
def __init__(self, options, input_q, output_q, **kwds):
def __init__(self, options, input_q, output_q, init_config=None, **kwds):
threading.Thread.__init__ (self, **kwds)
self.setDaemon(1)
self.keep_running = True
@ -267,10 +270,15 @@ class Backend(threading.Thread):
self.start()
self.subproc = None
self.backend = '%s/%s' % (os.getcwd(), 'rx.py')
self.msg = None
self.q_watcher = queue_watcher(self.input_q, self.process_msg)
if init_config:
d = {'command': 'rx-start', 'data': init_config}
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
self.input_q.insert_tail(msg)
def publish(self, msg):
t = msg.type()
s = msg.to_string()
@ -289,14 +297,28 @@ class Backend(threading.Thread):
return False
def process_msg(self, msg):
def make_command(options):
def make_command(options, config_file):
trunked_ct = [True for x in options._js_config['channels'] if x['trunked']]
total_ct = [True for x in options._js_config['channels']]
if trunked_ct and len(trunked_ct) != len(total_ct):
self.msg = 'no suitable backend found for this configuration'
return None
if not trunked_ct:
self.backend = '%s/%s' % (os.getcwd(), 'multi_rx.py')
opts = [self.backend]
filename = '%s%s.json' % (CFG_DIR, config_file)
opts.append('--config-file')
opts.append(filename)
return opts
types = {'costas-alpha': 'float', 'trunk-conf-file': 'str', 'demod-type': 'str', 'logfile-workers': 'int', 'decim-amt': 'int', 'wireshark-host': 'str', 'gain-mu': 'float', 'phase2-tdma': 'bool', 'seek': 'int', 'ifile': 'str', 'pause': 'bool', 'antenna': 'str', 'calibration': 'float', 'fine-tune': 'float', 'raw-symbols': 'str', 'audio-output': 'str', 'vocoder': 'bool', 'input': 'str', 'wireshark': 'bool', 'gains': 'str', 'args': 'str', 'sample-rate': 'int', 'terminal-type': 'str', 'gain': 'float', 'excess-bw': 'float', 'offset': 'float', 'audio-input': 'str', 'audio': 'bool', 'plot-mode': 'str', 'audio-if': 'bool', 'tone-detect': 'bool', 'frequency': 'int', 'freq-corr': 'float', 'hamlib-model': 'int', 'udp-player': 'bool', 'verbosity': 'int'}
self.backend = '%s/%s' % (os.getcwd(), 'rx.py')
opts = [self.backend]
for k in [ x for x in dir(options) if not x.startswith('_') ]:
kw = k.replace('_', '-')
val = getattr(options, k)
if kw not in types.keys():
print 'make_command: unknown option: %s %s type %s' % (k, val, type(val))
self.msg = 'make_command: unknown option: %s %s type %s' % (k, val, type(val))
return None
elif types[kw] == 'str':
if val:
@ -318,28 +340,41 @@ class Backend(threading.Thread):
if val:
opts.append('--%s' % kw)
else:
print 'make_command: unknown2 option: %s %s type %s' % (k, val, type(val))
self.msg = 'make_command: unknown2 option: %s %s type %s' % (k, val, type(val))
return None
return opts
msg = json.loads(msg.to_string())
if msg['command'] == 'rx-start':
if self.check_subproc():
sys.stderr.write('command failed: subprocess pid %d already active\n' % self.subproc.pid)
self.msg = 'start command failed: subprocess pid %d already active' % self.subproc.pid
return
options = rx_options(msg['data'])
if getattr(options, '_js_config', None) is None:
self.msg = 'start command failed: rx_options: unable to initialize config=%s' % (msg['data'])
return
options.verbosity = self.verbosity
options.terminal_type = 'zmq:tcp:%d' % (self.zmq_port)
cmd = make_command(options)
self.subproc = subprocess.Popen(cmd)
cmd = make_command(options, msg['data'])
if cmd:
self.subproc = subprocess.Popen(cmd)
elif msg['command'] == 'rx-stop':
if not self.check_subproc():
sys.stderr.write('command failed: subprocess not active\n')
self.msg = 'stop command failed: subprocess not active'
return
if msg['data'] == 'kill':
self.subproc.kill()
else:
self.subproc.terminate()
elif msg['command'] == 'rx-state':
d = {}
if self.check_subproc():
d['rx-state'] = 'subprocess pid %d active' % self.subproc.pid
else:
d['rx-state'] = 'subprocess not active, last msg: %s' % self.msg
msg = gr.message().make_from_string(json.dumps(d), -4, 0, 0)
if not self.output_q.full_p():
self.output_q.insert_tail(msg)
def run(self):
while self.keep_running:
@ -357,6 +392,7 @@ class rx_options(object):
filename = '%s%s.json' % (CFG_DIR, name)
if not os.access(filename, os.R_OK):
sys.stderr.write('unable to access config file %s\n' % (filename))
return
config = byteify(json.loads(open(filename).read()))
dev = [x for x in config['devices'] if x['active']][0]
@ -375,14 +411,14 @@ class rx_options(object):
self.sample_rate = dev['rate']
self.plot_mode = chan['plot']
self.phase2_tdma = chan['phase2_tdma']
self.trunk_conf_file = ""
self.trunk_conf_file = filename
self._js_config = config
def http_main():
global my_backend
# command line argument parsing
parser = OptionParser()
parser.add_option("-c", "--config-file", type="string", default=None, help="specify config file name")
parser.add_option("-c", "--config", type="string", default=None, help="config json name, without prefix/suffix")
parser.add_option("-e", "--endpoint", type="string", default="127.0.0.1:8080", help="address:port to listen on (use addr 0.0.0.0 to enable external clients)")
parser.add_option("-v", "--verbosity", type="int", default=0, help="message debug level")
parser.add_option("-p", "--pause", action="store_true", default=False, help="block on startup")
@ -399,7 +435,7 @@ def http_main():
backend_input_q = gr.msg_queue(20)
backend_output_q = gr.msg_queue(20)
my_backend = Backend(options, backend_input_q, backend_output_q)
my_backend = Backend(options, backend_input_q, backend_output_q, init_config=options.config)
server = http_server(input_q, output_q, options.endpoint)
q_watcher = queue_watcher(output_q, lambda msg : my_backend.publish(msg))
backend_q_watcher = queue_watcher(backend_output_q, lambda msg : process_qmsg(msg))

@ -39,6 +39,7 @@ import p25_decoder
from gr_gnuplot import constellation_sink_c
from gr_gnuplot import fft_sink_c
from gr_gnuplot import mixer_sink_c
from gr_gnuplot import symbol_sink_f
from gr_gnuplot import eye_sink_f
@ -61,10 +62,39 @@ def byteify(input): # thx so
return input
class device(object):
def __init__(self, config):
speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000]
def __init__(self, config, tb):
self.name = config['name']
self.sample_rate = config['rate']
self.args = config['args']
self.tb = tb
if config['args'].startswith('audio:'):
self.init_audio(config)
elif config['args'].startswith('file:'):
self.init_file(config)
else:
self.init_osmosdr(config)
def init_file(self, config):
filename = config['args'].replace('file:', '', 1)
src = blocks.file_source(gr.sizeof_gr_complex, filename, repeat = False)
throttle = blocks.throttle(gr.sizeof_gr_complex, config['rate'])
self.tb.connect(src, throttle)
self.src = throttle
self.frequency = config['frequency']
self.offset = config['offset']
def init_audio(self, config):
filename = config['args'].replace('audio:', '')
src = audio.source(self.sample_rate, filename)
gain = 1.0
if config['gains'].startswith('audio:'):
gain = float(config['gains'].replace('audio:', ''))
self.src = blocks.multiply_const_ff(gain)
self.tb.connect(src, self.src)
def init_osmosdr(self, config):
speeds = [250000, 1000000, 1024000, 1800000, 1920000, 2000000, 2048000, 2400000, 2560000]
sys.stderr.write('device: %s\n' % config)
if config['args'].startswith('rtl') and config['rate'] not in speeds:
@ -81,7 +111,6 @@ class device(object):
self.ppm = config['ppm']
self.src.set_sample_rate(config['rate'])
self.sample_rate = config['rate']
self.src.set_center_freq(config['frequency'])
self.frequency = config['frequency']
@ -97,7 +126,12 @@ class channel(object):
if 'symbol_rate' in config.keys():
self.symbol_rate = config['symbol_rate']
self.config = config
self.demod = p25_demodulator.p25_demod_cb(
if dev.args.startswith('audio:'):
self.demod = p25_demodulator.p25_demod_fb(
input_rate = dev.sample_rate,
filter_type = config['filter_type'])
else:
self.demod = p25_demodulator.p25_demod_cb(
input_rate = dev.sample_rate,
demod_type = config['demod_type'],
filter_type = config['filter_type'],
@ -116,7 +150,6 @@ class channel(object):
self.sinks = []
for plot in config['plot'].split(','):
# fixme: allow multiple complex consumers (fft and constellation currently mutually exclusive)
if plot == 'datascope':
assert config['demod_type'] == 'fsk4' ## datascope plot requires fsk4 demod type
sink = eye_sink_f(sps=config['if_rate'] / self.symbol_rate)
@ -127,10 +160,17 @@ class channel(object):
self.demod.connect_float(sink)
self.kill_sink.append(sink)
elif plot == 'fft':
assert config['demod_type'] == 'cqpsk' ## fft plot requires cqpsk demod type
i = len(self.sinks)
self.sinks.append(fft_sink_c())
self.demod.connect_complex('src', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'mixer':
assert config['demod_type'] == 'cqpsk' ## mixer plot requires cqpsk demod type
i = len(self.sinks)
self.sinks.append(mixer_sink_c())
self.demod.connect_complex('mixer', self.sinks[i])
self.kill_sink.append(self.sinks[i])
elif plot == 'constellation':
i = len(self.sinks)
assert config['demod_type'] == 'cqpsk' ## constellation plot requires cqpsk demod type
@ -156,10 +196,12 @@ class rx_block (gr.top_block):
self.devices = []
for cfg in config:
self.device_id_by_name[cfg['name']] = len(self.devices)
self.devices.append(device(cfg))
self.devices.append(device(cfg, self))
def find_device(self, chan):
for dev in self.devices:
if dev.args.startswith('audio:') and chan['demod_type'] == 'fsk4':
return dev
d = abs(chan['frequency'] - dev.frequency)
nf = dev.sample_rate / 2
if d + 6250 <= nf:

@ -77,9 +77,16 @@ class p25_decoder_sink_b(gr.hier_block2):
self.debug = debug
self.dest = dest
do_output = False
do_audio_output = False
do_phase2_tdma = False
if dest == 'wav':
do_output = True
do_audio_output = True
if do_imbe:
do_audio_output = True
if num_ambe > 0:
do_phase2_tdma = True
if msgq is None:
msgq = gr.msg_queue(1)
@ -93,7 +100,7 @@ class p25_decoder_sink_b(gr.hier_block2):
if num_ambe > 1:
num_decoders += num_ambe - 1
for slot in xrange(num_decoders):
self.p25_decoders.append(op25_repeater.p25_frame_assembler(wireshark_host, udp_port, debug, do_imbe, do_output, do_msgq, msgq, do_audio_output, True))
self.p25_decoders.append(op25_repeater.p25_frame_assembler(wireshark_host, udp_port, debug, do_imbe, do_output, do_msgq, msgq, do_audio_output, do_phase2_tdma))
self.p25_decoders[slot].set_slotid(slot)
self.xorhash.append('')
@ -123,6 +130,9 @@ class p25_decoder_sink_b(gr.hier_block2):
return
self.audio_sink[index].open(filename)
def set_nac(self, nac, index=0):
self.p25_decoders[index].set_nac(nac)
def set_xormask(self, xormask, xorhash, index=0):
if self.xorhash[index] == xorhash:
return

@ -229,8 +229,8 @@ class p25_demod_cb(p25_demod_base):
self.lo_freq = 0
self.float_sink = None
self.complex_sink = None
self.if1 = None
self.if2 = None
self.if1 = 0
self.if2 = 0
self.t_cache = {}
if filter_type == 'rrc':
self.set_baseband_gain(0.61)
@ -257,7 +257,7 @@ class p25_demod_cb(p25_demod_base):
sys.stderr.write( 'Unable to use two-stage decimator for speed=%d\n' % (input_rate))
# local osc
self.lo = analog.sig_source_c (input_rate, analog.GR_SIN_WAVE, 0, 1.0, 0)
lpf_coeffs = filter.firdes.low_pass(1.0, input_rate, 7250, 725, filter.firdes.WIN_HANN)
lpf_coeffs = filter.firdes.low_pass(1.0, input_rate, 7250, 1450, filter.firdes.WIN_HANN)
decimation = int(input_rate / if_rate)
self.lpf = filter.fir_filter_ccf(decimation, lpf_coeffs)
resampled_rate = float(input_rate) / float(decimation) # rate at output of self.lpf
@ -306,6 +306,9 @@ class p25_demod_cb(p25_demod_base):
self.set_relative_frequency(relative_freq)
def get_error_band(self):
return int(self.clock.get_error_band())
def get_freq_error(self): # get error in Hz (approx).
return int(self.clock.get_freq_error() * self.symbol_rate)
@ -318,7 +321,7 @@ class p25_demod_cb(p25_demod_base):
self.clock.set_omega(self.sps)
def set_relative_frequency(self, freq):
if abs(freq) > self.input_rate/2:
if abs(freq) > ((self.input_rate / 2) - (self.if1 / 2)):
#print 'set_relative_frequency: error, relative frequency %d exceeds limit %d' % (freq, self.input_rate/2)
return False
if freq == self.lo_freq:
@ -383,34 +386,19 @@ class p25_demod_cb(p25_demod_base):
print 'connect_float: state error', self.connect_state
assert 0 == 1
def disconnect_complex(self):
# assumes lock held or init
if not self.complex_sink:
return
self.disconnect(self.complex_sink[0], self.complex_sink[1])
self.complex_sink = None
def connect_complex(self, src, sink):
# assumes lock held or init
self.disconnect_complex()
if src == 'clock':
self.connect(self.clock, sink)
self.complex_sink = [self.clock, sink]
elif src == 'diffdec':
self.connect(self.diffdec, sink)
self.complex_sink = [self.diffdec, sink]
elif src == 'mixer':
self.connect(self.mixer, sink)
self.complex_sink = [self.mixer, sink]
elif src == 'src':
self.connect(self, sink)
self.complex_sink = [self, sink]
elif src == 'bpf':
self.connect(self.bpf, sink)
self.complex_sink = [self.bpf, sink]
elif src == 'if_out':
self.connect(self.if_out, sink)
self.complex_sink = [self.if_out, sink]
elif src == 'agc':
self.connect(self.agc, sink)
self.complex_sink = [self.agc, sink]

@ -2,7 +2,7 @@
# Copyright 2008-2011 Steve Glass
#
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017 Max H. Parke KA1RBI
# Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Max H. Parke KA1RBI
#
# Copyright 2003,2004,2005,2006 Free Software Foundation, Inc.
# (from radiorausch)
@ -64,6 +64,7 @@ from gr_gnuplot import constellation_sink_c
from gr_gnuplot import fft_sink_c
from gr_gnuplot import symbol_sink_f
from gr_gnuplot import eye_sink_f
from gr_gnuplot import mixer_sink_c
from terminal import op25_terminal
from sockaudio import socket_audio
@ -75,7 +76,7 @@ os.environ['IMBE'] = 'soft'
WIRESHARK_PORT = 23456
_def_interval = 5.0 # sec
_def_interval = 3.0 # sec
_def_file_dir = '../www/images'
# The P25 receiver
@ -96,6 +97,15 @@ class p25_rx_block (gr.top_block):
self.rtl_found = False
self.channel_rate = options.sample_rate
self.fft_sink = None
self.last_error_update = 0
self.error_band = 0
self.tuning_error = 0
self.freq_correction = 0
self.last_set_freq = 0
self.last_set_freq_at = time.time()
self.last_change_freq = 0
self.last_change_freq_at = time.time()
self.last_freq_params = {'freq' : 0.0, 'tgid' : None, 'tag' : "", 'tdma' : None}
self.src = None
if (not options.input) and (not options.audio) and (not options.audio_if):
@ -107,7 +117,7 @@ class p25_rx_block (gr.top_block):
print "osmosdr source_c creation failure"
ignore = True
if "rtl" in options.args.lower():
if any(x in options.args.lower() for x in ['rtl', 'airspy', 'hackrf', 'uhd']):
#print "'rtl' has been found in options.args (%s)" % (options.args)
self.rtl_found = True
@ -175,23 +185,25 @@ class p25_rx_block (gr.top_block):
# configure specified data source
if options.input:
self.open_file(options.input)
elif options.frequency:
self.open_usrp()
elif options.audio_if:
self.open_audio_c(self.channel_rate, options.gain, options.audio_input)
elif options.audio:
self.open_audio(self.channel_rate, options.gain, options.audio_input)
elif options.ifile:
self.open_ifile(self.channel_rate, options.gain, options.ifile, options.seek)
elif (self.rtl_found or options.frequency):
self.open_usrp()
else:
pass
# attach terminal thread
# attach terminal thread and make sure currently tuned frequency is displayed
self.terminal = op25_terminal(self.input_q, self.output_q, self.options.terminal_type)
if self.terminal is None:
sys.stderr.write('warning: no terminal attached\n')
# attach audio thread
if self.options.udp_player:
self.audio = socket_audio("127.0.0.1", WIRESHARK_PORT, self.options.audio_output)
self.audio = socket_audio("127.0.0.1", self.options.wireshark_port, self.options.audio_output, False, self.options.audio_gain)
else:
self.audio = None
@ -206,8 +218,16 @@ class p25_rx_block (gr.top_block):
self.rx_q = gr.msg_queue(100)
udp_port = 0
if self.options.udp_player or self.options.wireshark or (self.options.wireshark_host != "127.0.0.1"):
udp_port = WIRESHARK_PORT
vocoder = self.options.vocoder
wireshark = self.options.wireshark
wireshark_host = self.options.wireshark_host
if self.options.udp_player:
vocoder = True
wireshark = True
wireshark_host = "127.0.0.1"
if wireshark or (wireshark_host != "127.0.0.1"):
udp_port = self.options.wireshark_port
self.tdma_state = False
self.xor_cache = {}
@ -216,7 +236,7 @@ class p25_rx_block (gr.top_block):
self.demod = p25_demodulator.p25_demod_fb(input_rate=capture_rate, excess_bw=self.options.excess_bw)
else: # complex input
# local osc
self.lo_freq = self.options.offset + self.options.fine_tune
self.lo_freq = self.options.offset
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,
@ -233,11 +253,13 @@ class p25_rx_block (gr.top_block):
if self.options.phase2_tdma:
num_ambe = 1
self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=True, num_ambe=num_ambe, wireshark_host=self.options.wireshark_host, udp_port=udp_port, do_msgq = True, msgq=self.rx_q, audio_output=self.options.audio_output, debug=self.options.verbosity)
self.decoder = p25_decoder.p25_decoder_sink_b(dest='audio', do_imbe=vocoder, num_ambe=num_ambe, wireshark_host=wireshark_host, udp_port=udp_port, do_msgq = True, msgq=self.rx_q, audio_output=self.options.audio_output, debug=self.options.verbosity)
# connect it all up
self.connect(source, self.demod, self.decoder)
if self.baseband_input:
sps = int(capture_rate / 4800)
plot_modes = []
if self.options.plot_mode is not None:
plot_modes = self.options.plot_mode.split(',')
@ -254,6 +276,9 @@ class p25_rx_block (gr.top_block):
self.spectrum_decim = filter.rational_resampler_ccf(1, self.options.decim_amt)
self.connect(self.spectrum_decim, sink)
self.demod.connect_complex('src', self.spectrum_decim)
elif plot_mode == 'mixer':
sink = mixer_sink_c()
self.demod.connect_complex('mixer', sink)
elif plot_mode == 'datascope':
assert self.options.demod_type == 'fsk4' ## datascope requires fsk4 demod-type
sink = eye_sink_f(sps=sps)
@ -277,7 +302,7 @@ class p25_rx_block (gr.top_block):
demod = p25_demodulator.p25_demod_cb(input_rate=capture_rate,
demod_type=self.options.demod_type,
offset=self.options.offset)
decoder = p25_decoder.p25_decoder_sink_b(debug = self.options.verbosity, do_imbe = self.options.vocoder, num_ambe=num_ambe)
decoder = p25_decoder.p25_decoder_sink_b(debug = self.options.verbosity, do_imbe = vocoder, num_ambe=num_ambe)
logfile_workers.append({'demod': demod, 'decoder': decoder, 'active': False})
self.connect(source, demod, decoder)
@ -317,20 +342,21 @@ class p25_rx_block (gr.top_block):
def configure_tdma(self, params):
if params['tdma'] is not None and not self.options.phase2_tdma:
print '***TDMA request for frequency %d failed- phase2_tdma option not enabled' % params['freq']
sys.stderr.write("***TDMA request for frequency %d failed- phase2_tdma option not enabled\n" % params['freq'])
return
set_tdma = False
if params['tdma'] is not None:
set_tdma = True
self.decoder.set_slotid(params['tdma'])
if set_tdma == self.tdma_state:
return # already in desired state
self.tdma_state = set_tdma
if set_tdma:
self.decoder.set_slotid(params['tdma'])
hash = '%x%x%x' % (params['nac'], params['sysid'], params['wacn'])
if hash not in self.xor_cache:
self.xor_cache[hash] = lfsr.p25p2_lfsr(params['nac'], params['sysid'], params['wacn']).xor_chars
self.decoder.set_xormask(self.xor_cache[hash], hash)
self.decoder.set_nac(params['nac'])
rate = 6000
else:
rate = 4800
@ -338,36 +364,78 @@ class p25_rx_block (gr.top_block):
self.demod.set_symbol_rate(rate) # this and the foll. call should be merged?
self.demod.clock.set_omega(float(sps))
def error_tracking(self):
UPDATE_TIME = 3
if self.last_error_update + UPDATE_TIME > time.time() \
or self.last_change_freq_at + UPDATE_TIME > time.time():
return
self.last_error_update = time.time()
band = self.demod.get_error_band()
freq_error = self.demod.get_freq_error()
if band:
self.error_band += band
self.freq_correction += freq_error * 0.15
if self.freq_correction > 600:
self.freq_correction -= 1200
self.error_band += 1
elif self.freq_correction < -600:
self.freq_correction += 1200
self.error_band -= 1
self.tuning_error = self.error_band * 1200 + self.freq_correction
e = 0
if self.last_change_freq > 0:
e = (self.tuning_error*1e6) / float(self.last_change_freq)
if self.options.verbosity >= 10:
sys.stderr.write('frequency_tracking\t%d\t%d\t%d\t%d\t%f\n' % (freq_error, self.error_band, self.tuning_error, self.freq_correction, e))
def change_freq(self, params):
self.last_freq_params = params
freq = params['freq']
offset = params['offset']
offset = self.options.offset
center_freq = params['center_frequency']
fine_tune = self.options.fine_tune
self.error_tracking()
self.last_change_freq = freq
self.last_change_freq_at = time.time()
self.configure_tdma(params)
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:
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)
return
if not center_freq:
self.lo_freq = offset + self.tuning_error
self.demod.set_relative_frequency(self.lo_freq)
self.set_freq(freq)
return
relative_freq = center_freq - freq
if abs(relative_freq + offset + self.tuning_error) > self.channel_rate / 2:
self.lo_freq = offset + self.tuning_error # relative tune not possible
self.demod.set_relative_frequency(self.lo_freq) # reset demod relative freq
self.set_freq(freq) # direct tune instead
return
self.lo_freq = relative_freq + offset + self.tuning_error
if self.demod.set_relative_frequency(self.lo_freq): # relative tune successful
self.set_freq(center_freq)
if self.fft_sink:
self.fft_sink.set_relative_freq(self.lo_freq)
return
self.lo_freq = offset + self.tuning_error # relative tune unsuccessful