コード例 #1
0
ファイル: osmosdr.py プロジェクト: vpoluyaktov/shinysdr
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)
コード例 #2
0
ファイル: __init__.py プロジェクト: vpoluyaktov/shinysdr
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()
コード例 #3
0
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])
コード例 #4
0
ファイル: telemetry.py プロジェクト: vpoluyaktov/shinysdr
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