Beispiel #1
0
class MaxProbe(gr.hier_block2, ExportedState):
    """
    A probe whose level is the maximum magnitude-squared occurring within the specified window of samples.
    
    Also provides a cell with a message assuming magnitudes over 1.0 are too high.
    """
    
    __ABSURD_MAGNITUDE_SQUARED = 2.01
    
    def __init__(self, window=10000):
        gr.hier_block2.__init__(
            self, type(self).__name__,
            gr.io_signature(1, 1, gr.sizeof_gr_complex),
            gr.io_signature(0, 0, 0))
        self.__sink = None  # quiet pylint
        self.__absurd = False
        self.set_window_and_reconnect(window)
    
    def level(self):
        # pylint: disable=method-hidden, no-self-use
        # overridden in instances
        raise Exception('This placeholder should never get called')
    
    def set_window_and_reconnect(self, window):
        """
        Must be called while the flowgraph is locked already.
        """
        
        self.__absurd = False
        
        # Use a power-of-2 window size to satisfy gnuradio allocation alignment without going overboard.
        window = int(2 ** math.floor(math.log(window, 2)))
        self.disconnect_all()
        self.__sink = blocks.probe_signal_f()
        self.connect(
            self,
            blocks.complex_to_mag_squared(),
            blocks.stream_to_vector(itemsize=gr.sizeof_float, nitems_per_block=window),
            blocks.max_ff(window),
            self.__sink)
        
        # shortcut method implementation
        self.level = self.__sink.level
    
    @exported_value(type=NoticeT(always_visible=False), changes='continuous')
    def get_clip_warning(self):
        if not self.__absurd:
            magnitude_squared = self.level()
            # We assume that our input's absolute limits on I and Q values are the range -1.0 to 1.0. This is a square region; therefore the magnitude observed can be up to sqrt(2) = 1.414 above this, allowing us some opportunity to measure the amount of excess, and also to detect clipping even if the device doesn't produce exactly +-1.0 values.
            if magnitude_squared >= self.__ABSURD_MAGNITUDE_SQUARED:
                # Oops, our assumption of 1.0 being the limit was wrong. Disable warning.
                self.__absurd = True
            elif magnitude_squared >= 1.0:
                return u'Input amplitude too high (%.2f \u2265 1.0). Reduce gain.' % math.sqrt(magnitude_squared)
        return u''
Beispiel #2
0
class _ElecraftRadio(ExportedState):
    # TODO: Tell protocol to do no/less polling when nobody is looking.

    def __init__(self, protocol):
        self.__protocol = protocol
        self.__rx_main = _ElecraftReceiver(protocol, False)
        self.__rx_sub = _ElecraftReceiver(protocol, True)
        self.__init_center_cell()

    def __init_center_cell(self):
        base_freq_cell = self.__rx_main.state()[_FREQ_CELL_KEY]
        mode_cell = self.__rx_main.state()['MD']
        sidetone_cell = self.state()['CW']
        submode_cell = self.state()['DT']
        iq_offset_cell = LooseCell(value=0.0, type=float, writable=True)

        self.__iq_center_cell = ViewCell(
            base=base_freq_cell,
            get_transform=lambda x: x + iq_offset_cell.get(),
            set_transform=lambda x: x - iq_offset_cell.get(),
            type=base_freq_cell.type(),  # runtime variable...
            writable=True,
            persists=base_freq_cell.metadata().persists)

        def changed_iq(_value=None):
            # TODO this is KX3-specific
            mode = mode_cell.get()
            if mode == 'CW':
                iq_offset = sidetone_cell.get()
            elif mode == 'CW-REV':
                iq_offset = -sidetone_cell.get()
            elif mode == 'AM' or mode == 'FM':
                iq_offset = 11000.0
            elif mode == 'DATA' or mode == 'DATA-REV':
                submode = submode_cell.get()
                if submode == 0:  # "DATA A", SSB with less processing
                    iq_offset = 0.0  # ???
                elif submode == 1:  # "AFSK A", SSB with RTTY style filter
                    iq_offset = 0.0  # ???
                elif submode == 2:  # "FSK D", RTTY
                    iq_offset = 900.0
                elif submode == 3:  # "PSK D", PSK31
                    iq_offset = 1000.0  # I think so...
                else:
                    iq_offset = 0  # fallback
                if mode == 'DATA-REV':
                    iq_offset = -iq_offset
            else:  # USB, LSB, other
                iq_offset = 0.0
            iq_offset_cell.set(iq_offset)
            self.__iq_center_cell.changed_transform()

        # TODO bad practice
        mode_cell._subscribe_immediate(changed_iq)
        sidetone_cell._subscribe_immediate(changed_iq)
        submode_cell._subscribe_immediate(changed_iq)
        changed_iq()

    def state_def(self):
        """overrides ExportedState"""
        for d in super(_ElecraftRadio, self).state_def():
            yield d
        for d in _st.install_cells(self, self.__protocol, is_sub=None):
            yield d

    def close(self):
        """implements IComponent"""
        self.__protocol.transport.loseConnection()

    def iq_center_cell(self):
        """Made available for Device creation; not a well-thought-out interface."""
        return self.__iq_center_cell

    def get_direct_protocol(self):
        """For experimental use only."""
        return self.__protocol

    @exported_value(type=NoticeT(always_visible=False),
                    changes='continuous')  # TODO better changes
    def get_errors(self):
        error = self.__protocol.get_communication_error()
        if not error:
            return u''
        elif error == u'not_responding':
            return u'Radio not responding.'
        elif error == u'bad_data':
            return u'Bad data from radio.'
        else:
            return six.text_type(error)

    @exported_value(type=ReferenceT(), changes='never')
    def get_rx_main(self):
        return self.__rx_main

    @exported_value(type=ReferenceT(), changes='never')
    def get_rx_sub(self):
        return self.__rx_sub
Beispiel #3
0
class _HamlibProxy(ExportedState):
    # pylint: disable=no-member
    """
    Abstract class for objects which export state proxied to a hamlib daemon.
    """
    def __init__(self, protocol, log):
        # info from hamlib
        self.__cache = {}
        self.__caps = {}
        self.__levels = []

        # invert command table
        # TODO: we only need to do this once per class, really
        self._how_to_command = {
            key: command
            for command, keys in self._commands.items() for key in keys
        }

        # keys are same as __cache, values are functions to call with new values from rig
        self._cell_updaters = {}

        self.__communication_error = False
        self.__last_error = (-1e9, '', 0)

        self.__log = log
        self.__protocol = protocol
        self.__disconnect_deferred = defer.Deferred()
        protocol._set_proxy(self)

        # TODO: If hamlib backend supports "transceive mode", use it in lieu of polling
        self.__poller_slow = LoopingCall(self.__poll_slow)
        self.__poller_fast = LoopingCall(self.__poll_fast)
        self.__poller_slow.start(2.0)
        self.__poller_fast.start(0.2)

        self._ready_deferred = protocol.rc_send('dump_caps')

    def sync(self):
        # TODO: Replace 'sync' with more specifically meaningful operations
        d = self.__protocol.rc_send(self._dummy_command)
        d.addCallback(lambda _: None)  # ignore result
        return d

    def close(self):
        """implements IComponent"""
        self.__protocol.transport.loseConnection()
        return self.when_closed()  # used for tests, not part of IComponent

    def attach_context(self, device_context):
        """implements IComponent"""

    def when_closed(self):
        return fork_deferred(self.__disconnect_deferred)

    def _ehs_get(self, name_in_cmd):
        if name_in_cmd in self.__cache:
            return self.__cache[name_in_cmd]
        else:
            return 0.0

    def _clientReceived(self, command, key, value):
        self.__communication_error = False

        if command == 'dump_caps':

            def write(key):
                self.__caps[key] = value
                if key == 'Get level':
                    # add to polling info
                    for info in value.strip().split(' '):
                        match = re.match(r'^(\w+)\([^()]+\)$', info)
                        # part in parens is probably min/max/step info, but we don't have any working examples to test against (they are all 0)
                        if match:
                            self.__levels.append(match.group(1))
                        else:
                            self.__log.error(
                                'Unrecognized level description from %s: %r' %
                                (self._server_name,
                                 info))  # TODO use logger formatting

            # remove irregularity
            keymatch = re.match(r'(Can [gs]et )([\w\s,/-]+)', key)
            if keymatch and keymatch.group(2) in _cap_remap:
                for mapped in _cap_remap[keymatch.group(2)]:
                    write(keymatch.group(1) + mapped)
            else:
                write(key)
        else:
            self.__update_cache_and_cells(key, value)

    def _clientReceivedLevel(self, level_name, value_str):
        self.__update_cache_and_cells(level_name + ' level', value_str)

    def _clientError(self, cmd, error_number):
        if cmd.startswith('get_'):
            # these getter failures are boring, probably us polling something not implemented
            if error_number == RIG_ENIMPL or error_number == RIG_ENTARGET or error_number == RIG_BUSERROR:
                return
            elif error_number == RIG_ETIMEOUT:
                self.__communication_error = True
                return
        self.__last_error = (time.time(), cmd, error_number)
        self.state_changed('errors')

    def __update_cache_and_cells(self, key, value):
        self.__cache[key] = value
        if key in self._cell_updaters:
            self._cell_updaters[key](value)

    def _clientConnectionLost(self, reason):
        self.__poller_slow.stop()
        self.__poller_fast.stop()
        self.__disconnect_deferred.callback(None)

    def _ehs_set(self, name_full, value):
        if not isinstance(value, str):
            raise TypeError()
        name_in_cmd = self._how_to_command[name_full]  # raises if cannot set
        if value != self.__cache[name_full]:
            self.__cache[name_full] = value
            self.__protocol.rc_send(
                'set_' + name_in_cmd,
                ' '.join(self.__cache[arg_name]
                         for arg_name in self._commands[name_in_cmd]))

    def state_def(self):
        for d in super(_HamlibProxy, self).state_def():
            yield d
        for name in self._info:
            can_get = self.__caps.get('Can get ' + name)
            if can_get is None:
                self.__log.warn('No can-get information for ' +
                                name)  # TODO use logger formatting
            if can_get != 'Y':
                # TODO: Handle 'E' condition
                continue
            writable = name in self._how_to_command and self.__caps.get(
                'Can set ' + name) == 'Y'
            yield _install_cell(self, name, False, writable, self.__caps)
        for level_name in self.__levels:
            # TODO support writable levels
            yield _install_cell(self, level_name + ' level', True, False,
                                self.__caps)

    def __poll_fast(self):
        # TODO: Stop if we're getting behind
        p = self.__protocol
        self.poll_fast(p.rc_send)
        for level_name in self.__levels:
            p.rc_send('get_level', level_name)

    def __poll_slow(self):
        # TODO: Stop if we're getting behind
        p = self.__protocol
        self.poll_slow(p.rc_send)

    @exported_value(type=NoticeT(always_visible=False), changes='explicit')
    def get_errors(self):
        if self.__communication_error:
            return 'Rig not responding.'
        else:
            (error_time, cmd, error_number) = self.__last_error
            if error_time > time.time() - 10:
                return u'%s: %s' % (cmd, error_number)
            else:
                return u''

    def poll_fast(self, send):
        raise NotImplementedError()

    def poll_slow(self, send):
        raise NotImplementedError()
Beispiel #4
0
class Top(gr.top_block, ExportedState, RecursiveLockBlockMixin):
    def __init__(self, devices={}, audio_config=None, features=_stub_features):
        # pylint: disable=dangerous-default-value
        if len(devices) <= 0:
            raise ValueError('Must have at least one RF device')

        gr.top_block.__init__(self, "SDR top block")
        self.__running = False  # duplicate of GR state we can't reach, see __start_or_stop
        self.__has_a_useful_receiver = False

        # Configuration
        # TODO: device refactoring: Remove vestigial 'accessories'
        self._sources = CellDict(
            {k: d
             for k, d in devices.iteritems() if d.can_receive()})
        self._accessories = accessories = {
            k: d
            for k, d in devices.iteritems() if not d.can_receive()
        }
        for key in self._sources:
            # arbitrary valid initial value
            self.source_name = key
            break
        self.__rx_device_type = EnumT(
            {k: v.get_name() or k
             for (k, v) in self._sources.iteritems()})

        # Audio early setup
        self.__audio_manager = AudioManager(  # must be before contexts
            graph=self,
            audio_config=audio_config,
            stereo=features['stereo'])

        # Blocks etc.
        # TODO: device refactoring: remove 'source' concept (which is currently a device)
        # TODO: remove legacy no-underscore names, maybe get rid of self.source
        self.source = None
        self.__monitor_rx_driver = None
        self.monitor = MonitorSink(
            signal_type=SignalType(
                sample_rate=10000,
                kind='IQ'),  # dummy value will be updated in _do_connect
            context=Context(self))
        self.monitor.get_interested_cell().subscribe2(
            lambda value: self.__start_or_stop_later, the_subscription_context)
        self.__clip_probe = MaxProbe()

        # Receiver blocks (multiple, eventually)
        self._receivers = CellDict(dynamic=True)
        self._receiver_valid = {}

        # collections
        # TODO: No longer necessary to have these non-underscore names
        self.sources = CollectionState(CellDict(self._sources))
        self.receivers = ReceiverCollection(self._receivers, self)
        self.accessories = CollectionState(CellDict(accessories))
        self.__telemetry_store = TelemetryStore()

        # Flags, other state
        self.__needs_reconnect = [u'initialization']
        self.__in_reconnect = False
        self.receiver_key_counter = 0
        self.receiver_default_state = {}
        self.__cpu_calculator = LazyRateCalculator(lambda: time.clock())

        # Initialization

        def hookup_vfo_callback(
                k, d):  # function so as to not close over loop variable
            d.get_vfo_cell().subscribe2(
                lambda value: self.__device_vfo_callback(k),
                the_subscription_context)

        for k, d in devices.iteritems():
            hookup_vfo_callback(k, d)

        self._do_connect()

    def add_receiver(self, mode, key=None, state=None):
        if len(self._receivers) >= 100:
            # Prevent storage-usage DoS attack
            raise Exception('Refusing to create more than 100 receivers')

        if key is not None:
            assert key not in self._receivers
        else:
            while True:
                key = base26(self.receiver_key_counter)
                self.receiver_key_counter += 1
                if key not in self._receivers:
                    break

        if len(self._receivers) > 0:
            arbitrary = self._receivers.itervalues().next()
            defaults = arbitrary.state_to_json()
        else:
            defaults = self.receiver_default_state

        combined_state = defaults.copy()
        for do_not_use_default in ['device_name', 'freq_linked_to_device']:
            if do_not_use_default in combined_state:
                del combined_state[do_not_use_default]
        if state is not None:
            combined_state.update(state)

        facet = ContextForReceiver(self, key)
        receiver = unserialize_exported_state(
            Receiver,
            kwargs=dict(
                mode=mode,
                audio_channels=self.__audio_manager.get_channels(),
                device_name=self.source_name,
                audio_destination=self.__audio_manager.get_default_destination(
                ),  # TODO match others
                context=facet,
            ),
            state=combined_state)
        facet._receiver = receiver
        self._receivers[key] = receiver
        self._receiver_valid[key] = False

        self.__needs_reconnect.append(u'added receiver ' + key)
        self._do_connect()

        # until _enabled, the facet ignores any reconnect/rebuild-triggering callbacks
        facet._enabled = True

        return (key, receiver)

    def delete_receiver(self, key):
        assert key in self._receivers
        receiver = self._receivers[key]

        # save defaults for use if about to become empty
        if len(self._receivers) == 1:
            self.receiver_default_state = receiver.state_to_json()

        del self._receivers[key]
        del self._receiver_valid[key]
        self.__needs_reconnect.append(u'removed receiver ' + key)
        self._do_connect()

    # TODO move these methods to a facet of AudioManager
    def add_audio_queue(self, queue, queue_rate):
        self.__audio_manager.add_audio_queue(queue, queue_rate)
        self.__needs_reconnect.append(u'added audio queue')
        self._do_connect()
        self.__start_or_stop()

    def remove_audio_queue(self, queue):
        self.__audio_manager.remove_audio_queue(queue)
        self.__start_or_stop()
        self.__needs_reconnect.append(u'removed audio queue')
        self._do_connect()

    def get_audio_queue_channels(self):
        """
        Return the number of channels (which will be 1 or 2) in audio queue outputs.
        """
        return self.__audio_manager.get_channels()

    def _do_connect(self):
        """Do all reconfiguration operations in the proper order."""

        if self.__in_reconnect:
            raise Exception('reentrant reconnect or _do_connect crashed')
        self.__in_reconnect = True

        t0 = time.time()
        if self.source is not self._sources[self.source_name]:
            log.msg('Flow graph: Switching RF device to %s' %
                    (self.source_name))
            self.__needs_reconnect.append(u'switched device')

            this_source = self._sources[self.source_name]

            self.source = this_source
            self.state_changed('source')
            self.__monitor_rx_driver = this_source.get_rx_driver()
            monitor_signal_type = self.__monitor_rx_driver.get_output_type()
            self.monitor.set_signal_type(monitor_signal_type)
            self.monitor.set_input_center_freq(this_source.get_freq())
            self.__clip_probe.set_window_and_reconnect(
                0.5 * monitor_signal_type.get_sample_rate())

        if self.__needs_reconnect:
            log.msg(u'Flow graph: Rebuilding connections because: %s' %
                    (', '.join(self.__needs_reconnect), ))
            self.__needs_reconnect = []

            self._recursive_lock()
            self.disconnect_all()

            self.connect(self.__monitor_rx_driver, self.monitor)
            self.connect(self.__monitor_rx_driver, self.__clip_probe)

            # Filter receivers
            audio_rs = self.__audio_manager.reconnecting()
            n_valid_receivers = 0
            has_non_audio_receiver = False
            for key, receiver in self._receivers.iteritems():
                self._receiver_valid[key] = receiver.get_is_valid()
                if not self._receiver_valid[key]:
                    continue
                if not self.__audio_manager.validate_destination(
                        receiver.get_audio_destination()):
                    log.err(
                        'Flow graph: receiver audio destination %r is not available'
                        % (receiver.get_audio_destination(), ))
                    continue
                n_valid_receivers += 1
                if n_valid_receivers > 6:
                    # Sanity-check to avoid burning arbitrary resources
                    # TODO: less arbitrary constant; communicate this restriction to client
                    log.err(
                        'Flow graph: Refusing to connect more than 6 receivers'
                    )
                    break
                self.connect(
                    self._sources[receiver.get_device_name()].get_rx_driver(),
                    receiver)
                receiver_output_type = receiver.get_output_type()
                if receiver_output_type.get_sample_rate() <= 0:
                    # Demodulator has no output, but receiver has a dummy output, so connect it to something to satisfy flow graph structure.
                    self.connect(
                        receiver,
                        blocks.null_sink(gr.sizeof_float *
                                         self.__audio_manager.get_channels()))
                    # Note that we have a non-audio receiver which may be useful even if there is no audio output
                    has_non_audio_receiver = True
                else:
                    assert receiver_output_type.get_kind() == 'STEREO'
                    audio_rs.input(receiver,
                                   receiver_output_type.get_sample_rate(),
                                   receiver.get_audio_destination())

            self.__has_a_useful_receiver = audio_rs.finish_bus_connections() or \
                has_non_audio_receiver

            self._recursive_unlock()
            # (this is in an if block but it can't not execute if anything else did)
            log.msg('Flow graph: ...done reconnecting (%i ms).' %
                    ((time.time() - t0) * 1000, ))

            self.__start_or_stop_later()

        self.__in_reconnect = False

    def __device_vfo_callback(self, device_key):
        reactor.callLater(
            self._sources[device_key].get_rx_driver().get_tune_delay(),
            self.__device_vfo_changed, device_key)

    def __device_vfo_changed(self, device_key):
        device = self._sources[device_key]
        freq = device.get_freq()
        if self.source is device:
            self.monitor.set_input_center_freq(freq)
        for rec_key, receiver in self._receivers.iteritems():
            if receiver.get_device_name() == device_key:
                receiver.changed_device_freq()
                self._update_receiver_validity(rec_key)
            # TODO: If multiple receivers change validity we'll do redundant reconnects in this loop; avoid that.

    def _update_receiver_validity(self, key):
        receiver = self._receivers[key]
        if receiver.get_is_valid() != self._receiver_valid[key]:
            self.__needs_reconnect.append(u'receiver %s validity changed' %
                                          (key, ))
            self._do_connect()

    @exported_value(type=ReferenceT(), changes='never')
    def get_monitor(self):
        return self.monitor

    @exported_value(type=ReferenceT(), persists=False, changes='never')
    def get_sources(self):
        return self.sources

    @exported_value(type=ReferenceT(), persists=False, changes='explicit')
    def get_source(self):
        return self.source  # TODO no need for this now...?

    @exported_value(type=ReferenceT(), changes='never')
    def get_receivers(self):
        return self.receivers

    # TODO the concept of 'accessories' is old and needs to go away, but we don't have a flexible enough UI to replace it with just devices since only one device can be looked-at at a time so far.
    @exported_value(type=ReferenceT(), persists=False, changes='never')
    def get_accessories(self):
        return self.accessories

    @exported_value(type=ReferenceT(), changes='never', label='Telemetry')
    def get_telemetry_store(self):
        return self.__telemetry_store

    def start(self, **kwargs):
        # trigger reconnect/restart notification
        self._recursive_lock()
        self._recursive_unlock()

        super(Top, self).start(**kwargs)
        self.__running = True

    def stop(self):
        super(Top, self).stop()
        self.__running = False

    def __start_or_stop(self):
        # TODO: Improve start/stop conditions:
        #
        # * run if a client is watching an audio-having receiver's cell-based outputs (e.g. VOR) but not listening to audio
        #
        # * don't run if no client is watching a pure telemetry receiver
        #   (maybe a user preference since having a history when you connect is useful)
        #
        # Both of these refinements require becoming aware of cell subscriptions.
        should_run = (self.__has_a_useful_receiver
                      or self.monitor.get_interested_cell().get())
        if should_run != self.__running:
            if should_run:
                self.start()
            else:
                self.stop()
                self.wait()

    def __start_or_stop_later(self):
        reactor.callLater(0, self.__start_or_stop)

    def close_all_devices(self):
        """Close all devices in preparation for a clean shutdown.
        
        Makes this top block unusable"""
        for device in self._sources.itervalues():
            device.close()
        for device in self._accessories.itervalues():
            device.close()
        self.stop()
        self.wait()

    @exported_value(type_fn=lambda self: self.__rx_device_type,
                    changes='this_setter',
                    label='RF source')
    def get_source_name(self):
        return self.source_name

    @setter
    def set_source_name(self, value):
        if value == self.source_name:
            return
        if value not in self._sources:
            raise ValueError('Source %r does not exist' % (value, ))
        self.source_name = value
        self._do_connect()

    @exported_value(type=NoticeT(always_visible=False), changes='continuous')
    def get_clip_warning(self):
        level = self.__clip_probe.level()
        # We assume that our sample source's absolute limits on I and Q values are the range -1.0 to 1.0. This is a square region; therefore the magnitude observed can be up to sqrt(2) = 1.414 above this, allowing us some opportunity to measure the amount of excess, and also to detect clipping even if the device doesn't produce exactly +-1.0 valus.
        if level >= 1.0:
            return u'Input amplitude too high (%.2f \u2265 1.0). Reduce gain.' % math.sqrt(
                level)
        else:
            return u''

    # TODO: This becomes useless w/ Session fix
    @exported_value(type=float, changes='continuous')
    def get_cpu_use(self):
        return round(self.__cpu_calculator.get(), 2)

    def _get_rx_device_type(self):
        """for ContextForReceiver only"""
        return self.__rx_device_type

    def _get_audio_destination_type(self):
        """for ContextForReceiver only"""
        return self.__audio_manager.get_destination_type()

    def _trigger_reconnect(self, reason):
        self.__needs_reconnect.append(reason)
        self._do_connect()

    def _recursive_lock_hook(self):
        for source in self._sources.itervalues():
            source.notify_reconnecting_or_restarting()
Beispiel #5
0
class APRSStation(ExportedState):
    def __init__(self, object_id):
        self.__last_heard_time = None
        self.__address = object_id
        self.__track = empty_track
        self.__status = u''
        self.__symbol = u''
        self.__last_comment = u''
        self.__last_parse_error = u''

    def receive(self, message):
        """implement ITelemetryObject"""
        self.__last_heard_time = message.receive_time
        for fact in message.facts:
            if isinstance(fact, KillObject):
                # Kill by pretending the object is ancient.
                self.__last_heard_time = 0
            if isinstance(fact, Position):
                self.__track = self.__track._replace(
                    latitude=TelemetryItem(fact.latitude,
                                           message.receive_time),
                    longitude=TelemetryItem(fact.longitude,
                                            message.receive_time),
                )
            if isinstance(fact, Altitude):
                conversion = _FEET_TO_METERS if fact.feet_not_meters else 1
                self.__track = self.__track._replace(altitude=TelemetryItem(
                    fact.value * conversion, message.receive_time), )
            if isinstance(fact, Velocity):
                self.__track = self.__track._replace(
                    h_speed=TelemetryItem(
                        fact.speed_knots * _KNOTS_TO_METERS_PER_SECOND,
                        message.receive_time),
                    track_angle=TelemetryItem(fact.course_degrees,
                                              message.receive_time),
                )
            elif isinstance(fact, Status):
                # TODO: Empirically, not always ASCII. Move this implicit decoding off into parse stages.
                self.__status = unicode(fact.text)
            elif isinstance(fact, Symbol):
                self.__symbol = unicode(fact.id)
            else:
                # TODO: Warn somewhere in this case (recognized by parser but not here)
                pass
        self.__last_comment = unicode(message.comment)
        if len(message.errors) > 0:
            self.__last_parse_error = '; '.join(message.errors)
        self.state_changed()

    def is_interesting(self):
        """implement ITelemetryObject"""
        return True

    def get_object_expiry(self):
        """implement ITelemetryObject"""
        return self.__last_heard_time + drop_unheard_timeout_seconds

    @exported_value(type=TimestampT(),
                    changes='explicit',
                    sort_key='100',
                    label='Last heard')
    def get_last_heard_time(self):
        return self.__last_heard_time

    @exported_value(type=unicode,
                    changes='explicit',
                    label='Address/object ID')
    def get_address(self):
        return self.__address

    @exported_value(type=Track, changes='explicit', sort_key='010', label='')
    def get_track(self):
        return self.__track

    @exported_value(type=unicode,
                    changes='explicit',
                    sort_key='020',
                    label='Symbol')
    def get_symbol(self):
        """APRS symbol table identifier and symbol."""
        return self.__symbol

    @exported_value(type=unicode,
                    changes='explicit',
                    sort_key='080',
                    label='Status')
    def get_status(self):
        """String status text."""
        return self.__status

    @exported_value(type=unicode,
                    changes='explicit',
                    sort_key='090',
                    label='Last message comment')
    def get_last_comment(self):
        return self.__last_comment

    @exported_value(type=NoticeT(always_visible=False),
                    sort_key='000',
                    changes='explicit')
    def get_last_parse_error(self):
        return self.__last_parse_error