class _OsmoSDRRXDriver(ExportedState, gr.hier_block2): # Note: Docs for gr-osmosdr are in comments at gr-osmosdr/lib/source_iface.h def __init__(self, osmo_device, source, profile, name, tuning): gr.hier_block2.__init__( self, b'RX ' + str(name), gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__osmo_device = osmo_device self.__source = source self.__profile = profile self.__name = name self.__tuning = tuning self.__antenna_type = EnumT({unicode(name): unicode(name) for name in self.__source.get_antennas()}, strict=True) self.connect(self.__source, self) self.__gains = Gains(source, self) # State of the source that there are no getters for, so we must keep our own copy of self.__track_dc_offset_mode = DCOffsetOff self.__track_iq_balance_mode = IQBalanceOff source.set_dc_offset_mode(self.__track_dc_offset_mode, ch) source.set_iq_balance_mode(self.__track_iq_balance_mode, ch) # Blocks self.__state_while_inactive = {} self.__placeholder = blocks.vector_source_c([]) sample_rate = float(source.get_sample_rate()) self.__signal_type = SignalType( kind='IQ', sample_rate=sample_rate) self.__usable_bandwidth = tuning.calc_usable_bandwidth(sample_rate) @exported_value(type=SignalType, changes='never') def get_output_type(self): return self.__signal_type # implement IRXDriver def get_tune_delay(self): return self.__profile.tune_delay # implement IRXDriver def get_usable_bandwidth(self): return self.__usable_bandwidth # implement IRXDriver def close(self): self._stop_rx() self.__tuning = None @exported_value( type=QuantityT(unit=units.ppm), changes='this_setter', label='Freq.corr.') def get_correction_ppm(self): return self.__tuning.get_correction_ppm() @setter def set_correction_ppm(self, value): self.__tuning.set_correction_ppm(value) @exported_value(type=ReferenceT(), changes='never') def get_gains(self): return self.__gains @exported_value( type_fn=lambda self: convert_osmosdr_range( self.__source.get_gain_range(ch), unit=units.dB, strict=False), changes='this_setter', label='Gain') def get_gain(self): if self.__source is None: return 0.0 return self.__source.get_gain(ch) @setter def set_gain(self, value): self.__source.set_gain(float(value), ch) # The single gain and individual-stage gain controls have an unspecified relationship to each other. Thus, changing one must poll the other. self.__gains.state_changed() @exported_value( type_fn=lambda self: bool if self.__profile.agc else ConstantT(False), changes='this_setter', label='AGC on') def get_agc(self): if self.__source is None: return False return bool(self.__source.get_gain_mode(ch)) @setter def set_agc(self, value): self.__source.set_gain_mode(bool(value), ch) @exported_value( type_fn=lambda self: self.__antenna_type, changes='this_setter', label='Antenna') def get_antenna(self): if self.__source is None: return '' return unicode(self.__source.get_antenna(ch)) @setter def set_antenna(self, value): # TODO we should have a provision for restricting antenna selection when transmit is possible to avoid hardware damage self.__source.set_antenna(str(self.__antenna_type(value)), ch) # Note: dc_offset_mode has a 'manual' mode we are not yet exposing, which is why the internal tracking is an enum integer but the exported value is a boolean @exported_value( type_fn=lambda self: bool if self.__profile.dc_cancel else ConstantT(False), changes='this_setter', label='Use DC cancellation') def get_dc_cancel(self): return bool(self.__track_dc_offset_mode) @setter def set_dc_cancel(self, value): if value: mode = DCOffsetAutomatic else: mode = DCOffsetOff self.__source.set_dc_offset_mode(mode, ch) self.__track_dc_offset_mode = mode # Note: iq_balance_mode has a 'manual' mode we are not yet exposing, which is why the internal tracking is an enum integer but the exported value is a boolean @exported_value(type=bool, # TODO: detect gr-iqbal changes='this_setter', label='Use IQ balancer') def get_iq_balance(self): return bool(self.__track_iq_balance_mode) @setter def set_iq_balance(self, value): if value: mode = IQBalanceAutomatic else: mode = IQBalanceOff self.__source.set_iq_balance_mode(mode, ch) self.__track_iq_balance_mode = mode # add_zero because zero means automatic setting based on sample rate. # TODO: Display automaticness in the UI rather than having a zero value. @exported_value( type_fn=lambda self: convert_osmosdr_range( self.__source.get_bandwidth_range(ch), unit=units.Hz, add_zero=True), changes='this_setter', label='Analog bandwidth', description='Bandwidth of the analog antialiasing filter.') def get_bandwidth(self): if self.__source is None: return 0.0 return self.__source.get_bandwidth(ch) @setter def set_bandwidth(self, value): self.__source.set_bandwidth(float(value), ch) def notify_reconnecting_or_restarting(self): pass # link to tx driver def _stop_rx(self): self.disconnect_all() self.__state_while_inactive = self.state_to_json() self.__tuning.set_block(None) self.__gains.close() self.__source = None self.connect(self.__placeholder, self) # link to tx driver def _start_rx(self): self.disconnect_all() self.__source = osmosdr.source('numchan=1 ' + self.__osmo_device) self.__source.set_sample_rate(self.__signal_type.get_sample_rate()) self.__tuning.set_block(self.__source) self.__gains = Gains(self.__source, self) self.connect(self.__source, self) self.state_from_json(self.__state_while_inactive)
class VOR(SimpleAudioDemodulator): def __init__(self, mode='VOR', zero_point=59, **kwargs): self.channel_rate = channel_rate = 40000 internal_audio_rate = 20000 # TODO over spec'd self.zero_point = zero_point transition = 5000 SimpleAudioDemodulator.__init__(self, mode=mode, audio_rate=internal_audio_rate, demod_rate=channel_rate, band_filter=fm_subcarrier * 1.25 + fm_deviation + transition / 2, band_filter_transition=transition, **kwargs) self.dir_rate = dir_rate = 10 if internal_audio_rate % dir_rate != 0: raise ValueError( 'Audio rate %s is not a multiple of direction-finding rate %s' % (internal_audio_rate, dir_rate)) self.dir_scale = dir_scale = internal_audio_rate // dir_rate self.audio_scale = audio_scale = channel_rate // internal_audio_rate self.zeroer = blocks.add_const_vff((zero_point * (math.pi / 180), )) self.dir_vector_filter = grfilter.fir_filter_ccf( 1, firdes.low_pass(1, dir_rate, 1, 2, firdes.WIN_HAMMING, 6.76)) self.am_channel_filter_block = grfilter.fir_filter_ccf( 1, firdes.low_pass(1, channel_rate, 5000, 5000, firdes.WIN_HAMMING, 6.76)) self.goertzel_fm = fft.goertzel_fc(channel_rate, dir_scale * audio_scale, 30) self.goertzel_am = fft.goertzel_fc(internal_audio_rate, dir_scale, 30) self.fm_channel_filter_block = grfilter.freq_xlating_fir_filter_ccc( 1, (firdes.low_pass(1.0, channel_rate, fm_subcarrier / 2, fm_subcarrier / 2, firdes.WIN_HAMMING)), fm_subcarrier, channel_rate) self.multiply_conjugate_block = blocks.multiply_conjugate_cc(1) self.complex_to_arg_block = blocks.complex_to_arg(1) self.am_agc_block = analog.feedforward_agc_cc(1024, 1.0) self.am_demod_block = analog.am_demod_cf( channel_rate=channel_rate, audio_decim=audio_scale, audio_pass=5000, audio_stop=5500, ) self.fm_demod_block = analog.quadrature_demod_cf(1) self.phase_agc_fm = analog.agc2_cc(1e-1, 1e-2, 1.0, 1.0) self.phase_agc_am = analog.agc2_cc(1e-1, 1e-2, 1.0, 1.0) self.probe = blocks.probe_signal_f() self.audio_filter_block = grfilter.fir_filter_fff( 1, design_lofi_audio_filter(internal_audio_rate, False)) ################################################## # Connections ################################################## # Input self.connect(self, self.band_filter_block) # AM chain self.connect(self.band_filter_block, self.am_channel_filter_block, self.am_agc_block, self.am_demod_block) # AM audio self.connect( self.am_demod_block, blocks.multiply_const_ff(1.0 / audio_modulation_index * 0.5), self.audio_filter_block) self.connect_audio_output(self.audio_filter_block) # AM phase self.connect(self.am_demod_block, self.goertzel_am, self.phase_agc_am, (self.multiply_conjugate_block, 0)) # FM phase self.connect(self.band_filter_block, self.fm_channel_filter_block, self.fm_demod_block, self.goertzel_fm, self.phase_agc_fm, (self.multiply_conjugate_block, 1)) # Phase comparison and output self.connect( self.multiply_conjugate_block, self.dir_vector_filter, self.complex_to_arg_block, blocks.multiply_const_ff(-1), # opposite angle conventions self.zeroer, self.probe) @exported_value(type=QuantityT(units.degree), changes='this_setter', label='Zero') def get_zero_point(self): return self.zero_point @setter def set_zero_point(self, zero_point): self.zero_point = zero_point self.zeroer.set_k((self.zero_point * (math.pi / 180), )) # TODO: Have a dedicated angle type which can be specified as referenced to true/magnetic north @exported_value(type=QuantityT(units.degree), changes='continuous', label='Bearing') def get_angle(self): return self.probe.level()
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), gr.io_signature(1, 1, gr.sizeof_float * audio_channels)) 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_block = blocks.multiply_const_vff([0.0] * 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: # Construct stereo-to-mono conversion (used at least for level probe) if self.__demod_stereo: splitter = blocks.vector_to_streams(gr.sizeof_float, 2) mono_audio = blocks.multiply_matrix_ff(((0.5, 0.5), )) self.connect(self.__demodulator, splitter) self.connect((splitter, 0), (mono_audio, 0)) self.connect((splitter, 1), (mono_audio, 1)) else: mono_audio = self.__demodulator # Connect mono audio to level probe self.connect(mono_audio, self.probe_audio) # Connect demodulator to output gain control, converting as needed if (self.__audio_channels == 2) == self.__demod_stereo: # stereo to stereo or mono to mono self.connect(self.__demodulator, self.__audio_gain_block) elif self.__audio_channels == 2 and not self.__demod_stereo: # mono to stereo duplicator = blocks.streams_to_vector(gr.sizeof_float, 2) self.connect(self.__demodulator, (duplicator, 0)) self.connect(self.__demodulator, (duplicator, 1)) self.connect(duplicator, self.__audio_gain_block) elif self.__audio_channels == 1 and self.__demod_stereo: # stereo to mono self.connect(mono_audio, self.__audio_gain_block) else: raise Exception('shouldn\'t happen') # Connect gain control to output of receiver self.connect(self.__audio_gain_block, self) else: # Dummy output, ignored by containing block self.connect( blocks.vector_source_f([], vlen=self.__audio_channels), self) 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 self.state_changed('rec_freq') self.state_changed('is_valid') @exported_value(type=ReferenceT(), changes='explicit') def get_demodulator(self): return self.__demodulator @exported_value(type_fn=lambda self: self.context.get_rx_device_type(), changes='this_setter', label='RF source') 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: EnumT({d.mode: d.info for d in get_modes()}), changes='this_setter', label='Mode') 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=QuantityT(units.Hz), parameter='freq_absolute', changes='explicit', label='Frequency') 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) self.state_changed('rec_freq') self.state_changed('is_valid') @exported_value( type=bool, changes='this_setter', label='Follow device', description= 'When this receiver\'s frequency or the device\'s frequency is changed, maintain the relative offset between them.' ) 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=RangeT([(-30, 20)], unit=units.dB, strict=False), changes='this_setter', label='Volume') 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: RangeT( [(-1, 1)] if self.__audio_channels > 1 else [(0, 0)], strict=True), changes='this_setter', label='Pan') 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(), changes='this_setter', label='Audio destination') 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, changes='explicit') 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. # TODO: document what we are using as the reference level. It's not dBFS because we're floating-point and before the gain stage. @exported_value(type=RangeT([(_audio_power_minimum_dB, 0)], unit=units.dB, strict=False), changes='continuous', label='Audio power') def get_audio_power(self): if self.get_is_valid(): return to_dB( 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 showing that revalidate is needed and works self.context.revalidate(tuning=False) # in case our bandwidth changed self.state_changed('is_valid') 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 self.state_changed('demodulator') # 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_block = blocks.multiply_const_vff( [0.0] * 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: Instead of left-to-left and right-to-right, panning other than center should mix left and right content. (A "pan law" defines the proper mix.) This implies a matrix multiplication type operation. self.__audio_gain_block.set_k([ gain_lin * (1 - pan), gain_lin * (1 + pan), ]) else: self.__audio_gain_block.set_k([gain_lin])
class WSPRStation(ExportedState): __last_heard = 0 __snr = None __frequency = None __call = None __grid = None __txpower = None def __init__(self, object_id): pass def receive(self, message): self.__last_heard = message.time self.__snr = message.snr self.__frequency = message.frequency self.__call = message.call self.__grid = message.grid self.__txpower = message.txpower self.state_changed() def is_interesting(self): """Every WSPR message is about as interesting as another, I suppose.""" return True def get_object_expiry(self): return self.__last_heard + 30 * MINUTES @exported_value(type=TimestampT(), changes='explicit', label='Last heard') def get_last_heard(self): return self.__last_heard @exported_value(type=QuantityT(units.dB), changes='explicit', label='SNR') def get_snr(self): return self.__snr or -999 @exported_value(type=QuantityT(units.MHz), changes='explicit', label='Frequency') def get_frequency(self): return self.__frequency or 0 @exported_value(type=unicode, changes='explicit', label='Call') def get_call(self): return self.__call or '' @exported_value(type=unicode, changes='explicit', label='Grid') def get_grid(self): return self.__grid or '' @exported_value(type=QuantityT(units.dBm), changes='explicit', label='Tx Power') def get_txpower(self): return self.__txpower or 0 @exported_value(type=Track, changes='explicit', label='Track') def get_track(self): if self.__grid: latitude, longitude = grid_to_lat_long(self.__grid) track = Track(latitude=TelemetryItem(latitude, self.__last_heard), longitude=TelemetryItem(longitude, self.__last_heard)) return track else: return empty_track