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''
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
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()
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()
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