class _SimulatedTransmitter(gr.hier_block2, ExportedState): '''provides frequency parameters''' def __init__(self, modulator, audio_rate, rf_rate, freq): modulator = IModulator(modulator) gr.hier_block2.__init__( self, 'SimulatedChannel', gr.io_signature(1, 1, gr.sizeof_float * 1), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__freq = freq self.__rf_rate = rf_rate self.modulator = modulator # exported modulator_input_type = modulator.get_input_type() if modulator_input_type.get_kind() == 'MONO': audio_resampler = make_resampler( audio_rate, modulator_input_type.get_sample_rate()) self.connect(self, audio_resampler, modulator) elif modulator_input_type.get_kind() == 'NONE': self.connect(self, blocks.null_sink(gr.sizeof_float)) else: raise Exception('don\'t know how to supply input of type %s' % modulator_input_type) rf_resampler = rational_resampler.rational_resampler_ccf( interpolation=int(rf_rate), decimation=int(modulator.get_output_type().get_sample_rate())) self.__rotator = blocks.rotator_cc( rotator_inc(rate=rf_rate, shift=freq)) self.__mult = blocks.multiply_const_cc(dB(-10)) self.connect(modulator, rf_resampler, self.__rotator, self.__mult, self) def state_def(self, callback): super(_SimulatedTransmitter, self).state_def(callback) # TODO make this possible to be decorator style callback(BlockCell(self, 'modulator')) @exported_value(ctor_fn=lambda self: Range( [(-self.__rf_rate / 2, self.__rf_rate / 2)], strict=False)) def get_freq(self): return self.__freq @setter def set_freq(self, value): self.__freq = float(value) self.__rotator.set_phase_inc( rotator_inc(rate=self.__rf_rate, shift=self.__freq)) @exported_value(ctor=Range([(-50.0, 0.0)], strict=False)) def get_gain(self): return todB(self.__mult.k().real) @setter def set_gain(self, value): self.__mult.set_k(dB(value))
def calc_usable_bandwidth(self, sample_rate): passband = sample_rate * (3/8) # 3/4 of + and - halves if self.__profile.dc_offset: epsilon = 1.0 # Range has only inclusive bounds, so we need a nonzero value. return Range([(-passband, -epsilon), (epsilon, passband)]) else: return Range([(-passband, passband)])
def __init__(self, device_name, sample_rate, channel_mapping): self.__device_name = device_name self.__sample_rate = sample_rate if len(channel_mapping) == 2: self.__signal_type = SignalType(kind='IQ', sample_rate=self.__sample_rate) # TODO should be configurable self.__usable_bandwidth = Range([(-self.__sample_rate / 2, self.__sample_rate / 2)]) else: self.__signal_type = SignalType( kind= 'USB', # TODO obtain correct type from config (or say hamlib) sample_rate=self.__sample_rate) self.__usable_bandwidth = Range([(500, 2500)]) gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__source = audio.source(self.__sample_rate, device_name=self.__device_name, ok_to_block=True) channel_matrix = blocks.multiply_matrix_ff(channel_mapping) combine = blocks.float_to_complex(1) for i in xrange(0, len(channel_mapping[0])): self.connect((self.__source, i), (channel_matrix, i)) for i in xrange(0, len(channel_mapping)): self.connect((channel_matrix, i), (combine, i)) self.connect(combine, self)
class _HamlibRotator(_HamlibProxy): implements(IRotator) _server_name = 'rotctld' _dummy_command = 'get_pos' # TODO: support imperative commands: # move # stop # park # reset _info = { # TODO: Get ranges from dump_caps 'Azimuth': (Range([(-180, 180)])), 'Elevation': (Range([(0, 90)])), } _commands = { 'pos': ['Azimuth', 'Elevation'], } def poll_fast(self, send): send('get_pos') def poll_slow(self, send): pass
def __init__(self, device_name, sample_rate, quadrature_as_stereo): self.__device_name = device_name self.__sample_rate = sample_rate self.__quadrature_as_stereo = quadrature_as_stereo if self.__quadrature_as_stereo: self.__signal_type = SignalType(kind='IQ', sample_rate=self.__sample_rate) # TODO should be configurable self.__usable_bandwidth = Range([(-self.__sample_rate / 2, self.__sample_rate / 2)]) else: self.__signal_type = SignalType( kind= 'USB', # TODO obtain correct type from config (or say hamlib) sample_rate=self.__sample_rate) self.__usable_bandwidth = Range([(500, 2500)]) gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__source = audio.source(self.__sample_rate, device_name=self.__device_name, ok_to_block=True) combine = blocks.float_to_complex(1) self.connect(self.__source, combine, self) if self.__quadrature_as_stereo: # if we don't do this, the imaginary component is 0 and the spectrum is symmetric self.connect((self.__source, 1), (combine, 1))
def SimulatedDevice(name='Simulated RF', freq=0.0, allow_tuning=False): return Device( name=name, vfo_cell=LooseCell( key='freq', value=freq, ctor=Range([(-1e9, 1e9)]) if allow_tuning else Range([(freq, freq)]), # TODO kludge magic numbers writable=True, persists=False), rx_driver=_SimulatedRXDriver(name))
def _install_cell(self, name, is_level, writable, callback, caps): # this is a function for the sake of the closure variables if name == 'Frequency': cell_name = 'freq' # consistency with our naming scheme elsewhere, also IHasFrequency else: cell_name = name if is_level: # TODO: Use range info from hamlib if available if name == 'STRENGTH level': vtype = Range([(-54, 50)], strict=False) elif name == 'SWR level': vtype = Range([(1, 30)], strict=False) elif name == 'RFPOWER level': vtype = Range([(0, 100)], strict=False) else: vtype = Range([(-10, 10)], strict=False) elif name == 'Mode' or name == 'TX Mode': # kludge vtype = Enum({x: x for x in caps['Mode list'].strip().split(' ')}) elif name == 'VFO' or name == 'TX VFO': vtype = Enum({x: x for x in caps['VFO list'].strip().split(' ')}) else: vtype = self._info[name] def updater(strval): try: if vtype is bool: value = bool(int(strval)) else: value = vtype(strval) except ValueError: value = unicode(strval) cell.set_internal(value) def actually_write_value(value): if vtype is bool: self._ehs_set(name, str(int(value))) else: self._ehs_set(name, str(vtype(value))) cell = LooseCell( key=cell_name, value='placeholder', type=vtype, writable=writable, persists=False, post_hook=actually_write_value, label=name) # TODO: supply label values from _info table self._cell_updaters[name] = updater updater(self._ehs_get(name)) callback(cell)
def test_repr(self): self.assertEqual( 'Range([(1, 2), (3, 4)], strict=True, logarithmic=False, integer=False)', repr(Range([(1, 2), (3, 4)]))) self.assertEqual( 'Range([(1, 2), (3, 4)], strict=False, logarithmic=False, integer=False)', repr(Range([(1, 2), (3, 4)], strict=False))) self.assertEqual( 'Range([(1, 2), (3, 4)], strict=True, logarithmic=True, integer=False)', repr(Range([(1, 2), (3, 4)], logarithmic=True))) self.assertEqual( 'Range([(1, 2), (3, 4)], strict=True, logarithmic=False, integer=True)', repr(Range([(1, 2), (3, 4)], integer=True)))
class RTTYFSKDemodulator(gr.hier_block2, ExportedState): """ Demodulate FSK with parameters suitable for gr-rtty. TODO: Make this into something more reusable once we have other examples of FSK. Note this differs from the GFSK demod in gnuradio.digital by having a DC blocker. """ def __init__(self, input_rate, baud): gr.hier_block2.__init__( self, 'RTTY FSK demodulator', gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(1, 1, gr.sizeof_float * 1), ) self.bit_time = bit_time = input_rate / baud fsk_deviation_hz = 85 # TODO param or just don't care self.__dc_blocker = grfilter.dc_blocker_ff( int(bit_time * _HALF_BITS_PER_CODE * 10), False) self.__quadrature_demod = analog.quadrature_demod_cf( -input_rate / (2 * math.pi * fsk_deviation_hz)) self.__freq_probe = blocks.probe_signal_f() self.connect(self, self.__quadrature_demod, self.__dc_blocker, digital.binary_slicer_fb(), blocks.char_to_float(scale=1), self) self.connect(self.__dc_blocker, self.__freq_probe) @exported_value(type=Range([(-2, 2)])) def get_probe(self): return abs(self.__freq_probe.level())
def test_vfos(self): d = merge_devices([ Device(vfo_cell=_ConstantVFOCell(1)), Device(vfo_cell=LooseCell( key='freq', value=0, ctor=Range([(10, 20)]), writable=True)) ]) self.assertTrue(d.get_vfo_cell().isWritable())
def test_bandwidth_discontiguous(self): self.assertEqual( Range([(-30000.0, -1.0), (1.0, 30000.0)]), OsmoSDRDevice( 'file=/dev/null,rate=80000', profile=OsmoSDRProfile( dc_offset=True)).get_rx_driver().get_usable_bandwidth())
class ChirpModulator(gr.hier_block2, ExportedState): implements(IModulator) def __init__(self, context, mode, chirp_rate=0.1, output_rate=10000): gr.hier_block2.__init__(self, type(self).__name__, gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex)) self.__output_rate = output_rate self.__chirp_rate = chirp_rate self.__control = analog.sig_source_f(output_rate, analog.GR_SAW_WAVE, chirp_rate, output_rate * 2 * math.pi, 0) chirp_vco = blocks.vco_c(output_rate, 1, 1) self.connect(self.__control, chirp_vco, self) def get_input_type(self): return no_signal def get_output_type(self): return SignalType(kind='IQ', sample_rate=self.__output_rate) @exported_value(parameter='chirp_rate', type=Range([(-10.0, 10.0)], strict=False)) def get_chirp_rate(self): return self.__chirp_rate @setter def set_chirp_rate(self, value): self.__chirp_rate = value self.__control.set_frequency(value)
def _ConstantVFOCell(value): value = float(value) return LooseCell(key='freq', value=value, ctor=Range([(value, value)]), writable=False, persists=False)
def test_log_integer(self): _testType( self, Range([(1, 32)], strict=True, logarithmic=True, integer=True), [(0, 1), 1, 2, 4, 32, (2.0, 2), (2.5, 2), (3.5, 4), (33, 32)], [])
def AudioDevice( rx_device='', # may be used positionally, not recommented tx_device=None, name=None, sample_rate=44100, quadrature_as_stereo=False): rx_device = str(rx_device) if tx_device is not None: tx_device = str(tx_device) if name is None: full_name = u'Audio ' + rx_device if tx_device is not None: full_name += '/' + tx_device else: full_name = unicode(name) rx_driver = _AudioRXDriver(device_name=rx_device, sample_rate=sample_rate, quadrature_as_stereo=quadrature_as_stereo) if tx_device is not None: tx_driver = _AudioTXDriver(device_name=tx_device, sample_rate=sample_rate, quadrature_as_stereo=quadrature_as_stereo) else: tx_driver = nullExportedState return Device(name=full_name, vfo_cell=LooseCell(key='freq', value=0.0, ctor=Range([(0.0, 0.0)]), writable=True, persists=False), rx_driver=rx_driver, tx_driver=tx_driver)
def SimulatedDevice(name='Simulated RF', freq=0.0): return Device(name=name, vfo_cell=LooseCell(key='freq', value=freq, ctor=Range([(freq, freq)]), writable=True, persists=False), rx_driver=_SimulatedRXDriver(name))
def convert_osmosdr_range(meta_range, add_zero=False, transform=lambda f: f, **kwargs): # TODO: Recognize step values from osmosdr subranges = [] for i in xrange(0, meta_range.size()): single_range = meta_range[i] subranges.append((transform(single_range.start()), transform(single_range.stop()))) if add_zero: subranges[0:0] = [(0, 0)] return Range(subranges, **kwargs)
class SquelchMixin(ExportedState): def __init__(self, squelch_rate, squelch_threshold=-100): alpha = 80.0 / squelch_rate self.rf_squelch_block = analog.simple_squelch_cc( squelch_threshold, alpha) self.rf_probe_block = analog.probe_avg_mag_sqrd_c(0, alpha=alpha) @exported_value(type=Range([(-100, 0)], strict=False)) def get_rf_power(self): return todB(max(1e-10, self.rf_probe_block.level())) @exported_value(type=Range([(-100, 0)], strict=False, logarithmic=False)) def get_squelch_threshold(self): return self.rf_squelch_block.threshold() @setter def set_squelch_threshold(self, level): self.rf_squelch_block.set_threshold(level)
def setUp(self): self.lc = LooseCell(value=0, key='a', type=Range([(-100, 100)])) self.delta = 1 self.vc = ViewCell( base=self.lc, get_transform=lambda x: x + self.delta, set_transform=lambda x: x - self.delta, key='b', type=int)
class DecoratorInheritanceSpecimen(DecoratorInheritanceSpecimenSuper): """Helper for TestDecorator""" def __init__(self): self.rw = 0.0 @exported_value(type=Range([(0.0, 10.0)])) def get_rw(self): return self.rw @setter def set_rw(self, value): self.rw = value
def test_Range_shifted_integer(self): _testType(self, Range([(3, 4)], strict=True, logarithmic=False, integer=True).shifted_by(-3), [(-0.5, 0), 0, (0.25, 0), 1, (1.5, 1)], [])
def test_Range_discrete(self): _testType(self, Range([(1, 1), (2, 3), (5, 5)], strict=True, integer=False), [(0, 1), 1, (1.49, 1), (1.50, 1), (1.51, 2), 2, 2.5, 3, (4, 3), (4.1, 5), 5, (6, 5)], [])
class MonitorSink(gr.hier_block2, ExportedState): """ Convenience wrapper around all the bits and pieces to display the signal spectrum to the client. The units of the FFT output are dB power/Hz (power spectral density) relative to unit amplitude (i.e. dBFS assuming the source clips at +/-1). Note this is different from the standard logpwrfft result of power _per bin_, which would be undesirably dependent on the sample rate and bin size. """ def __init__(self, signal_type=None, enable_scope=False, freq_resolution=4096, time_length=2048, frame_rate=30.0, input_center_freq=0.0, paused=False, context=None): assert isinstance(signal_type, SignalType) assert context is not None itemsize = signal_type.get_itemsize() gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(1, 1, itemsize), gr.io_signature(0, 0, 0), ) # constant parameters self.__power_offset = 40 # TODO autoset or controllable self.__itemsize = itemsize self.__context = context self.__enable_scope = enable_scope # settable parameters self.__signal_type = signal_type self.__freq_resolution = int(freq_resolution) self.__time_length = int(time_length) self.__frame_rate = float(frame_rate) self.__input_center_freq = float(input_center_freq) self.__paused = bool(paused) self.__interested_cell = LooseCell(key='interested', type=bool, value=False, writable=False, persists=False) # blocks self.__gate = None self.__fft_sink = None self.__scope_sink = None self.__scope_chunker = None self.__before_fft = None self.__logpwrfft = None self.__overlapper = None self.__rebuild() self.__connect() def state_def(self, callback): super(MonitorSink, self).state_def(callback) # TODO make this possible to be decorator style callback( StreamCell(self, 'fft', type=BulkDataType(array_format='b', info_format='dff'))) callback( StreamCell(self, 'scope', type=BulkDataType(array_format='f', info_format='d'))) def __rebuild(self): if self.__signal_type.is_analytic(): input_length = self.__freq_resolution output_length = self.__freq_resolution self.__after_fft = None else: # use vector_to_streams to cut the output in half and discard the redundant part input_length = self.__freq_resolution * 2 output_length = self.__freq_resolution self.__after_fft = blocks.vector_to_streams( itemsize=output_length * gr.sizeof_float, nstreams=2) sample_rate = self.__signal_type.get_sample_rate() overlap_factor = int( math.ceil(_maximum_fft_rate * input_length / sample_rate)) # sanity limit -- OverlapGimmick is not free overlap_factor = min(16, overlap_factor) self.__gate = blocks.copy(gr.sizeof_gr_complex) self.__gate.set_enabled(not self.__paused) self.__fft_sink = MessageDistributorSink( itemsize=output_length * gr.sizeof_char, context=self.__context, migrate=self.__fft_sink, notify=self.__update_interested) self.__overlapper = _OverlapGimmick(size=input_length, factor=overlap_factor, itemsize=self.__itemsize) # Adjusts units so displayed level is independent of resolution and sample rate. Also throw in the packing offset compensation = to_dB(input_length / sample_rate) + self.__power_offset # TODO: Consider not using the logpwrfft block self.__logpwrfft = logpwrfft.logpwrfft_c( sample_rate=sample_rate * overlap_factor, fft_size=input_length, ref_scale=10.0**(-compensation / 20.0) * 2, # not actually using this as a reference scale value but avoiding needing to use a separate add operation to apply the unit change -- this expression is the inverse of what logpwrfft does internally frame_rate=self.__frame_rate, avg_alpha=1.0, average=False) # It would make slightly more sense to use unsigned chars, but blocks.float_to_uchar does not support vlen. self.__fft_converter = blocks.float_to_char( vlen=self.__freq_resolution, scale=1.0) self.__scope_sink = MessageDistributorSink( itemsize=self.__time_length * gr.sizeof_gr_complex, context=self.__context, migrate=self.__scope_sink, notify=self.__update_interested) self.__scope_chunker = blocks.stream_to_vector_decimator( item_size=gr.sizeof_gr_complex, sample_rate=sample_rate, vec_rate=self.__frame_rate, # TODO doesn't need to be coupled vec_len=self.__time_length) def __connect(self): self.__context.lock() try: self.disconnect_all() self.connect(self, self.__gate, self.__overlapper, self.__logpwrfft) if self.__after_fft is not None: self.connect(self.__logpwrfft, self.__after_fft) self.connect(self.__after_fft, self.__fft_converter, self.__fft_sink) self.connect( (self.__after_fft, 1), blocks.null_sink(gr.sizeof_float * self.__freq_resolution)) else: self.connect(self.__logpwrfft, self.__fft_converter, self.__fft_sink) if self.__enable_scope: self.connect(self.__gate, self.__scope_chunker, self.__scope_sink) finally: self.__context.unlock() # non-exported def get_interested_cell(self): return self.__interested_cell def __update_interested(self): self.__interested_cell.set_internal( not self.__paused and (self.__fft_sink.get_subscription_count() > 0 or self.__scope_sink.get_subscription_count() > 0)) @exported_value() def get_signal_type(self): return self.__signal_type # non-exported def set_signal_type(self, value): # TODO: don't rebuild if the rate did not change and the spectrum-sidedness of the type did not change assert self.__signal_type.compatible_items(value) self.__signal_type = value self.__rebuild() self.__connect() # non-exported def set_input_center_freq(self, value): self.__input_center_freq = float(value) @exported_value(type=Range([(2, 4096)], logarithmic=True, integer=True)) def get_freq_resolution(self): return self.__freq_resolution @setter def set_freq_resolution(self, freq_resolution): self.__freq_resolution = freq_resolution self.__rebuild() self.__connect() @exported_value(type=Range([(1, 4096)], logarithmic=True, integer=True)) def get_time_length(self): return self.__time_length @setter def set_time_length(self, value): self.__time_length = value self.__rebuild() self.__connect() @exported_value(type=Range([(1, _maximum_fft_rate)], logarithmic=True, integer=False)) def get_frame_rate(self): return self.__frame_rate @setter def set_frame_rate(self, value): self.__logpwrfft.set_vec_rate(float(value)) self.__frame_rate = self.__logpwrfft.frame_rate() @exported_value(type=bool) def get_paused(self): return self.__paused @setter def set_paused(self, value): self.__paused = value self.__gate.set_enabled(not value) self.__update_interested() # exported via state_def def get_fft_info(self): return (self.__input_center_freq, self.__signal_type.get_sample_rate(), self.__power_offset) def get_fft_distributor(self): return self.__fft_sink # exported via state_def def get_scope_info(self): return (self.__signal_type.get_sample_rate(), ) def get_scope_distributor(self): return self.__scope_sink
class VORModulator(gr.hier_block2, ExportedState): implements(IModulator) __vor_sig_freq = 30 __audio_rate = 10000 __rf_rate = 30000 # needs to be above fm_subcarrier * 2 def __init__(self, context, mode, angle=0.0): gr.hier_block2.__init__( self, 'SimulatedDevice VOR modulator', gr.io_signature(1, 1, gr.sizeof_float * 1), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__angle = 0.0 # dummy statically visible value will be overwritten # TODO: My signal level parameters are probably wrong because this signal doesn't look like a real VOR signal vor_30 = analog.sig_source_f(self.__audio_rate, analog.GR_COS_WAVE, self.__vor_sig_freq, 1, 0) vor_add = blocks.add_cc(1) vor_audio = blocks.add_ff(1) # Audio/AM signal self.connect( vor_30, blocks.multiply_const_ff(0.3), # M_n (vor_audio, 0)) self.connect( self, blocks.multiply_const_ff(audio_modulation_index), # M_i (vor_audio, 1)) # Carrier component self.connect(analog.sig_source_c(0, analog.GR_CONST_WAVE, 0, 0, 1), (vor_add, 0)) # AM component self.__delay = blocks.delay(gr.sizeof_gr_complex, 0) # configured by set_angle self.connect( vor_audio, make_resampler(self.__audio_rate, self.__rf_rate ), # TODO make a complex version and do this last blocks.float_to_complex(1), self.__delay, (vor_add, 1)) # FM component vor_fm_mult = blocks.multiply_cc(1) self.connect( # carrier generation analog.sig_source_f(self.__rf_rate, analog.GR_COS_WAVE, fm_subcarrier, 1, 0), blocks.float_to_complex(1), (vor_fm_mult, 1)) self.connect( # modulation vor_30, make_resampler(self.__audio_rate, self.__rf_rate), analog.frequency_modulator_fc(2 * math.pi * fm_deviation / self.__rf_rate), blocks.multiply_const_cc(0.3), # M_d vor_fm_mult, (vor_add, 2)) self.connect(vor_add, self) # calculate and initialize delay self.set_angle(angle) @exported_value(type=Range([(0, 2 * math.pi)], strict=False)) def get_angle(self): return self.__angle @setter def set_angle(self, value): value = float(value) compensation = math.pi / 180 * -6.5 # empirical, calibrated against VOR receiver (and therefore probably wrong) value = value + compensation value = value % (2 * math.pi) phase_shift = int(self.__rf_rate / self.__vor_sig_freq * (value / (2 * math.pi))) self.__delay.set_dly(phase_shift) self.__angle = value def get_input_type(self): return SignalType(kind='MONO', sample_rate=self.__audio_rate) def get_output_type(self): return SignalType(kind='IQ', sample_rate=self.__rf_rate)
def get_usable_bandwidth(self): return Range([(-1, 1)])
class Receiver(gr.hier_block2, ExportedState): implements(IReceiver) def __init__(self, mode, freq_absolute=100.0, freq_relative=None, freq_linked_to_device=False, audio_destination=None, device_name=None, audio_gain=-6, audio_pan=0, audio_channels=0, context=None): assert audio_channels == 1 or audio_channels == 2 assert audio_destination is not None assert device_name is not None gr.hier_block2.__init__( # str() because insists on non-unicode self, str('%s receiver' % (mode, )), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(audio_channels, audio_channels, gr.sizeof_float * 1), ) if lookup_mode(mode) is None: # TODO: communicate back to client if applicable log.msg('Unknown mode %r in Receiver(); using AM' % (mode, )) mode = 'AM' # Provided by caller self.context = context self.__audio_channels = audio_channels # cached info from device self.__device_name = device_name # Simple state self.mode = mode self.audio_gain = audio_gain self.audio_pan = min(1, max(-1, audio_pan)) self.__audio_destination = audio_destination # Receive frequency. self.__freq_linked_to_device = bool(freq_linked_to_device) if self.__freq_linked_to_device and freq_relative is not None: self.__freq_relative = float(freq_relative) self.__freq_absolute = self.__freq_relative + self.__get_device( ).get_freq() else: self.__freq_absolute = float(freq_absolute) self.__freq_relative = self.__freq_absolute - self.__get_device( ).get_freq() # Blocks self.__rotator = blocks.rotator_cc() self.__demodulator = self.__make_demodulator(mode, {}) self.__update_demodulator_info() self.__audio_gain_blocks = [ blocks.multiply_const_ff(0.0) for _ in xrange(self.__audio_channels) ] self.probe_audio = analog.probe_avg_mag_sqrd_f( 0, alpha=10.0 / 44100) # TODO adapt to output audio rate # Other internals self.__last_output_type = None self.__update_rotator( ) # initialize rotator, also in case of __demod_tunable self.__update_audio_gain() self.__do_connect(reason=u'initialization') def __update_demodulator_info(self): self.__demod_tunable = ITunableDemodulator.providedBy( self.__demodulator) output_type = self.__demodulator.get_output_type() assert isinstance(output_type, SignalType) # TODO: better expression of this condition assert output_type.get_kind() == 'STEREO' or output_type.get_kind( ) == 'MONO' or output_type.get_kind() == 'NONE' self.__demod_output = output_type.get_kind() != 'NONE' self.__demod_stereo = output_type.get_kind() == 'STEREO' self.__output_type = SignalType( kind='STEREO', sample_rate=output_type.get_sample_rate() if self.__demod_output else 0) def __do_connect(self, reason): #log.msg(u'receiver do_connect: %s' % (reason,)) self.context.lock() try: self.disconnect_all() # Connect input of demodulator if self.__demod_tunable: self.connect(self, self.__demodulator) else: self.connect(self, self.__rotator, self.__demodulator) if self.__demod_output: # Connect output of demodulator self.connect((self.__demodulator, 0), self.__audio_gain_blocks[0]) # left or mono if self.__audio_channels == 2: self.connect( (self.__demodulator, 1 if self.__demod_stereo else 0), self.__audio_gain_blocks[1]) else: if self.__demod_stereo: self.connect((self.__demodulator, 1), blocks.null_sink(gr.sizeof_float)) # Connect output of receiver for ch in xrange(self.__audio_channels): self.connect(self.__audio_gain_blocks[ch], (self, ch)) # Level meter # TODO: should mix left and right or something self.connect((self.__demodulator, 0), self.probe_audio) else: # Dummy output, ignored by containing block source_of_nothing = blocks.vector_source_f([]) for ch in xrange(0, self.__audio_channels): self.connect(source_of_nothing, (self, ch)) if self.__output_type != self.__last_output_type: self.__last_output_type = self.__output_type self.context.changed_needed_connections(u'changed output type') finally: self.context.unlock() def get_output_type(self): return self.__output_type def changed_device_freq(self): if self.__freq_linked_to_device: self.__freq_absolute = self.__freq_relative + self.__get_device( ).get_freq() else: self.__freq_relative = self.__freq_absolute - self.__get_device( ).get_freq() self.__update_rotator() # note does not revalidate() because the caller will handle that @exported_block() def get_demodulator(self): return self.__demodulator @exported_value(type_fn=lambda self: self.context.get_rx_device_type()) def get_device_name(self): return self.__device_name @setter def set_device_name(self, value): value = self.context.get_rx_device_type()(value) if self.__device_name != value: self.__device_name = value self.changed_device_freq() # freq self._rebuild_demodulator( reason=u'changed device, thus maybe sample rate') # rate self.context.changed_needed_connections(u'changed device') # type construction is deferred because we don't want loading this file to trigger loading plugins @exported_value( type_fn=lambda self: Enum({d.mode: d.label for d in get_modes()})) def get_mode(self): return self.mode @setter def set_mode(self, mode): mode = unicode(mode) if mode == self.mode: return if self.__demodulator and self.__demodulator.can_set_mode(mode): self.__demodulator.set_mode(mode) self.mode = mode else: self._rebuild_demodulator(mode=mode, reason=u'changed mode') # TODO: rename rec_freq to just freq @exported_value(type=float, parameter='freq_absolute') def get_rec_freq(self): return self.__freq_absolute @setter def set_rec_freq(self, absolute): absolute = float(absolute) if self.__freq_linked_to_device: # Temporarily violating the (device freq + relative freq = absolute freq) invariant, which will be restored below by changing the device freq. self.__freq_absolute = absolute else: self.__freq_absolute = absolute self.__freq_relative = absolute - self.__get_device().get_freq() self.__update_rotator() if self.__freq_linked_to_device: # TODO: reconsider whether we should be giving commands directly to the device, vs. going through the context. self.__get_device().set_freq(self.__freq_absolute - self.__freq_relative) else: self.context.revalidate(tuning=True) @exported_value(type=bool) def get_freq_linked_to_device(self): return self.__freq_linked_to_device @setter def set_freq_linked_to_device(self, value): self.__freq_linked_to_device = bool(value) # TODO: support non-audio demodulators at which point these controls should be optional @exported_value(parameter='audio_gain', type=Range([(-30, 20)], strict=False)) def get_audio_gain(self): return self.audio_gain @setter def set_audio_gain(self, value): self.audio_gain = value self.__update_audio_gain() @exported_value(type_fn=lambda self: Range( [(-1, 1)] if self.__audio_channels > 1 else [(0, 0)], strict=True)) def get_audio_pan(self): return self.audio_pan @setter def set_audio_pan(self, value): self.audio_pan = value self.__update_audio_gain() @exported_value( type_fn=lambda self: self.context.get_audio_destination_type()) def get_audio_destination(self): return self.__audio_destination @setter def set_audio_destination(self, value): if self.__audio_destination != value: self.__audio_destination = value self.context.changed_needed_connections(u'changed destination') @exported_value(type=bool) def get_is_valid(self): if self.__demodulator is None: return False half_sample_rate = self.__get_device().get_rx_driver().get_output_type( ).get_sample_rate() / 2 demod_shape = self.__demodulator.get_band_filter_shape() valid_bandwidth_lower = -half_sample_rate - self.__freq_relative valid_bandwidth_upper = half_sample_rate - self.__freq_relative return valid_bandwidth_lower <= min(0, demod_shape['low']) and \ valid_bandwidth_upper >= max(0, demod_shape['high']) # Note that the receiver cannot measure RF power because we don't know what the channel bandwidth is; we have to leave that to the demodulator. @exported_value(type=Range([(_audio_power_minimum_dB, 0)], strict=False)) def get_audio_power(self): if self.get_is_valid(): return todB( max(_audio_power_minimum_amplitude, self.probe_audio.level())) else: # will not be receiving samples, so probe's value will be meaningless return _audio_power_minimum_dB def __update_rotator(self): device = self.__get_device() sample_rate = device.get_rx_driver().get_output_type().get_sample_rate( ) if self.__demod_tunable: # TODO: Method should perhaps be renamed to convey that it is relative self.__demodulator.set_rec_freq(self.__freq_relative) else: self.__rotator.set_phase_inc( rotator_inc(rate=sample_rate, shift=-self.__freq_relative)) def __get_device(self): return self.context.get_device(self.__device_name) # called from facet def _rebuild_demodulator(self, mode=None, reason='<unspecified>'): self.__rebuild_demodulator_nodirty(mode) self.__do_connect(reason=u'demodulator rebuilt: %s' % (reason, )) # TODO write a test for this! #self.context.revalidaate(tuning=False) # in case our bandwidth changed def __rebuild_demodulator_nodirty(self, mode=None): if self.__demodulator is None: defaults = {} else: defaults = self.__demodulator.state_to_json() if mode is None: mode = self.mode self.__demodulator = self.__make_demodulator(mode, defaults) self.__update_demodulator_info() self.__update_rotator() self.mode = mode # Replace blocks downstream of the demodulator so as to flush samples that are potentially at a different sample rate and would therefore be audibly wrong. Caller will handle reconnection. self.__audio_gain_blocks = [ blocks.multiply_const_ff(0.0) for _ in xrange(self.__audio_channels) ] self.__update_audio_gain() def __make_demodulator(self, mode, state): """Returns the demodulator.""" t0 = time.time() mode_def = lookup_mode(mode) if mode_def is None: # TODO: Better handling, like maybe a dummy demod raise ValueError('Unknown mode: ' + mode) clas = mode_def.demod_class state = state.copy() # don't modify arg if 'mode' in state: del state[ 'mode'] # don't switch back to the mode we just switched from facet = ContextForDemodulator(self) init_kwargs = dict(mode=mode, input_rate=self.__get_device().get_rx_driver(). get_output_type().get_sample_rate(), context=facet) demodulator = unserialize_exported_state(ctor=clas, state=state, kwargs=init_kwargs) # until _enabled, ignore any callbacks resulting from unserialization calling setters facet._enabled = True log.msg('Constructed %s demodulator: %i ms.' % (mode, (time.time() - t0) * 1000)) return demodulator def __update_audio_gain(self): gain_lin = dB(self.audio_gain) if self.__audio_channels == 2: pan = self.audio_pan # TODO: Determine correct computation for panning. http://en.wikipedia.org/wiki/Pan_law seems relevant but was short on actual formulas. May depend on headphones vs speakers? This may be correct already for headphones -- it sounds nearly-flat to me. self.__audio_gain_blocks[0].set_k(gain_lin * (1 - pan)) self.__audio_gain_blocks[1].set_k(gain_lin * (1 + pan)) else: self.__audio_gain_blocks[0].set_k(gain_lin)
class _HamlibRig(_HamlibProxy): implements(IRig, IHasFrequency) _server_name = 'rigctld' _dummy_command = 'get_freq' _info = { 'Frequency': (Range([(0, 9999999999)], integer=True)), 'Mode': (_modes), 'Passband': (_passbands), 'VFO': (_vfos), 'RIT': (int), 'XIT': (int), 'PTT': (bool), 'DCD': (bool), 'Rptr Shift': (Enum({ '+': '+', '-': '-', 'None': 'None' }, strict=False)), 'Rptr Offset': (int), 'CTCSS Tone': (int), 'DCS Code': (str), 'CTCSS Sql': (int), 'DCS Sql': (str), 'TX Frequency': (int), 'TX Mode': (_modes), 'TX Passband': (_passbands), 'Split': (bool), 'TX VFO': (_vfos), 'Tuning Step': (int), 'Antenna': (int), } _commands = { 'freq': ['Frequency'], 'mode': ['Mode', 'Passband'], 'vfo': ['VFO'], 'rit': ['RIT'], 'xit': ['XIT'], # 'ptt': ['PTT'], # writing disabled until when we're more confident in correct functioning 'rptr_shift': ['Rptr Shift'], 'rptr_offs': ['Rptr Offset'], 'ctcss_tone': ['CTCSS Tone'], 'dcs_code': ['DCS Code'], 'ctcss_sql': ['CTCSS Sql'], 'dcs_sql': ['DCS Sql'], 'split_freq': ['TX Frequency'], 'split_mode': ['TX Mode', 'TX Passband'], 'split_vfo': ['Split', 'TX VFO'], 'ts': ['Tuning Step'], # TODO: describe func, level, parm 'ant': ['Antenna'], 'powerstat': ['Power Stat'], } def poll_fast(self, send): # likely to be set by hw controls send('get_freq') send('get_mode') # received signal info send('get_dcd') def poll_slow(self, send): send('get_vfo') send('get_rit') send('get_xit') send('get_ptt') send('get_rptr_shift') send('get_rptr_offs') send('get_ctcss_tone') send('get_dcs_code') send('get_split_freq') send('get_split_mode') send('get_split_vfo') send('get_ts')
_vfos = Enum( { 'VFOA': 'VFO A', 'VFOB': 'VFO B', 'VFOC': 'VFO C', 'currVFO': 'currVFO', 'VFO': 'VFO', 'MEM': 'MEM', 'Main': 'Main', 'Sub': 'Sub', 'TX': 'TX', 'RX': 'RX' }, strict=False) _passbands = Range([(0, 0)]) _cap_remap = { # TODO: Make this well-founded 'Ant': ['Antenna'], 'CTCSS Squelch': ['CTCSS Sql'], 'CTCSS': ['CTCSS Tone'], 'DCS Squelch': ['DCS Sql'], 'DCS': ['DCS Code'], 'Mode': ['Mode', 'Passband'], 'Repeater Offset': ['Rptr Offset', 'Rptr Shift'], 'Split Freq': ['TX Frequency'], 'Split Mode': ['TX Mode', 'TX Passband'], 'Split VFO': ['Split', 'TX VFO'], 'Position': ['Azimuth', 'Elevation'], }
def test_equal(self): self.assertEqual(Range([(1, 2), (3, 4)]), Range([(1, 2), (3, 4)])) self.assertEqual( Range([(1, 2), (3, 4)], integer=True, logarithmic=True), Range([(1, 2), (3, 4)], integer=True, logarithmic=True)) self.assertNotEqual(Range([(1, 2), (3, 4)]), Range([(0, 2), (3, 4)])) self.assertNotEqual(Range([(1, 2)]), Range([(1, 2)], integer=True)) self.assertNotEqual(Range([(1, 2)]), Range([(1, 2)], logarithmic=True)) self.assertNotEqual(Range([(1, 2)]), Range([(1, 2)], strict=False))
def test_bandwidth_default(self): self.assertEqual( Range([(-30000.0, 30000.0)]), OsmoSDRDevice('file=/dev/null,rate=80000').get_rx_driver(). get_usable_bandwidth())