diff --git a/utils/gmr1_process_recording.py b/utils/gmr1_process_recording.py new file mode 100755 index 0000000..175e452 --- /dev/null +++ b/utils/gmr1_process_recording.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +# +# (C) 2011-2019 by Sylvain Munaut +# All Rights Reserved +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from collections import namedtuple + +import datetime +import os +import sys +import re + + +EXEC_GMR1_DEMOD = 'gmr1_rx_live' +EXEC_GMR1_SPLIT = 'gmr1_rx_sdr.py' + + + +CHAN_BW = 31.25e3 + +Recording = namedtuple('Recording', 'center samplerate timestamp') + +def parse_filename(fn): + m = re.match('^.*-f([0-9\.e+-]*)-s([0-9\.e+-]*)-t([0-9]{14})\.cfile$', fn) + if not m: + return m + + return Recording( + float(m.group(1)), + float(m.group(2)), + datetime.datetime.strptime(m.group(3), '%Y%m%d%H%M%S'), + ) + +def arfcn_to_freq(arfcn, band='L'): + base = { + 'L': 1525e6, + 'S': 2170e6 + 15.625e3, + } + return base[band] + 31.25e3 * arfcn + +def arfcn_fifo(arfcn): + return "/tmp/arfcn_%d.cfile" % arfcn + + +def main(argv0, capture_fn): + + # Parse filename + p = parse_filename(capture_fn) + + # Derive upper and lower band + ll = p.center - p.samplerate / 2 + CHAN_BW + ul = p.center + p.samplerate / 2 - CHAN_BW + + # Select band + band = 'S' if ul > 2e9 else 'L' + n_arfcns = { + 'L': 1087, + 'S': 960, + } + + # List all visible arfcns + visible_arfcns = [x for x in range(0,n_arfcns[band]+1) if ll <= arfcn_to_freq(x, band) <= ul] + + # Create all FIFOs + #for arfcn in visible_arfcns: + # if os.path.exists(arfcn_fifo(arfcn)): + # print "Output FIFO already exists, aborting !" + # return -1 + + #for arfcn in visible_arfcns: + # os.mkfifo(arfcn_fifo(arfcn)) + + # + exec_split = [ + EXEC_GMR1_SPLIT, + '-s', '%f' % p.samplerate, + '-f', '%f' % p.center, + '-B', band, + '--args', 'file=%s,freq=%f,rate=%f,throttle=false,repeat=false' % (capture_fn, p.center, p.samplerate), + ] + + for arfcn in visible_arfcns: + exec_split.extend(['-a', '%d' % arfcn]) + + print ' '.join(exec_split) + + # + exec_rx = [ + EXEC_GMR1_DEMOD, '4', + ] + + for arfcn in visible_arfcns: + exec_rx.append('%d:%s' % (arfcn, arfcn_fifo(arfcn))) + + print ' '.join(exec_rx) + + # Cleanup all FIFOs + #for arfcn in visible_arfcns: + # os.unlink(arfcn_fifo(arfcn)) + + +if __name__ == '__main__': + main(*sys.argv) diff --git a/utils/gmr1_rx_sdr.py b/utils/gmr1_rx_sdr.py new file mode 100755 index 0000000..a75dfe4 --- /dev/null +++ b/utils/gmr1_rx_sdr.py @@ -0,0 +1,1122 @@ +#!/usr/bin/env python + +# +# (C) 2011-2019 by Sylvain Munaut +# All Rights Reserved +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +# Call XInitThreads as the _very_ first thing. +# After some Qt import, it's too late +import ctypes +import sys +if sys.platform.startswith('linux'): + try: + x11 = ctypes.cdll.LoadLibrary('libX11.so') + x11.XInitThreads() + except: + print "Warning: failed to XInitThreads()" + +# Standard lib imports +import argparse +import math + +# Try to import UI +try: + from PyQt4 import Qt + from distutils.version import StrictVersion + import sip + from gnuradio import fosphor + QT_AVAILABLE = True + +except ImportError: + QT_AVAILABLE = False + +# GNURadio +from gnuradio import blocks +from gnuradio import eng_notation +from gnuradio import filter +from gnuradio import gr +from gnuradio.filter import firdes +from gnuradio.filter import pfb +from gnuradio.fft import window + +import osmosdr + + +# ---------------------------------------------------------------------------- +# Utils +# ---------------------------------------------------------------------------- + +def indent(txt, n=1): + return '\n'.join(['\t'*n + l for l in txt.splitlines()]) + + +# ---------------------------------------------------------------------------- +# Channel description +# ---------------------------------------------------------------------------- + +class Channel(object): + + BASE_BANDWIDTH = 31.25e3 + BASE_SYMRATE = 23.4e3 + + def __init__(self, arfcn, width=1, uplink=False, band='L'): + if width not in (1,2,3,5): + raise ValueError("Invalid channel width") + if band not in ('L', 'S'): + raise ValueError("Invalid frequency band") + + if isinstance(arfcn, basestring): + if arfcn[0] == 'U': + uplink = True + arfcn = arfcn[1:] + + if 'x' in arfcn: + width = int(arfcn.split('x')[1]) + arfcn = arfcn.split('x')[0] + + arfcn = int(arfcn) + + self._arfcn = arfcn + self._width = width + self._uplink = uplink + self.band = band # Use setter + + def __repr__(self): + pfx = 'U' if self._uplink else '' + sfx = ('x%d' % self._width) if (self._width > 1) else '' + return '%s%d%s' % (pfx, self._arfcn, sfx) + + @property + def band(self): + return self._band + + @band.setter + def band(self, band): + if band not in ('L', 'S'): + raise ValueError("Invalid frequency band") + self._band = band + + if self._band == 'L': + self._base_ul = 1626.5e6 + self._base_dl = 1525e6 + + elif self._band == 'S': + self._base_ul = 1980e6 + 15.625e3 + self._base_dl = 2170e6 + 15.625e3 + + @property + def arfcn(self): + return self._arfcn + + @property + def arfcns(self): + return range(self.arfcn - (self.width-1)//2, self.arfcn + (self.width+2)//2) + + @property + def width(self): + return self._width + + @property + def uplink(self): + return self._uplink + + @property + def frequency(self): + base_freq = self._base_ul if self._uplink else self._base_dl + return base_freq + Channel.BASE_BANDWIDTH * (self._arfcn + 0.5 * ((self._width ^ 1) & 1)) + + @property + def bandwidth(self): + return Channel.BASE_BANDWIDTH * self._width + + @property + def symbol_rate(self): + return Channel.BASE_SYMRATE * self._width + + @property + def subchannels(self): + return [ + Channel(sa, 1) + for sa in range( + self.arfcn - (self.width-1) // 2, + self.arfcn + (self.width+2) // 2 + ) + ] + + @classmethod + def align_freq(kls, freq): + bases = [ + 1525e6, # DL L-band + 1626.5e6, # UL L-band + 1980e6 + 15.625e3, # UL S-band + 2170e6 + 15.625e3 # DL S-band + ] + + base_freq = min([(abs(b-freq), b) for b in bases])[1] + chan = round((freq - base_freq) / kls.BASE_BANDWIDTH) + return base_freq + chan * kls.BASE_BANDWIDTH + + +# ---------------------------------------------------------------------------- +# Arguments parsing +# ---------------------------------------------------------------------------- + +class PerChannelArgType(object): + + def __init__(self, type_func): + self._type_func = type_func + + def __call__(self, val): + if isinstance(val, basestring) and ('/' in val): + val = val.split('/') + return (int(val[0]), self._type_func(val[1])) + else: + return (None, self._type_func(val)) + + +def gain_type(val): + if ':' not in val: + return (None, float(val)) + else: + val = val.split(':') + return (val[0], float(val[1])) + + +class AttrDictWithFallback(dict): + + def __init__(self, *args, **kwargs): + self.__dict__['_fallback'] = kwargs.pop('_fallback', {}) + super(AttrDictWithFallback, self).__init__(*args, **kwargs) + + def __getattr__(self, key, *args): + return self[key] if (key in self) else self._fallback.get(key, *args) + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + del self[key] + + + +def args_parse_raw(): + # Create parser + parser = argparse.ArgumentParser() + + # Global options + gp = parser.add_argument_group('Global options') + + gp.add_argument( + "--args", + dest="args", + metavar="ARGS", + default="", + type=str, + help="Arguments to the osmosdr source" + ) + + gp.add_argument( + "-s", "--samp-rate", + dest="samp_rate", + metavar="SAMP_RATE", + type=eng_notation.str_to_num, + help="Set samp_rate", + required=True + ) + + gp.add_argument( + "-a", "--arfcn", + dest="arfcns", + metavar="ARFCN", + type=Channel, + action="append", + help="Add an ARFCN to listen to", + required=True + ) + + gp.add_argument( + "-B", "--band", + dest="band", + metavar="BAND", + default="L", + type=str, + choices=("L", "S"), + help="Select operating band (L-band / S-band)", + required=True + ) + + gp.add_argument( + "-t", "--time", + dest="time", + metavar="SEC", + type=float, + help="Set the time to record", + ) + + gp.add_argument( + "-q", "--qt", + dest="qt", + action='store_true', + help="Enable Qt UI", + ) + + gp.add_argument( + "-o", "--output", + dest="output", + metavar="TEMPLATE", + default="/tmp/arfcn_%s.cfile", + type=str, + help="Output filename template", + ) + + gp.add_argument( + "-p", "--pfb", + dest="pfb", + action="store_true", + help="Use PFB topology instead of independent DDCs", + ) + + # Per input channel options + pcp = parser.add_argument_group('Per input channel options', + "Use the 'n/' prefix to specify to which input channel to apply. " + + "Non prefixed value will apply to all channels") + + pcp.add_argument( + "-f", "--center-freq", + dest="center_freq", + metavar="FREQ", + type=PerChannelArgType(eng_notation.str_to_num), + default=[], + action="append", + help="Set center_freq", + required=True + ) + + pcp.add_argument( + "-g", "--gain", + dest="gain", + metavar="GAIN", + type=PerChannelArgType(gain_type), + default=[], + action="append", + help="Set gain to the osmosdr source" + ) + + pcp.add_argument( + "--corr", + dest="corr", + metavar="PPM", + type=PerChannelArgType(float), + default=[], + action="append", + help="Set correction factor in PPM" + ) + + pcp.add_argument( + "-b", "--bw", + dest="bw", + metavar="BW_HZ", + type=PerChannelArgType(eng_notation.str_to_num), + default=[], + action="append", + help="Select the filter bandwidth" + ) + + return parser.parse_args() + + +def args_parse(): + # Grab raw arguments + raw = args_parse_raw() + + # Post process + ga = ['args', 'samp_rate', 'arfcns', 'band', 'time', 'qt', 'output', 'pfb'] + pca = ['center_freq', 'gain', 'corr', 'bw'] + + # Global: Just copy + gad = AttrDictWithFallback() + for k in ga: + gad[k] = getattr(raw, k) + + # Per-Channel: Group in dict + pcad = { None: AttrDictWithFallback() } + for k in pca: + if k == 'gain': + for ci, v in (getattr(raw, k) or []): + pcad.setdefault(ci, AttrDictWithFallback(_fallback=pcad[None])).setdefault(k,{})[v[0]] = v[1] + else: + for ci, v in (getattr(raw, k) or []): + pcad.setdefault(ci, AttrDictWithFallback(_fallback=pcad[None]))[k] = v + + # Gain: Transform to dict and handle fallback mix + if 'gain' in pcad[None]: + fgs = pcad[None]['gain'] + for k,v in pcad.iteritems(): + if k is None: + continue + if 'gain' not in v: + continue + for l,w in fgs.iteritems(): + if l not in v['gain']: + v['gain'][l] = w + + # Qt check + if gad.qt and not QT_AVAILABLE: + print "Qt UI not available" + gad.qt = False + + # Return value + gad.channel = pcad + return gad + + +# ---------------------------------------------------------------------------- +# PFB Channelizer mode +# ---------------------------------------------------------------------------- + +class PFBBase(gr.hier_block2): + + def __init__(self, center_freq, samp_rate, chan_width, chan_align_fn, need_Nx=False, sps=4): + # Pre-compute params + # Grid alignement + mid_center_freq = chan_align_fn(center_freq) + + if abs(mid_center_freq - center_freq) > 200: + self.rotation = 2.0 * math.pi * (self.center_freq - new_center_freq) / samp_rate + else: + self.rotation = 0 + + # Save pfb alignement data + self.pfb_center_freq = mid_center_freq + self.pfb_chan_width = chan_width + + # Channel count (must be even !) + self.n_chans = (int(math.ceil(samp_rate / chan_width)) + 1) & ~1 + + # Resampling + self.resamp = (self.n_chans * chan_width) / samp_rate + + if abs(self.resamp - 1.0) < 1e-5: + self.resamp = 1.0 + mid_samp_rate = samp_rate + else: + mid_samp_rate = (math.ceil(self.samp_rate / chan_width) * chan_width) + + # PFB taps + if need_Nx: + # Need multiple width channels, so we need a filter supporting perfect reconstruction ! + self.taps = firdes.low_pass_2( + 1.0, + self.n_chans, + 0.5, + 0.2, + 80, + firdes.WIN_BLACKMAN_HARRIS + ) + else: + # Use a looser filter to reduce CPU + self.taps = firdes.low_pass( + 1.0, + mid_samp_rate, + chan_width * 0.50, + chan_width * 0.25, + ) + + # Super + gr.hier_block2.__init__(self, + "OutputBranch", + gr.io_signature(1,1,gr.sizeof_gr_complex), + gr.io_signature(self.n_chans,self.n_chans,gr.sizeof_gr_complex) + ) + prev = self + + # Pre-rotation + if self.rotation: + self.rotator = blocks.rotator_cc(self.rotation) + self.connect((prev, 0), (self.rotator, 0)) + prev = self.rotator + + # Pre-resampling + if self.resamp != 1: + self.resamp = pfb.arb_resampler_ccf( + self.resamp, + taps = None, + flt_size = 32 + ) + self.connect( (prev, 0), (self.resamp, 0) ) + prev = self.resamp + + # Channelizer + self.channelizer = pfb.channelizer_ccf( + self.n_chans, + self.taps, + 2, + 100 + ) + self.connect( (prev, 0), (self.channelizer, 0) ) + + # Link all outputs + for i in range(self.n_chans): + self.connect( (self.channelizer, i), (self, i) ) + + def describe(self): + return '\n'.join([ + "Channelize pre-rotation : %s" % (("%f rad/sample" % self.rotation) if (self.rotation != 0) else "None"), + "Channelize pre-resample : %s" % (("%f" % self.resamp) if (self.resamp != 1) else "None"), + "Channelize # channels : %d" % self.n_chans, + "Channelize taps : %d" % len(self.taps), + ]) + + def freq2index(self, freq): + idx = int(round((freq - self.pfb_center_freq) / self.pfb_chan_width)) + if (idx >= (self.n_chans / 2)) or (idx <= -(self.n_chans / 2)): + return None + elif idx < 0: + idx += self.n_chans + return idx + + +class PFBOutputParameters(object): + + OVERSAMPLE = 2 # Each channel rate is in fact oversamples by 2x internally + + def __init__(self, width, chan_width, sym_rate, sps): + # Save params + self.width = width + + # Synthesizer (always need even # of channels for 2x oversampling) + if width > 1: + self.width_synth = ((width + 1) & ~1) + self.taps_synth = firdes.low_pass_2( + 1.0, + self.width_synth, + 0.5, + 0.2, + 80, + firdes.WIN_BLACKMAN_HARRIS + ) + self.rotation = - math.pi * ((self.width-1) / (2.0 * self.width_synth)) + chan_rate = chan_width * self.width_synth / self.width + + else: + self.width_synth = None + self.taps_synth = None + self.rotation = 0 + chan_rate = chan_width + + # Resampler + self.resamp = (sym_rate * sps) / (chan_rate * self.OVERSAMPLE) + self.taps_resamp = firdes.root_raised_cosine( + 32.0, + 32.0 * chan_rate * self.OVERSAMPLE, + sym_rate, + 0.35, + int(11.0 * 32 * chan_rate * self.OVERSAMPLE / sym_rate) + ) + + def describe(self): + return '\n'.join([ + "Width : %d" % self.width, + "Synthesizer : %s" % (("%d chans, %d taps" % (self.width_synth, len(self.taps_synth))) if self.width > 1 else "None"), + "Resampling : %f [%d taps, 32 filters]" % (self.resamp, len(self.taps_resamp)), + "Min Delay : %f" % (self.min_delay(),), + ]) + + def min_delay(self): + if self.width > 1: + return ( + (len(self.taps_synth) / (2.0 * self.width_synth)) + + (len(self.taps_resamp) / (2.0 * 32.0 * self.width_synth)) + ) + else: + return ( + len(self.taps_resamp) / (2.0 * 32.0) + ) + + def adjust_delay(self, delay): + self.delay = delay - self.min_delay() + + +class PFBOutputBranch(gr.hier_block2): + + def __init__(self, params, filename): + # Super + gr.hier_block2.__init__(self, + "PFBOutputBranch", + gr.io_signature(params.width,params.width,gr.sizeof_gr_complex), + gr.io_signature(0,0,0) + ) + prev = self + + # Synthesizer + if params.width > 1: + self.synth = filter.pfb_synthesizer_ccf( + params.width_synth, + params.taps_synth, + True # 2x oversample + ) + for i in range(params.width): + self.connect( (prev, i), (self.synth, i) ) + prev = self.synth + + # Delay + if params.delay: + self.delay = blocks.delay( + gr.sizeof_gr_complex, + int(round(params.delay * params.width_synth)), + ) + self.connect( (prev, 0), (self.delay, 0) ) + prev = self.delay + + # Post synth rotation + if params.rotation != 0: + self.rotator = blocks.rotator_cc(params.rotation) + self.connect( (prev, 0), (self.rotator, 0) ) + prev = self.rotator + + # PFB Arb Resampler + if params.resamp != 1: + self.resamp = pfb.arb_resampler_ccf( + params.resamp, params.taps_resamp, + flt_size = 32 + ) + self.connect( (prev, 0), (self.resamp, 0) ) + prev = self.resamp + + # Output file + self.sink = blocks.file_sink(gr.sizeof_gr_complex, filename, False) + self.connect( (prev, 0), (self.sink, 0) ) + + +# ---------------------------------------------------------------------------- +# Direct mode +# ---------------------------------------------------------------------------- + +class DirectOutputParameters(object): + + def __init__(self, samp_rate, sym_rate, sps): + # Save input rate + self.samp_rate = samp_rate + self.sym_rate = sym_rate + self.sps = sps + + # Select the decimation and resampling ratio + self._select_decim() + + # Generate the taps + self._generate_taps() + + # Default is no delay + self.delay = 0 + + def describe(self): + return '\n'.join([ + "Decimation 1: %d [%d taps]" % (self.decim1, len(self.taps1)), + "Decimation 2: %d [%d taps]" % (self.decim2, len(self.taps2)), + "Resampling rate: %f [%d taps, 32 filters]" % (self.resamp, len(self.taps_resamp)), + "Min Delay : %f\n" % (self.min_delay(),), + ]) + + def min_delay(self): + return ( + (len(self.taps1) / 2.0) + + (len(self.taps2) / 2.0) * self.decim1 + + (len(self.taps_resamp) / (2.0 * 32.0)) * (self.decim1 * self.decim2) + ) + + def adjust_delay(self, delay): + self.delay = delay - self.min_delay() + + def _factor(self, decim): + d_ideal = int(round(math.sqrt(decim))) + for i in range(d_ideal, 1, -1): + if (decim % i) == 0: + return [ decim // i, i ] + return [ decim ] + + def _score(self, factors): + # If single factor, prefer larger + if len(factors) == 1: + return factors[0] + + # If two factor, balance larger first decim and 'squareness' + return (factors[0] * factors[0] * factors[1]) / (1 + (1.0 * factors[0] / factors[1])) + + def _select_decim(self): + # Handle the 'exact' case + if (self.samp_rate % (self.sym_rate * self.sps)) == 0: + decim = int(self.samp_rate / (self.sym_rate * self.sps)) + factors = self._factor(decim) + return (factors + [1, 1])[0:3] + + # Min an max total decim + decim_max = int(math.floor(self.samp_rate / (2 * self.sym_rate))) + decim_min = int(math.ceil (self.samp_rate / (3 * self.sym_rate))) + + # Factors + factors = [self._factor(i) for i in range(decim_min, decim_max+1)] + + # Rank them and select best + factors_best = sorted(factors, key=lambda x: -self._score(x))[0] + factors_best = (factors_best + [1])[0:2] + + # Resampling factor + decim = factors_best[0] * factors_best[1] + resamp = (1.0 * self.sym_rate * self.sps * decim) / self.samp_rate + + # If decim2 is <= 4, merge with resampler + if factors_best[1] <= 4: + resamp /= factors_best[1] + factors_best[1] = 1 + + # Store result + self.decim1 = factors_best[0] + self.decim2 = factors_best[1] + self.resamp = resamp + + def _generate_taps(self): + # Filter taps + need_rrc = True + + # PFB Arb Resampler + if self.resamp != 1: + if need_rrc: + self.taps_resamp = firdes.root_raised_cosine( + 32.0, + 32.0 * self.samp_rate / (self.decim1 * self.decim2), + self.sym_rate, + 0.35, + int(11.0 * 32 * self.samp_rate / (self.decim1 * self.decim2 * self.sym_rate)) + ) + need_rrc = False + else: + self.taps_resamp = firdes.low_pass( + 32.0, + 32.0 * self.samp_rate / (self.decim1 * self.decim2), + self.sym_rate * 1.4 / 2, + self.sym_rate * 0.1 + ) + else: + self.taps_resamp = [] + + # Decim 2 + if self.decim2 != 1: + if need_rrc: + self.taps2 = firdes.root_raised_cosine( + 1.0, + self.samp_rate / self.decim1, + self.sym_rate, + 0.35, + int(11.0 * self.samp_rate / (self.decim1 * self.sym_rate)) + ) + need_rrc = False + else: + self.taps2 = firdes.low_pass( + 1.0, + 1.0, + 0.45 / self.decim2, + 0.10 / self.decim2 + ) + else: + self.taps2 = [] + + # Decim 1 + if need_rrc: + self.taps1 = firdes.root_raised_cosine( + 1.0, + self.samp_rate, + self.sym_rate, + 0.35, + int(11.0 * self.samp_rate / self.sym_rate) + ) + need_rrc = False + else: + self.taps1 = firdes.low_pass( + 1.0, + 1.0, + 0.3 / self.decim1, + 0.3 / self.decim1 + ) + + +class DirectOutputBranch(gr.hier_block2): + + def __init__(self, params, freq, filename): + # Super + gr.hier_block2.__init__(self, + "DirectOutputBranch", + gr.io_signature(1,1,gr.sizeof_gr_complex), + gr.io_signature(0,0,0) + ) + + prev = self + + # Delay + if params.delay: + self.delay = blocks.delay( + gr.sizeof_gr_complex, + int(round(params.delay)), + ) + self.connect( (prev, 0), (self.delay, 0) ) + prev = self.delay + + # Freq xlating filter + if params.decim1 > 1: + self.filt1 = filter.freq_xlating_fir_filter_ccc( + params.decim1, params.taps1, + freq, params.samp_rate + ) + + self.connect( (prev, 0), (self.filt1, 0) ) + prev = self.filt1 + + # Decimating FIR filter + if params.decim2 > 1: + self.filt2 = filter.fir_filter_ccc( + params.decim2, params.taps2 + ) + + self.connect( (prev, 0), (self.filt2, 0) ) + prev = self.filt2 + + # PFB Arb Resampler + if params.resamp != 1: + self.resamp = pfb.arb_resampler_ccf( + params.resamp, params.taps_resamp, + flt_size = 32 + ) + self.connect( (prev, 0), (self.resamp, 0) ) + prev = self.resamp + + # Output file + self.sink = blocks.file_sink(gr.sizeof_gr_complex, filename, False) + self.connect( (prev, 0), (self.sink, 0) ) + + +# ---------------------------------------------------------------------------- +# Top-Level flowgraph +# ---------------------------------------------------------------------------- + +class top_block(gr.top_block): + + def __init__(self, config): + # Super init + gr.top_block.__init__(self, "GMR-1 L-band RX Top Block") + + # Save config + self.config = config + + # Setup source + self._setup_source() + + # Setup GUI base + if self.config.qt: + self._setup_qt() + + # ARFCNs sorting & source assignement + self._arfcn_prepare() + + # Setup Channelizer or Direct topology + if self.config.pfb: + self._setup_pfb() + else: + self._setup_direct() + + def _setup_qt_channel(self, chan): + fblk = fosphor.qt_sink_c() + fblk.set_fft_window(window.WIN_BLACKMAN_hARRIS) + fblk.set_frequency_range(self.source_freq[chan], self.source_rate) + fblk_win = sip.wrapinstance(fblk.pyqwidget(), Qt.QWidget) + self.top_layout.addWidget(fblk_win) + self.connect( self.source_ep[chan], (fblk, 0) ) + + def _setup_qt(self): + # Qt window setup + self.widget = Qt.QWidget() + self.widget.setWindowTitle("GMR-1 L-band RX Top Block") + self.widget.setWindowIcon(Qt.QIcon.fromTheme('gnuradio-grc')) + + self.top_scroll_layout = Qt.QVBoxLayout() + self.widget.setLayout(self.top_scroll_layout) + self.top_scroll = Qt.QScrollArea() + self.top_scroll.setFrameStyle(Qt.QFrame.NoFrame) + self.top_scroll_layout.addWidget(self.top_scroll) + self.top_scroll.setWidgetResizable(True) + self.top_widget = Qt.QWidget() + self.top_scroll.setWidget(self.top_widget) + self.top_layout = Qt.QVBoxLayout(self.top_widget) + self.top_grid_layout = Qt.QGridLayout() + self.top_layout.addLayout(self.top_grid_layout) + + # Setup GUI for each channels + for i in range(self.source_chans): + self._setup_qt_channel(i) + + def _setup_source_channel(self, chan): + # Source params + cp = self.config.channel.get(chan, self.config.channel[None]) + + self.source.set_center_freq(cp.center_freq, chan) + self.source_freq[chan] = self.source.get_center_freq(chan) + + corr = cp.get('corr', None) + if corr is not None: + self.source.set_freq_corr(corr, chan) + + bw = cp.get('bw', None) + if bw is not None: + self.source.set_bandwidth(bw, chan) + + gain = cp.get('gain', []) + if gain: + if None in gain: + self.source.set_gain(gain.pop(None), chan) + for gs,gv in gain.iteritems(): + self.source.set_gain(gv, gs, chan) + + # Time limit or direct + if self.config.time: + hb = blocks.head(gr.sizeof_gr_complex, int(1.0 * self.source_rate * self.config.time)) + self.connect( (self.source, chan), (hb, 0) ) + self.source_ep[chan] = (hb, 0) + else: + self.source_ep[chan] = (self.source, chan) + + def _setup_source(self): + # Source instance + self.source = osmosdr.source(args=self.config.args) + + self.source.set_sample_rate(self.config.samp_rate) + self.source_rate = self.source.get_sample_rate() + + self.source.set_min_output_buffer(int(self.source_rate * 0.01 * gr.sizeof_gr_complex)) + + # Tag debug + if True: + td = blocks.tag_debug(gr.sizeof_gr_complex, "Source") + self.connect( (self.source, 0), (td, 0) ) + + # Configure channels + self.source_chans = self.source.get_num_channels() + self.source_ep = {} + self.source_freq = {} + + for i in range(self.source_chans): + self._setup_source_channel(i) + + def _arfcn_prepare(self): + # Set the band + for a in self.config.arfcns: + a.band = self.config.band + + # Accumulate all channels width we need to support + self.needed_widths = dict([ + (x.width, (x.bandwidth, x.symbol_rate)) + for x in self.config.arfcns + ]) + + # Assign ARFCNs to sources + self.source_arfcns = {} + + for arfcn in self.config.arfcns: + bs = sorted(range(len(self.source_freq)), key=lambda i:abs(arfcn.frequency - self.source_freq[i]))[0] + self.source_arfcns.setdefault(bs, []).append(arfcn) + + def _setup_direct(self, sps=4): + # Compute params + oparams = {} + for k in sorted(self.needed_widths.keys()): + oparams[k] = DirectOutputParameters( + self.source_rate, + self.needed_widths[k][1], + sps + ) + print "Params for width %dx:" % k + print indent(oparams[k].describe()) + print "" + + # Adjust the delays to match + delay = max([x.min_delay() for x in oparams.values()]) + for x in oparams.values(): + x.adjust_delay(delay) + + # Generate all the output branches + for source_chan, arfcns in self.source_arfcns.iteritems(): + for arfcn in arfcns: + # Compute frequency offset + f = arfcn.frequency + df = f - self.source_freq[source_chan] + if abs(df) >= (self.source_rate / 2): + print "ARFCN %s (%sHz) is outside the range\n" % ( + arfcn, + eng_notation.num_to_str(f) + ) + continue + + # Debug print + print "ARFCN %s (abs: %sHz, rel: %sHz)" % ( + arfcn, + eng_notation.num_to_str(f), + eng_notation.num_to_str(df) + ) + + # Generate branch and connect it + b = DirectOutputBranch( + oparams[arfcn.width], + df, + self.config.output % ( arfcn, ) + ) + + self.connect( (self.source, source_chan), (b, 0) ) + + def _setup_pfb(self, sps=4): + # Do we need more the 1x channels ? + need_Nx = self.needed_widths.keys() != [1] + + # Create the base channelization block for each source channel + self.pfb_base = {} + for source_chan, freq in sorted(self.source_freq.iteritems()): + self.pfb_base[source_chan] = PFBBase( + freq, + self.source_rate, + Channel.BASE_BANDWIDTH, + Channel.align_freq, + need_Nx + ) + + print "Channelization of source port %d:" % source_chan + print indent(self.pfb_base[source_chan].describe()) + print "" + + self.connect( (self.source, source_chan), (self.pfb_base[source_chan], 0) ) + + # Compute the output branch params for each width + oparams = {} + for k in sorted(self.needed_widths.keys()): + oparams[k] = PFBOutputParameters( + k, + self.needed_widths[k][0], + self.needed_widths[k][1], + 4 + ) + + print "Output params for width %dx:" % k + print indent(oparams[k].describe()) + print "" + + # Adjust the delays to match + delay = max([x.min_delay() for x in oparams.values()]) + for x in oparams.values(): + x.adjust_delay(delay) + + # Generate all the output branches + for source_chan, arfcns in self.source_arfcns.iteritems(): + # Need to save used indexes to NULL sink the unused ones + used_indexes = set() + + # Scan all arfcn + for arfcn in arfcns: + # Map this arfcn to a channel list from the PFB base + pcl = [ + self.pfb_base[source_chan].freq2index(sc.frequency) + for sc in arfcn.subchannels + ] + + if None in pcl: + print "ARFCN %s (out-of-range)" % (arfcn,) + continue + + # Collect indexes + used_indexes.update(pcl) + + # Debug print + print "ARFCN %s (abs: %sHz, pfb chans: %r)" % ( + arfcn, + eng_notation.num_to_str(arfcn.frequency), + pcl # FIXME + ) + + # Generate branch and connect it + b = PFBOutputBranch( + oparams[arfcn.width], + self.config.output % ( arfcn, ) + ) + + for i, pc in enumerate(pcl): + self.connect( (self.pfb_base[source_chan], pc), (b, i) ) + + # Plug unused channels + term = blocks.null_sink(gr.sizeof_gr_complex) + i = 0 + for index in range(self.pfb_base[source_chan].n_chans): + if index not in used_indexes: + self.connect( (self.pfb_base[source_chan], index), (term, i) ) + i += 1 + + def show(self): + self.widget.show() + + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +def main(): + # Arguments + args = args_parse() + + # Qt setup ? + if args.qt: + # Qt config + if(StrictVersion(Qt.qVersion()) >= StrictVersion("4.5.0")): + Qt.QApplication.setGraphicsSystem(gr.prefs().get_string('qtgui','style','raster')) + + # Create app + qapp = Qt.QApplication(sys.argv) + + # Create top-block + tb = top_block(config=args) + + # Qt run ... + if args.qt: + # Ensure proper shutdown + def quitting(): + tb.stop() + tb.wait() + + qapp.connect(qapp, Qt.SIGNAL("aboutToQuit()"), quitting) + + # Run the flow graph & app + tb.start() + tb.show() + + # App run + qapp.exec_() + + # ... or Console run + else: + tb.start() + tb.wait() + + # Force gargage collection, to clean up Qt widgets + tb = None + + return 0 + + +if __name__ == '__main__': + sys.exit(main())