This commit is contained in:
Max 2020-06-18 21:30:19 -04:00
parent 5e01df4883
commit 921a1d8ea0
11 changed files with 96 additions and 48 deletions

View File

@ -2,7 +2,7 @@
#################################################################################
#
# Multiprotocol Digital Voice TX (C) Copyright 2017 Max H. Parke KA1RBI
# Multiprotocol Digital Voice TX (C) Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
#
# This file is part of OP25
#
@ -40,18 +40,22 @@ from op25_c4fm_mod import p25_mod_bf
sys.path.append('..')
from gr_gnuplot import float_sink_f
RC_FILTER = {'dmr': 'rrc', 'p25': 'rc', 'ysf': 'rrc', 'dstar': None}
RC_FILTER = {'dmr': 'rrc', 'p25': 'rc', 'ysf': 'rrc', 'dstar': None, 'nxdn48': 'nxdn48', 'nxdn96': 'nxdn96'}
output_gains = {
'dmr': 5.5,
'dstar': 0.95,
'p25': 4.5,
'ysf': 5.5
'ysf': 5.5,
'nxdn48': 1.0,
'nxdn96': 1.0
}
gain_adjust = {
'dmr': 3.0,
'dstar': 7.5,
'ysf': 4.0
'ysf': 4.0,
'nxdn48': 1.0,
'nxdn96': 1.0
}
gain_adjust_fullrate = {
'p25': 2.0,
@ -61,7 +65,9 @@ mod_adjust = { # rough values
'dmr': 0.35,
'dstar': 0.075,
'p25': 0.33,
'ysf': 0.42
'ysf': 0.42,
'nxdn48': 1.66667,
'nxdn96': 1.93
}
class my_top_block(gr.top_block):
@ -94,7 +100,7 @@ class my_top_block(gr.top_block):
parser.add_option("-N", "--gains", type="string", default=None, help="gain settings")
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")
parser.add_option("-p", "--protocol", type="choice", default=None, choices=('dmr', 'dstar', 'p25', 'ysf'), help="specify protocol: dmr, dstar, p25, ysf")
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")
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")
parser.add_option("-r", "--repeat", action="store_true", default=False, help="input file repeat")
@ -108,14 +114,14 @@ class my_top_block(gr.top_block):
max_inputs = 1
if options.protocol is None:
print 'protocol [-p] option missing'
if options.protocol is None:
print ('protocol [-p] option missing')
sys.exit(0)
if options.protocol == 'ysf' or options.protocol == 'dmr' or options.protocol == 'dstar':
assert options.config_file # dstar, dmr and ysf require config file ("-c FILENAME" option)
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)
output_gain = output_gains[options.protocol]
output_gain = output_gains[options.protocol]
if options.test: # input file is in symbols of size=char
ENCODER = blocks.file_source(gr.sizeof_char, options.test, True)
@ -142,6 +148,8 @@ class my_top_block(gr.top_block):
ENCODER.set_gain_adjust(gain_adjust_fullrate['ysf'])
else:
ENCODER.set_gain_adjust(gain_adjust['ysf'])
elif options.protocol.startswith('nxdn'):
ENCODER = op25_repeater.nxdn_tx_sb(options.verbose, options.config_file, options.protocol == 'nxdn96')
if options.protocol == 'p25' and not options.test:
ENCODER.set_gain_adjust(gain_adjust_fullrate[options.protocol])
elif not options.test and not options.protocol == 'ysf':
@ -209,14 +217,14 @@ class my_top_block(gr.top_block):
f1 = float(options.if_rate) / options.alt_modulator_rate
i1 = int(options.if_rate / options.alt_modulator_rate)
if f1 - i1 > 1e-3:
print '*** Error, sdr rate %d not an integer multiple of alt modulator rate %d - ratio=%f' % (options.if_rate, options.alt_modulator_rate, f1)
print ('*** Error, sdr rate %d not an integer multiple of alt modulator rate %d - ratio=%f' % (options.if_rate, options.alt_modulator_rate, f1))
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:
interp = filter.rational_resampler_fff(options.if_rate / options.modulator_rate, 1)
interp = filter.rational_resampler_fff(options.if_rate // options.modulator_rate, 1)
self.connect(MOD, AMP, interp, self.fm_modulator, self.u)
else:
self.connect(MOD, AMP, OUT)
@ -232,12 +240,12 @@ class my_top_block(gr.top_block):
gain_names = self.u.get_gain_names()
for name in gain_names:
range = self.u.get_gain_range(name)
print "gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step())
print ("gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step()))
if options.gains:
for tuple in options.gains.split(","):
name, gain = tuple.split(":")
gain = int(gain)
print "setting gain %s to %d" % (name, gain)
print ("setting gain %s to %d" % (name, gain))
self.u.set_gain(gain, name)
self.u.set_sample_rate(options.if_rate)
@ -246,7 +254,7 @@ class my_top_block(gr.top_block):
#self.u.set_bandwidth(options.if_rate)
if __name__ == "__main__":
print 'Multiprotocol Digital Voice TX (C) Copyright 2017 Max H. Parke KA1RBI'
print ('Multiprotocol Digital Voice TX (C) Copyright 2017-2020 Max H. Parke KA1RBI')
try:
my_top_block().run()
except KeyboardInterrupt:

View File

@ -2,7 +2,7 @@
#################################################################################
#
# Multiprotocol Digital Voice TX (C) Copyright 2017 Max H. Parke KA1RBI
# Multiprotocol Digital Voice TX (C) Copyright 2017, 2018, 2019, 2020 Max H. Parke KA1RBI
#
# This file is part of OP25
#
@ -73,6 +73,9 @@ class pipeline(gr.hier_block2):
elif protocol == 'ysf':
assert config_file
ENCODER = op25_repeater.ysf_tx_sb(verbose, config_file, fullrate_mode)
elif protocol.startswith('nxdn'):
assert config_file
ENCODER = op25_repeater.nxdn_tx_sb(verbose, config_file, protocol == 'nxdn96')
ENCODER.set_gain_adjust(gain_adjust)
MOD = p25_mod_bf(output_sample_rate = sample_rate, dstar = (protocol == 'dstar'), bt = bt, rc = RC_FILTER[protocol])
@ -89,7 +92,7 @@ class pipeline(gr.hier_block2):
else:
self.connect(self, ENCODER, MOD)
INTERP = filter.rational_resampler_fff(if_rate / sample_rate, 1)
INTERP = filter.rational_resampler_fff(if_rate // sample_rate, 1)
MIXER = blocks.multiply_cc()
LO = analog.sig_source_c(if_rate, analog.GR_SIN_WAVE, if_freq, 1.0, 0)
@ -126,15 +129,21 @@ class my_top_block(gr.top_block):
f1 = float(options.if_rate) / options.modulator_rate
i1 = int(options.if_rate / options.modulator_rate)
if f1 - i1 > 1e-3:
print '*** Error, sdr rate %d not an integer multiple of modulator rate %d - ratio=%f' % (options.if_rate, options.modulator_rate, f1)
print ('*** Error, sdr rate %d not an integer multiple of modulator rate %d - ratio=%f' % (options.if_rate, options.modulator_rate, f1))
sys.exit(1)
protocols = 'dmr p25 dstar ysf'.split()
protocols = 'nxdn48 dmr dstar ysf p25'.split()
start_freq = options.frequency
end_freq = options.frequency + options.if_offset * (len(protocols)-1)
tune_freq = (start_freq + end_freq) // 2
print ('start %d end %d center tune %d' % (start_freq, end_freq, tune_freq))
bw = options.if_offset * len(protocols) + 50000
if bw > options.if_rate:
print '*** Error, a %d Hz band is required for %d channels and guardband.' % (bw, len(protocols))
print '*** Either reduce channel spacing using -o (current value is %d Hz),' % (options.if_offset)
print '*** or increase SDR output sample rate using -i (current rate is %d Hz)' % (options.if_rate)
print ('*** Error, a %d Hz band is required for %d channels and guardband.' % (bw, len(protocols)))
print ('*** Either reduce channel spacing using -o (current value is %d Hz),' % (options.if_offset) )
print ('*** or increase SDR output sample rate using -i (current rate is %d Hz)' % (options.if_rate) )
sys.exit(1)
max_inputs = 1
@ -155,7 +164,7 @@ class my_top_block(gr.top_block):
SUM = blocks.add_cc()
input_repeat = True
for i in xrange(len(protocols)):
for i in range(len(protocols)):
SOURCE = blocks.file_source(gr.sizeof_short, options.file, input_repeat)
protocol = protocols[i]
if (options.fullrate_mode and protocol == 'ysf') or protocol == 'p25':
@ -168,15 +177,19 @@ class my_top_block(gr.top_block):
cfg = 'ysf-cfg.dat'
elif protocols[i] == 'dstar':
cfg = 'dstar-cfg.dat'
elif protocols[i].startswith('nxdn'):
cfg = 'nxdn-cfg.dat'
else:
cfg = None
this_freq = start_freq + i * options.if_offset
if_freq = this_freq - tune_freq
print ('%s\t%d\t%d\t%d' % (protocols[i], this_freq, tune_freq, if_freq))
CHANNEL = pipeline(
protocol = protocols[i],
output_gain = output_gains[protocols[i]],
gain_adjust = gain_adj,
mod_adjust = mod_adjust[protocols[i]],
if_freq = (i - len(protocols)/2) * options.if_offset,
if_freq = if_freq,
if_rate = options.if_rate,
sample_rate = options.modulator_rate,
bt = options.bt,
@ -187,29 +200,30 @@ class my_top_block(gr.top_block):
self.u = osmosdr.sink (options.args)
AMP = blocks.multiply_const_cc(1.0 / float(len(protocols)))
self.setup_sdr_output(options)
self.setup_sdr_output(options, tune_freq)
self.connect(SUM, AMP, self.u)
def setup_sdr_output(self, options):
def setup_sdr_output(self, options, tune_freq):
gain_names = self.u.get_gain_names()
for name in gain_names:
range = self.u.get_gain_range(name)
print "gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step())
print ("gain: name: %s range: start %d stop %d step %d" % (name, range[0].start(), range[0].stop(), range[0].step()))
if options.gains:
for tuple in options.gains.split(","):
name, gain = tuple.split(":")
gain = int(gain)
print "setting gain %s to %d" % (name, gain)
print ("setting gain %s to %d" % (name, gain))
self.u.set_gain(gain, name)
print 'setting sample rate'
print ('setting sample rate %d' % options.if_rate)
self.u.set_sample_rate(options.if_rate)
self.u.set_center_freq(options.frequency)
print ('setting SDR tuning frequency %d' % tune_freq)
self.u.set_center_freq(tune_freq)
self.u.set_freq_corr(options.frequency_correction)
if __name__ == "__main__":
print 'Multiprotocol Digital Voice TX (C) Copyright 2017 Max H. Parke KA1RBI'
print ('Multiprotocol Digital Voice TX (C) Copyright 2017-2020 Max H. Parke KA1RBI')
try:
my_top_block().run()
except KeyboardInterrupt:

View File

@ -159,7 +159,7 @@ class gmsk_taps(object):
self.span = span
self.bt = bt
self.samples_per_symbol = self.sample_rate / self.symbol_rate
self.samples_per_symbol = self.sample_rate // self.symbol_rate
self.ntaps = self.span * self.samples_per_symbol
def generate(self):
@ -216,7 +216,10 @@ class p25_mod_bf(gr.hier_block2):
gr.io_signature(1, 1, gr.sizeof_char), # Input signature
gr.io_signature(1, 1, gr.sizeof_float)) # Output signature
input_sample_rate = 4800 # P25 baseband symbol rate
input_sample_rate = 4800 # P25/ysf/dmr/dstar baseband symbol rate
if rc == 'nxdn48':
input_sample_rate = 2400 # only exception is nxdn48 = 2400 rate
intermediate_rate = 48000
self._interp_factor = intermediate_rate / input_sample_rate
@ -235,13 +238,15 @@ class p25_mod_bf(gr.hier_block2):
self.generator = generator
assert rc is None or rc == 'rc' or rc == 'rrc'
assert rc is None or rc == 'rc' or rc == 'rrc' or rc.startswith('nxdn')
if rc:
coeffs = filter.firdes.root_raised_cosine(1.0, intermediate_rate, input_sample_rate, 0.2, 91)
if rc == 'rc':
coeffs = c4fm_taps(sample_rate=intermediate_rate).generate()
elif self.dstar:
coeffs = gmsk_taps(sample_rate=intermediate_rate, bt=self.bt).generate()
elif rc.startswith('nxdn'):
coeffs = c4fm_taps(sample_rate=intermediate_rate, generator=transfer_function_nxdn_tx, symbol_rate=input_sample_rate).generate()
elif not rc:
coeffs = c4fm_taps(sample_rate=intermediate_rate, generator=self.generator).generate()
self.filter = filter.interp_fir_filter_fff(self._interp_factor, coeffs)

View File

@ -29,5 +29,6 @@ install(FILES
ambe_encoder_sb.h
ysf_tx_sb.h
dstar_tx_sb.h
nxdn_tx_sb.h
fsk4_slicer_fb.h DESTINATION include/op25_repeater
)

View File

@ -28,6 +28,7 @@ list(APPEND op25_repeater_sources
ambe_encoder_sb_impl.cc
dmr_bs_tx_bb_impl.cc
ysf_tx_sb_impl.cc
nxdn_tx_sb_impl.cc
dstar_tx_sb_impl.cc
vocoder_impl.cc
gardner_costas_cc_impl.cc

View File

@ -121,7 +121,7 @@ void nxdn_descramble(uint8_t dibits[], int len) {
}
}
static inline void decode_cac(const uint8_t dibits[], int len) {
static inline void decode_cac(const uint8_t dibits[], int len, uint8_t*answer, int*answer_len) {
uint8_t cacbits[300];
uint8_t deperm[300];
uint8_t depunc[350];
@ -151,13 +151,25 @@ static inline void decode_cac(const uint8_t dibits[], int len) {
}
trellis_decode(decode, depunc, 171);
crc = crc16(decode, 171, 0xc3ee);
if (crc != 0)
if (crc != 0) {
*answer_len = 0;
return; // ignore msg if crc failed
}
uint8_t msg_type = load_i(decode+8, 8) & 0x3f;
// result length after crc and 3 zero bits removed = 152 = 19 bytes
for (int i=0; i<19; i++) {
uint8_t c = 0;
for (int j=0; j<8; j++) {
c = (c << 1) | (decode[(i*8)+j] & 1);
}
answer[i] = c;
}
assert (*answer_len >= 19);
*answer_len = 19; /* return 19 bytes */
// todo: forward CAC message
}
void nxdn_frame(const uint8_t dibits[], int ndibits) {
void nxdn_frame(const uint8_t dibits[], int ndibits, uint8_t*answer, int*answer_len) {
uint8_t descrambled[182];
uint8_t lich;
uint8_t lich_test;
@ -171,6 +183,8 @@ void nxdn_frame(const uint8_t dibits[], int ndibits) {
lich |= (descrambled[i] >> 1) << (7-i);
/* todo: parity check & process LICH */
if (lich >> 1 == 0x01)
decode_cac(descrambled+8, 150);
decode_cac(descrambled+8, 150, answer, answer_len);
else
*answer_len = 0;
/* todo: process E: 12 dibits at descrambed+158; */
}

View File

@ -22,7 +22,7 @@
#ifndef INCLUDED_NXDN_H
#define INCLUDED_NXDN_H
void nxdn_frame(const uint8_t dibits[], int ndibits);
void nxdn_frame(const uint8_t dibits[], int ndibits, uint8_t*answer, int*answer_len);
void nxdn_descramble(uint8_t dibits[], int len);
#endif /* INCLUDED_NXDN_H */

View File

@ -27,6 +27,7 @@
/* postamble + frame sync (FS) */
static const uint64_t NXDN_POSTFS_SYNC_MAGIC = 0x5775fdcdf59LL;
/* frame sync + scrambled rendition of LICH=0x6e (a halfrate voice 4V) */
static const uint64_t NXDN_CONV_SYNC_MAGIC = 0xcdf59d5dfLL;
static const uint64_t NXDN_FS6E_SYNC_MAGIC = 0xcdf5975d7LL;
#endif /* INCLUDED_NXDN_CONST_H */

View File

@ -274,12 +274,11 @@ static const int MAX_OUT = 1;
d_verbose_flag(verbose_flag),
d_config_file(config_file),
d_nxdn96_mode(nxdn96_mode),
d_output_amount(0),
d_output_amount((nxdn96_mode) ? 384 : 192);
d_sacch_seq(0),
d_lich(0),
d_ran(0)
{
d_output_amount = (nxdn96_mode) ? 384 : 192;
set_output_multiple(d_output_amount);
memset(d_acch, 0, sizeof(d_acch));
config();

View File

@ -458,6 +458,8 @@ void rx_sync::rx_sym(const uint8_t sym)
uint8_t tmpcw[144];
bool ysf_fullrate;
uint8_t dbuf[182];
int answer_len=0;
uint8_t answer[128];
d_symbol_count ++;
d_sync_reg = (d_sync_reg << 2) | (sym & 3);
@ -540,16 +542,15 @@ void rx_sync::rx_sym(const uint8_t sym)
}
break;
case RX_TYPE_NXDN_CAC:
nxdn_frame(symbol_ptr+22, 170);
answer_len = sizeof(answer);
nxdn_frame(symbol_ptr+22, 170, answer, &answer_len);
break;
case RX_TYPE_NXDN_EHR:
memcpy(dbuf, symbol_ptr+10, sizeof(dbuf));
nxdn_descramble(dbuf, sizeof(dbuf));
// todo: process SACCH
codeword(dbuf+38+36*0, CODEWORD_NXDN_EHR, 0);
codeword(dbuf+38+36*1, CODEWORD_NXDN_EHR, 0);
codeword(dbuf+38+36*2, CODEWORD_NXDN_EHR, 0);
codeword(dbuf+38+36*3, CODEWORD_NXDN_EHR, 0);
for (int vcw = 0; vcw < 4; vcw++)
codeword(dbuf+38+36*vcw, CODEWORD_NXDN_EHR, 0);
break;
case RX_N_TYPES:
assert(0==1); /* should not occur */

View File

@ -16,6 +16,7 @@
#include "op25_repeater/ambe_encoder_sb.h"
#include "op25_repeater/dmr_bs_tx_bb.h"
#include "op25_repeater/ysf_tx_sb.h"
#include "op25_repeater/nxdn_tx_sb.h"
#include "op25_repeater/dstar_tx_sb.h"
%}
@ -28,6 +29,9 @@ GR_SWIG_BLOCK_MAGIC2(op25_repeater, dmr_bs_tx_bb);
%include "op25_repeater/ysf_tx_sb.h"
GR_SWIG_BLOCK_MAGIC2(op25_repeater, ysf_tx_sb);
%include "op25_repeater/nxdn_tx_sb.h"
GR_SWIG_BLOCK_MAGIC2(op25_repeater, nxdn_tx_sb);
%include "op25_repeater/dstar_tx_sb.h"
GR_SWIG_BLOCK_MAGIC2(op25_repeater, dstar_tx_sb);