2022-09-25 22:53:44 +00:00
|
|
|
#!/usr/bin/env python3
|
2017-03-12 06:17:44 +00:00
|
|
|
|
|
|
|
#################################################################################
|
|
|
|
#
|
2020-06-19 01:30:19 +00:00
|
|
|
# Multiprotocol Digital Voice TX (C) Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
|
2017-03-12 06:17:44 +00:00
|
|
|
#
|
|
|
|
# This file is part of OP25
|
|
|
|
#
|
|
|
|
# This 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.
|
|
|
|
#
|
|
|
|
# This software 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 this software; see the file COPYING. If not, write to
|
|
|
|
# the Free Software Foundation, Inc., 51 Franklin Street,
|
|
|
|
# Boston, MA 02110-1301, USA.
|
|
|
|
#################################################################################
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import math
|
2022-05-20 00:21:21 +00:00
|
|
|
from gnuradio import gr, audio, eng_notation
|
2022-09-20 19:40:41 +00:00
|
|
|
from gnuradio import filter, blocks, analog, digital, fft
|
2017-03-12 06:17:44 +00:00
|
|
|
from gnuradio.eng_option import eng_option
|
|
|
|
from optparse import OptionParser
|
|
|
|
|
|
|
|
import op25
|
|
|
|
import op25_repeater
|
|
|
|
|
|
|
|
from math import pi
|
|
|
|
|
2017-10-18 01:34:50 +00:00
|
|
|
from op25_c4fm_mod import p25_mod_bf
|
|
|
|
|
2018-12-26 02:10:06 +00:00
|
|
|
sys.path.append('..')
|
|
|
|
from gr_gnuplot import float_sink_f
|
|
|
|
|
2020-06-19 01:30:19 +00:00
|
|
|
RC_FILTER = {'dmr': 'rrc', 'p25': 'rc', 'ysf': 'rrc', 'dstar': None, 'nxdn48': 'nxdn48', 'nxdn96': 'nxdn96'}
|
2017-10-18 01:34:50 +00:00
|
|
|
|
|
|
|
output_gains = {
|
|
|
|
'dmr': 5.5,
|
|
|
|
'dstar': 0.95,
|
2017-11-10 02:12:03 +00:00
|
|
|
'p25': 4.5,
|
2020-06-19 01:30:19 +00:00
|
|
|
'ysf': 5.5,
|
|
|
|
'nxdn48': 1.0,
|
|
|
|
'nxdn96': 1.0
|
2017-10-18 01:34:50 +00:00
|
|
|
}
|
|
|
|
gain_adjust = {
|
|
|
|
'dmr': 3.0,
|
|
|
|
'dstar': 7.5,
|
2020-06-19 01:30:19 +00:00
|
|
|
'ysf': 4.0,
|
|
|
|
'nxdn48': 1.0,
|
|
|
|
'nxdn96': 1.0
|
2017-10-18 01:34:50 +00:00
|
|
|
}
|
|
|
|
gain_adjust_fullrate = {
|
|
|
|
'p25': 2.0,
|
|
|
|
'ysf': 3.0
|
|
|
|
}
|
|
|
|
mod_adjust = { # rough values
|
|
|
|
'dmr': 0.35,
|
|
|
|
'dstar': 0.075,
|
|
|
|
'p25': 0.33,
|
2020-06-19 01:30:19 +00:00
|
|
|
'ysf': 0.42,
|
|
|
|
'nxdn48': 1.66667,
|
|
|
|
'nxdn96': 1.93
|
2017-10-18 01:34:50 +00:00
|
|
|
}
|
2017-03-12 06:17:44 +00:00
|
|
|
|
|
|
|
class my_top_block(gr.top_block):
|
|
|
|
|
|
|
|
"""
|
|
|
|
Reads up to two channels of input and generates an output stream (in float format)
|
|
|
|
|
|
|
|
Input may be either from sound card or from files.
|
|
|
|
|
|
|
|
Likewise the output channel may be directed to an audio output or to a file.
|
|
|
|
|
|
|
|
the output audio is suitable for direct application to an FM modulator
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
2017-10-18 01:34:50 +00:00
|
|
|
global output_gains, gain_adjust, gain_adjust_fullrate, mod_adjust
|
2017-03-12 06:17:44 +00:00
|
|
|
gr.top_block.__init__(self)
|
|
|
|
parser = OptionParser(option_class=eng_option)
|
|
|
|
|
2017-04-01 21:20:39 +00:00
|
|
|
parser.add_option("-a", "--args", type="string", default="", help="device args")
|
2017-12-23 23:54:48 +00:00
|
|
|
parser.add_option("-A", "--alt-modulator-rate", type="int", default=50000, help="when mod rate is not a submutiple of IF rate")
|
2017-10-05 23:15:17 +00:00
|
|
|
parser.add_option("-b", "--bt", type="float", default=0.5, help="specify bt value")
|
2017-03-12 06:17:44 +00:00
|
|
|
parser.add_option("-c", "--config-file", type="string", default=None, help="specify the config file name")
|
|
|
|
parser.add_option("-f", "--file1", type="string", default=None, help="specify the input file slot 1")
|
|
|
|
parser.add_option("-F", "--file2", type="string", default=None, help="specify the input file slot 2 (DMR)")
|
|
|
|
parser.add_option("-g", "--gain", type="float", default=1.0, help="input gain")
|
2017-11-29 18:16:50 +00:00
|
|
|
parser.add_option("-i", "--if-rate", type="int", default=480000, help="output rate to sdr")
|
2017-03-12 06:17:44 +00:00
|
|
|
parser.add_option("-I", "--audio-input", type="string", default="", help="pcm input device name. E.g., hw:0,0 or /dev/dsp")
|
2017-11-29 18:16:50 +00:00
|
|
|
parser.add_option("-k", "--symbol-sink", type="string", default=None, help="write symbols to file (optional)")
|
2017-04-01 21:20:39 +00:00
|
|
|
parser.add_option("-N", "--gains", type="string", default=None, help="gain settings")
|
2017-03-12 06:17:44 +00:00
|
|
|
parser.add_option("-O", "--audio-output", type="string", default="default", help="pcm output device name. E.g., hw:0,0 or /dev/dsp")
|
|
|
|
parser.add_option("-o", "--output-file", type="string", default=None, help="specify the output file")
|
2020-06-19 01:30:19 +00:00
|
|
|
parser.add_option("-p", "--protocol", type="choice", default=None, choices=('dmr', 'dstar', 'p25', 'ysf', 'nxdn48', 'nxdn96'), help="specify protocol: dmr, dstar, p25, ysf, nxdn48, nxdn96")
|
2017-04-01 21:20:39 +00:00
|
|
|
parser.add_option("-q", "--frequency-correction", type="float", default=0.0, help="ppm")
|
|
|
|
parser.add_option("-Q", "--frequency", type="float", default=0.0, help="Hz")
|
2017-03-12 06:17:44 +00:00
|
|
|
parser.add_option("-r", "--repeat", action="store_true", default=False, help="input file repeat")
|
2018-12-26 02:10:06 +00:00
|
|
|
parser.add_option("-P", "--plot-audio", action="store_true", default=False, help="scope input")
|
2017-03-12 06:17:44 +00:00
|
|
|
parser.add_option("-R", "--fullrate-mode", action="store_true", default=False, help="ysf fullrate")
|
2017-12-23 23:54:48 +00:00
|
|
|
parser.add_option("-s", "--modulator-rate", type="int", default=48000, help="must be submultiple of IF rate - see also -A")
|
2017-11-12 00:59:07 +00:00
|
|
|
parser.add_option("-S", "--alsa-rate", type="int", default=48000, help="sound source/sink sample rate")
|
2017-04-01 21:20:39 +00:00
|
|
|
parser.add_option("-t", "--test", type="string", default=None, help="test pattern symbol file")
|
2017-03-12 06:17:44 +00:00
|
|
|
parser.add_option("-v", "--verbose", type="int", default=0, help="additional output")
|
|
|
|
(options, args) = parser.parse_args()
|
|
|
|
|
|
|
|
max_inputs = 1
|
2017-03-19 21:09:05 +00:00
|
|
|
|
2020-06-19 01:30:19 +00:00
|
|
|
if options.protocol is None:
|
|
|
|
print ('protocol [-p] option missing')
|
2017-04-01 21:20:39 +00:00
|
|
|
sys.exit(0)
|
|
|
|
|
2020-06-19 01:30:19 +00:00
|
|
|
if options.protocol == 'ysf' or options.protocol == 'dmr' or options.protocol == 'dstar' or options.protocol.startswith('nxdn'):
|
|
|
|
assert options.config_file # dstar, dmr, ysf, and nxdn require config file ("-c FILENAME" option)
|
2017-10-18 01:34:50 +00:00
|
|
|
|
2020-06-19 01:30:19 +00:00
|
|
|
output_gain = output_gains[options.protocol]
|
2017-10-18 01:34:50 +00:00
|
|
|
|
|
|
|
if options.test: # input file is in symbols of size=char
|
2017-04-01 21:20:39 +00:00
|
|
|
ENCODER = blocks.file_source(gr.sizeof_char, options.test, True)
|
|
|
|
elif options.protocol == 'dmr':
|
2017-03-12 06:17:44 +00:00
|
|
|
max_inputs = 2
|
|
|
|
ENCODER = op25_repeater.ambe_encoder_sb(options.verbose)
|
|
|
|
ENCODER2 = op25_repeater.ambe_encoder_sb(options.verbose)
|
2017-10-18 01:34:50 +00:00
|
|
|
ENCODER2.set_gain_adjust(gain_adjust['dmr'])
|
2017-03-12 06:17:44 +00:00
|
|
|
DMR = op25_repeater.dmr_bs_tx_bb(options.verbose, options.config_file)
|
|
|
|
self.connect(ENCODER, (DMR, 0))
|
|
|
|
self.connect(ENCODER2, (DMR, 1))
|
|
|
|
elif options.protocol == 'dstar':
|
|
|
|
ENCODER = op25_repeater.dstar_tx_sb(options.verbose, options.config_file)
|
|
|
|
elif options.protocol == 'p25':
|
|
|
|
ENCODER = op25_repeater.vocoder(True, # 0=Decode,True=Encode
|
2017-11-11 03:08:07 +00:00
|
|
|
False, # Verbose flag
|
2017-03-12 06:17:44 +00:00
|
|
|
0, # flex amount
|
|
|
|
"", # udp ip address
|
|
|
|
0, # udp port
|
|
|
|
False) # dump raw u vectors
|
|
|
|
elif options.protocol == 'ysf':
|
|
|
|
ENCODER = op25_repeater.ysf_tx_sb(options.verbose, options.config_file, options.fullrate_mode)
|
2017-10-18 01:34:50 +00:00
|
|
|
if options.fullrate_mode:
|
|
|
|
ENCODER.set_gain_adjust(gain_adjust_fullrate['ysf'])
|
|
|
|
else:
|
|
|
|
ENCODER.set_gain_adjust(gain_adjust['ysf'])
|
2020-06-19 01:30:19 +00:00
|
|
|
elif options.protocol.startswith('nxdn'):
|
|
|
|
ENCODER = op25_repeater.nxdn_tx_sb(options.verbose, options.config_file, options.protocol == 'nxdn96')
|
2017-11-29 18:16:50 +00:00
|
|
|
if options.protocol == 'p25' and not options.test:
|
2017-10-18 01:34:50 +00:00
|
|
|
ENCODER.set_gain_adjust(gain_adjust_fullrate[options.protocol])
|
|
|
|
elif not options.test and not options.protocol == 'ysf':
|
|
|
|
ENCODER.set_gain_adjust(gain_adjust[options.protocol])
|
2017-03-12 06:17:44 +00:00
|
|
|
nfiles = 0
|
|
|
|
if options.file1:
|
|
|
|
nfiles += 1
|
|
|
|
if options.file2 and options.protocol == 'dmr':
|
|
|
|
nfiles += 1
|
2017-04-01 21:20:39 +00:00
|
|
|
if nfiles < max_inputs and not options.test:
|
2017-11-12 00:59:07 +00:00
|
|
|
AUDIO = audio.source(options.alsa_rate, options.audio_input)
|
|
|
|
lpf_taps = filter.firdes.low_pass(1.0, options.alsa_rate, 3400.0, 3400 * 0.1, filter.firdes.WIN_HANN)
|
2017-03-12 22:03:21 +00:00
|
|
|
audio_rate = 8000
|
2017-11-12 00:59:07 +00:00
|
|
|
AUDIO_DECIM = filter.fir_filter_fff (int(options.alsa_rate / audio_rate), lpf_taps)
|
2017-03-12 06:17:44 +00:00
|
|
|
AUDIO_SCALE = blocks.multiply_const_ff(32767.0 * options.gain)
|
|
|
|
AUDIO_F2S = blocks.float_to_short()
|
2017-03-12 22:03:21 +00:00
|
|
|
self.connect(AUDIO, AUDIO_DECIM, AUDIO_SCALE, AUDIO_F2S)
|
2018-12-26 02:10:06 +00:00
|
|
|
if options.plot_audio:
|
|
|
|
PLOT_F = float_sink_f()
|
|
|
|
self.connect(AUDIO, PLOT_F)
|
2017-03-12 06:17:44 +00:00
|
|
|
|
|
|
|
if options.file1:
|
|
|
|
IN1 = blocks.file_source(gr.sizeof_short, options.file1, options.repeat)
|
|
|
|
S2F1 = blocks.short_to_float()
|
|
|
|
AMP1 = blocks.multiply_const_ff(options.gain)
|
|
|
|
F2S1 = blocks.float_to_short()
|
|
|
|
self.connect(IN1, S2F1, AMP1, F2S1, ENCODER)
|
2017-04-01 21:20:39 +00:00
|
|
|
elif not options.test:
|
2017-03-12 06:17:44 +00:00
|
|
|
self.connect(AUDIO_F2S, ENCODER)
|
|
|
|
|
|
|
|
if options.protocol == 'dmr':
|
|
|
|
if options.file2:
|
|
|
|
IN2 = blocks.file_source(gr.sizeof_short, options.file2, options.repeat)
|
|
|
|
S2F2 = blocks.short_to_float()
|
|
|
|
AMP2 = blocks.multiply_const_ff(options.gain)
|
|
|
|
F2S2 = blocks.float_to_short()
|
|
|
|
self.connect(IN2, S2F2, AMP2, F2S2, ENCODER2)
|
|
|
|
else:
|
|
|
|
self.connect(AUDIO_F2S, ENCODER2)
|
|
|
|
|
2017-11-12 00:59:07 +00:00
|
|
|
MOD = p25_mod_bf(output_sample_rate = options.modulator_rate, dstar = (options.protocol == 'dstar'), bt = options.bt, rc = RC_FILTER[options.protocol])
|
2017-03-12 06:17:44 +00:00
|
|
|
AMP = blocks.multiply_const_ff(output_gain)
|
|
|
|
|
|
|
|
if options.output_file:
|
|
|
|
OUT = blocks.file_sink(gr.sizeof_float, options.output_file)
|
2017-04-01 21:20:39 +00:00
|
|
|
elif not options.args:
|
2017-11-12 00:59:07 +00:00
|
|
|
OUT = audio.sink(options.alsa_rate, options.audio_output)
|
2017-03-12 06:17:44 +00:00
|
|
|
|
2017-11-29 18:16:50 +00:00
|
|
|
if options.symbol_sink:
|
|
|
|
SYMBOL_SINK = blocks.file_sink(gr.sizeof_char, options.symbol_sink)
|
2017-04-01 21:20:39 +00:00
|
|
|
if options.protocol == 'dmr' and not options.test:
|
2017-03-12 06:17:44 +00:00
|
|
|
self.connect(DMR, MOD)
|
2017-11-29 18:16:50 +00:00
|
|
|
if options.symbol_sink:
|
|
|
|
self.connect(DMR, SYMBOL_SINK)
|
2017-03-12 06:17:44 +00:00
|
|
|
else:
|
|
|
|
self.connect(ENCODER, MOD)
|
2017-11-29 18:16:50 +00:00
|
|
|
if options.symbol_sink:
|
|
|
|
self.connect(ENCODER, SYMBOL_SINK)
|
2017-03-12 06:17:44 +00:00
|
|
|
|
2017-04-01 21:20:39 +00:00
|
|
|
if options.args:
|
2017-12-23 23:54:48 +00:00
|
|
|
self.setup_sdr_output(options, mod_adjust[options.protocol])
|
2017-11-12 00:59:07 +00:00
|
|
|
f1 = float(options.if_rate) / options.modulator_rate
|
|
|
|
i1 = int(options.if_rate / options.modulator_rate)
|
|
|
|
if f1 - i1 > 1e-3:
|
2017-12-23 23:54:48 +00:00
|
|
|
f1 = float(options.if_rate) / options.alt_modulator_rate
|
|
|
|
i1 = int(options.if_rate / options.alt_modulator_rate)
|
|
|
|
if f1 - i1 > 1e-3:
|
2020-06-19 01:30:19 +00:00
|
|
|
print ('*** Error, sdr rate %d not an integer multiple of alt modulator rate %d - ratio=%f' % (options.if_rate, options.alt_modulator_rate, f1))
|
2017-12-23 23:54:48 +00:00
|
|
|
sys.exit(0)
|
|
|
|
a_resamp = filter.pfb.arb_resampler_fff(options.alt_modulator_rate / float(options.modulator_rate))
|
|
|
|
sys.stderr.write('adding resampler for rate change %d ===> %d\n' % (options.modulator_rate, options.alt_modulator_rate))
|
|
|
|
interp = filter.rational_resampler_fff(options.if_rate / options.alt_modulator_rate, 1)
|
|
|
|
self.connect(MOD, AMP, a_resamp, interp, self.fm_modulator, self.u)
|
|
|
|
else:
|
2020-06-19 01:30:19 +00:00
|
|
|
interp = filter.rational_resampler_fff(options.if_rate // options.modulator_rate, 1)
|
2017-12-23 23:54:48 +00:00
|
|
|
self.connect(MOD, AMP, interp, self.fm_modulator, self.u)
|
2017-04-01 21:20:39 +00:00
|
|
|
else:
|
|
|
|
self.connect(MOD, AMP, OUT)
|
|
|
|
|
|
|
|
def setup_sdr_output(self, options, adjustment):
|
|
|
|
import osmosdr
|
|
|
|
max_dev = 12.5e3
|
|
|
|
k = 2 * math.pi * max_dev / options.if_rate
|
|
|
|
|
|
|
|
self.fm_modulator = analog.frequency_modulator_fc (k * adjustment)
|
|
|
|
|
|
|
|
self.u = osmosdr.sink (options.args)
|
|
|
|
gain_names = self.u.get_gain_names()
|
|
|
|
for name in gain_names:
|
|
|
|
range = self.u.get_gain_range(name)
|
2020-06-19 01:30:19 +00:00
|
|
|
print ("gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step()))
|
2017-04-01 21:20:39 +00:00
|
|
|
if options.gains:
|
|
|
|
for tuple in options.gains.split(","):
|
|
|
|
name, gain = tuple.split(":")
|
|
|
|
gain = int(gain)
|
2020-06-19 01:30:19 +00:00
|
|
|
print ("setting gain %s to %d" % (name, gain))
|
2017-04-01 21:20:39 +00:00
|
|
|
self.u.set_gain(gain, name)
|
|
|
|
|
|
|
|
self.u.set_sample_rate(options.if_rate)
|
|
|
|
self.u.set_center_freq(options.frequency)
|
|
|
|
self.u.set_freq_corr(options.frequency_correction)
|
|
|
|
#self.u.set_bandwidth(options.if_rate)
|
2017-03-12 06:17:44 +00:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2020-06-19 01:30:19 +00:00
|
|
|
print ('Multiprotocol Digital Voice TX (C) Copyright 2017-2020 Max H. Parke KA1RBI')
|
2017-03-12 06:17:44 +00:00
|
|
|
try:
|
|
|
|
my_top_block().run()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
tb.stop()
|