class Top(gr.top_block, ExportedState, RecursiveLockBlockMixin): def __init__(self, devices={}, audio_config=None, stereo=True): if not 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 = {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()} self.source_name = self._sources.keys()[0] # arbitrary valid initial value self.__rx_device_type = Enum({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=stereo) # Blocks etc. # TODO: device refactoring: remove 'source' concept (which is currently a device) 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().subscribe(self.__start_or_stop_later) self.__clip_probe = MaxProbe() # Receiver blocks (multiple, eventually) self._receivers = {} self._receiver_valid = {} self.__shared_objects = {} # kludge for using collection like block - TODO: better architecture self.sources = CollectionState(self._sources) self.receivers = ReceiverCollection(self._receivers, self) self.accessories = CollectionState(accessories) # TODO: better name than "shared objects" self.shared_objects = CollectionState(self.__shared_objects, dynamic=True) # 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().subscribe(lambda: self.__device_vfo_callback(k)) 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() if 'device_name' in combined_state: del combined_state['device_name'] # should not be overridden if state is not None: combined_state.update(state) facet = ContextForReceiver(self, key) receiver = Receiver( 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, ) receiver.state_from_json(combined_state) # TODO: Use unserialize_exported_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.__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 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) audio_rs.input(receiver, receiver.get_output_type().get_sample_rate(), receiver.get_audio_destination()) self.__has_a_useful_receiver = audio_rs.finish_bus_connections() 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): # Note that in addition to the flow graph delay, the callLater is also needed in order to ensure we don't do our reconfiguration in the middle of the source's own workings. 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() def state_def(self, callback): super(Top, self).state_def(callback) # TODO make this possible to be decorator style callback(BlockCell(self, 'monitor')) callback(BlockCell(self, 'sources')) callback(BlockCell(self, 'source', persists=False)) callback(BlockCell(self, 'receivers')) callback(BlockCell(self, 'accessories', persists=False)) callback(BlockCell(self, 'shared_objects')) 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: We should also run if any of: # there are any data-logging receivers (e.g. APRS, ADS-B) # (requires becoming aware of no-audio receivers) # a client is watching a receiver's cell-based outputs (e.g. VOR) # (requires 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) 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=Notice(always_visible=False)) 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) def get_cpu_use(self): return round(self.__cpu_calculator.get(), 2) def get_shared_object(self, ctor): # TODO: Make shared objects able to persist. This will probably require some kind of up-front registry. # TODO: __name__ is a lousy strategy key = ctor.__name__ if key not in self.__shared_objects: self.__shared_objects[key] = ctor() return self.__shared_objects[key] 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 ModeSDemodulator(gr.hier_block2, ExportedState): implements(IDemodulator) def __init__(self, mode='MODE-S', input_rate=0, context=None): assert input_rate > 0 gr.hier_block2.__init__( self, 'Mode S/ADS-B/1090 demodulator', gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(0, 0, 0)) self.mode = mode self.input_rate = input_rate hex_msg_queue = gr.msg_queue(100) band_filter = MultistageChannelFilter( input_rate=input_rate, output_rate=demod_rate, cutoff_freq=demod_rate / 2, transition_width=transition_width) # TODO optimize filter band self.__demod = air_modes.rx_path( rate=demod_rate, threshold=7.0, # default used in air-modes code but not exposed queue=hex_msg_queue, use_pmf=False, use_dcblock=True) self.connect( self, band_filter, self.__demod) self.__messages_seen = 0 self.__message_rate_calc = LazyRateCalculator(lambda: self.__messages_seen, min_interval=2) # Parsing # TODO: These bits are mimicking gr-air-modes toplevel code. Figure out if we can have less glue. # Note: gr pubsub is synchronous -- subscribers are called on the publisher's thread parser_output = gr.pubsub.pubsub() parser = air_modes.make_parser(parser_output) cpr_decoder = air_modes.cpr_decoder(my_location=None) # TODO: get position info from device air_modes.output_print(cpr_decoder, parser_output) def callback(msg): # called on msgq_runner's thrad # pylint: disable=broad-except try: reactor.callFromThread(parser, msg.to_string()) except Exception: print traceback.format_exc() self.__msgq_runner = gru.msgq_runner(hex_msg_queue, callback) def parsed_callback(msg): timestamp = time.time() self.__messages_seen += 1 context.output_message(ModeSMessageWrapper(msg, cpr_decoder, timestamp)) for i in xrange(0, 2 ** 5): parser_output.subscribe('type%i_dl' % i, parsed_callback) def __del__(self): self.__msgq_runner.stop() @exported_value(float) def get_message_rate(self): return round(self.__message_rate_calc.get(), 1) def can_set_mode(self, mode): return False def get_half_bandwidth(self): return demod_rate / 2 def get_output_type(self): return no_signal @exported_value() def get_band_filter_shape(self): return { 'low': -demod_rate / 2, 'high': demod_rate / 2, 'width': transition_width }
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 ModeSDemodulator(gr.hier_block2, ExportedState): def __init__(self, mode='MODE-S', input_rate=0, context=None): assert input_rate > 0 gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(0, 0, 0)) demod_rate = 2000000 transition_width = 500000 hex_msg_queue = gr.msg_queue(100) self.__band_filter = MultistageChannelFilter( input_rate=input_rate, output_rate=demod_rate, cutoff_freq=demod_rate / 2, transition_width=transition_width) # TODO optimize filter band self.__demod = air_modes.rx_path( rate=demod_rate, threshold=7.0, # default used in air-modes code but not exposed queue=hex_msg_queue, use_pmf=False, use_dcblock=True) self.connect(self, self.__band_filter, self.__demod) self.__messages_seen = 0 self.__message_rate_calc = LazyRateCalculator( lambda: self.__messages_seen, min_interval=2) # Parsing # TODO: These bits are mimicking gr-air-modes toplevel code. Figure out if we can have less glue. # Note: gr pubsub is synchronous -- subscribers are called on the publisher's thread parser_output = gr.pubsub.pubsub() parser = air_modes.make_parser(parser_output) cpr_decoder = air_modes.cpr_decoder( my_location=None) # TODO: get position info from device air_modes.output_print(cpr_decoder, parser_output) def msq_runner_callback(msg): # called on msgq_runner's thread # pylint: disable=broad-except try: reactor.callFromThread(parser, msg.to_string()) except Exception: print(traceback.format_exc()) self.__msgq_runner = gru.msgq_runner(hex_msg_queue, msq_runner_callback) def parsed_callback(msg): timestamp = time.time() self.__messages_seen += 1 context.output_message( ModeSMessageWrapper(msg, cpr_decoder, timestamp)) for i in six.moves.range(0, 2**5): parser_output.subscribe('type%i_dl' % i, parsed_callback) def __del__(self): self.__msgq_runner.stop() @exported_value(type=RangeT([(0, 30)], unit=units.dB), changes='this_setter', label='Decode threshold') def get_decode_threshold(self): return self.__demod.get_threshold() @setter def set_decode_threshold(self, value): self.__demod.set_threshold(float(value)) @exported_value(float, changes='continuous', label='Messages/sec decoded') def get_message_rate(self): return round(self.__message_rate_calc.get(), 1) def get_output_type(self): return no_signal @exported_value(type=BandShape, changes='never') def get_band_shape(self): return self.__band_filter.get_shape()
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(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): # pylint: disable=arguments-differ # 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, unused_subscription_value=None): 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 ModeSDemodulator(gr.hier_block2, ExportedState): def __init__(self, mode='MODE-S', input_rate=0, context=None): assert input_rate > 0 gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(0, 0, 0)) demod_rate = 2000000 transition_width = 500000 hex_msg_queue = gr.msg_queue(100) self.__band_filter = MultistageChannelFilter( input_rate=input_rate, output_rate=demod_rate, cutoff_freq=demod_rate / 2, transition_width=transition_width) # TODO optimize filter band self.__demod = air_modes.rx_path( rate=demod_rate, threshold=7.0, # default used in air-modes code but not exposed queue=hex_msg_queue, use_pmf=False, use_dcblock=True) self.connect( self, self.__band_filter, self.__demod) self.__messages_seen = 0 self.__message_rate_calc = LazyRateCalculator(lambda: self.__messages_seen, min_interval=2) # Parsing # TODO: These bits are mimicking gr-air-modes toplevel code. Figure out if we can have less glue. # Note: gr pubsub is synchronous -- subscribers are called on the publisher's thread parser_output = gr.pubsub.pubsub() parser = air_modes.make_parser(parser_output) cpr_decoder = air_modes.cpr_decoder(my_location=None) # TODO: get position info from device air_modes.output_print(cpr_decoder, parser_output) def msq_runner_callback(msg): # called on msgq_runner's thread # pylint: disable=broad-except try: reactor.callFromThread(parser, msg.to_string()) except Exception: print traceback.format_exc() self.__msgq_runner = gru.msgq_runner(hex_msg_queue, msq_runner_callback) def parsed_callback(msg): timestamp = time.time() self.__messages_seen += 1 context.output_message(ModeSMessageWrapper(msg, cpr_decoder, timestamp)) for i in xrange(0, 2 ** 5): parser_output.subscribe('type%i_dl' % i, parsed_callback) def __del__(self): self.__msgq_runner.stop() @exported_value(type=RangeT([(0, 30)], unit=units.dB), changes='this_setter', label='Decode threshold') def get_decode_threshold(self): return self.__demod.get_threshold() @setter def set_decode_threshold(self, value): self.__demod.set_threshold(float(value)) @exported_value(float, changes='continuous', label='Messages/sec decoded') def get_message_rate(self): return round(self.__message_rate_calc.get(), 1) def get_output_type(self): return no_signal @exported_value(type=BandShape, changes='never') def get_band_shape(self): return self.__band_filter.get_shape()