예제 #1
0
파일: top.py 프로젝트: ptanguy/shinysdr
    def __init__(self, devices={}, audio_config=None, features=_stub_features):
        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 = {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=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().subscribe(self.__start_or_stop_later)
        self.__clip_probe = MaxProbe()
        
        # Receiver blocks (multiple, eventually)
        self._receivers = {}
        self._receiver_valid = {}
        
        # collections
        # TODO: No longer necessary to have these non-underscore names
        self.sources = CollectionState(self._sources)
        self.receivers = ReceiverCollection(self._receivers, self)
        self.accessories = CollectionState(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().subscribe(lambda: self.__device_vfo_callback(k))
        
        for k, d in devices.iteritems():
            hookup_vfo_callback(k, d)
        
        self._do_connect()
예제 #2
0
파일: top.py 프로젝트: nunb/shinysdr
    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()
예제 #3
0
파일: top.py 프로젝트: shadown/shinysdr
	def __init__(self, devices={}, stereo=True):
		gr.top_block.__init__(self, "SDR top block")
		self.__unpaused = True  # user state
		self.__running = False  # actually started

		# Configuration
		# TODO: device refactoring: Remove vestigial 'accessories'
		self._sources = {k: d for k, d in devices.iteritems() if d.can_receive()}
		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

		# Blocks etc.
		# TODO: device refactoring: remove 'source' concept (which is currently a device)
		self.source = None
		self.__rx_driver = None
		self.__source_tune_subscription = None
		self.monitor = MonitorSink(
			signal_type=SignalType(sample_rate=10000, kind='IQ'),  # dummy value will be updated in _do_connect
			context=Context(self))
		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)
		
		# Audio stream bits
		self.__audio_channels = 2 if stereo else 1
		self.audio_resampler_cache = {}
		self.audio_queue_sinks = {}
		self.__audio_bus_rate = 1  # dummy initial value, computed in _do_connect
		
		# Flags, other state
		self.__needs_reconnect = True
		self.input_rate = None
		self.input_freq = None
		self.receiver_key_counter = 0
		self.receiver_default_state = {}
		self.last_wall_time = time.time()
		self.last_cpu_time = time.clock()
		self.last_cpu_use = 0
		
		self._do_connect()
예제 #4
0
파일: top.py 프로젝트: amberadams/shinysdr
	def __init__(self, sources={}):
		gr.top_block.__init__(self, "SDR top block")
		self.__unpaused = True  # user state
		self.__running = False  # actually started
		self.__lock_count = 0

		# Configuration
		self._sources = dict(sources)
		self.source_name = self._sources.keys()[0]  # arbitrary valid initial value
		self.audio_rate = audio_rate = 44100

		# Blocks etc.
		self.source = None
		self.monitor = MonitorSink(
			sample_rate=10000,  # dummy value will be updated in _do_connect
			complex_in=True,
			context=Context(self))
		
		# Receiver blocks (multiple, eventually)
		self._receivers = {}
		self._receiver_valid = {}

		# kludge for using collection like block - TODO: better architecture
		self.sources = CollectionState(self._sources)
		self.receivers = ReceiverCollection(self._receivers, self)
		
		# Audio stream bits
		self.audio_resampler_cache = {}
		self.audio_queue_sinks = {}
		
		# Flags, other state
		self.__needs_reconnect = True
		self.input_rate = None
		self.input_freq = None
		self.receiver_key_counter = 0
		self.receiver_default_state = {}
		self.last_wall_time = time.time()
		self.last_cpu_time = time.clock()
		self.last_cpu_use = 0
		
		self._do_connect()
예제 #5
0
파일: top.py 프로젝트: shadown/shinysdr
class Top(gr.top_block, ExportedState, RecursiveLockBlockMixin):

	def __init__(self, devices={}, stereo=True):
		gr.top_block.__init__(self, "SDR top block")
		self.__unpaused = True  # user state
		self.__running = False  # actually started

		# Configuration
		# TODO: device refactoring: Remove vestigial 'accessories'
		self._sources = {k: d for k, d in devices.iteritems() if d.can_receive()}
		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

		# Blocks etc.
		# TODO: device refactoring: remove 'source' concept (which is currently a device)
		self.source = None
		self.__rx_driver = None
		self.__source_tune_subscription = None
		self.monitor = MonitorSink(
			signal_type=SignalType(sample_rate=10000, kind='IQ'),  # dummy value will be updated in _do_connect
			context=Context(self))
		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)
		
		# Audio stream bits
		self.__audio_channels = 2 if stereo else 1
		self.audio_resampler_cache = {}
		self.audio_queue_sinks = {}
		self.__audio_bus_rate = 1  # dummy initial value, computed in _do_connect
		
		# Flags, other state
		self.__needs_reconnect = True
		self.input_rate = None
		self.input_freq = None
		self.receiver_key_counter = 0
		self.receiver_default_state = {}
		self.last_wall_time = time.time()
		self.last_cpu_time = time.clock()
		self.last_cpu_use = 0
		
		self._do_connect()

	def add_receiver(self, mode, key=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
		
		receiver = self._make_receiver(mode, defaults, key)
		
		self._receivers[key] = receiver
		self._receiver_valid[key] = False
		
		self.__needs_reconnect = True
		self._do_connect()
		
		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 = True
		self._do_connect()

	def add_audio_queue(self, queue, queue_rate):
		# TODO: place limit on maximum requested sample rate
		self.audio_queue_sinks[queue] = (queue_rate,
			AudioQueueSink(channels=self.__audio_channels, queue=queue))
		
		self.__needs_reconnect = True
		self._do_connect()
		self.__start_or_stop()
	
	def remove_audio_queue(self, queue):
		del self.audio_queue_sinks[queue]
		
		self.__start_or_stop()
		self.__needs_reconnect = True
		self._do_connect()
	
	def get_audio_channels(self):
		'''
		Return the number of channels (which will be 1 or 2) in audio queue outputs.
		'''
		return self.__audio_channels

	def _do_connect(self):
		"""Do all reconfiguration operations in the proper order."""
		rate_changed = False
		if self.source is not self._sources[self.source_name]:
			log.msg('Flow graph: Switching RF source')
			self.__needs_reconnect = True

			this_source = self._sources[self.source_name]
			
			def update_input_freqs():
				freq = this_source.get_freq()
				self.input_freq = freq
				self.monitor.set_input_center_freq(freq)
				for receiver in self._receivers.itervalues():
					receiver.set_input_center_freq(freq)
			
			def tune_hook():
				# 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.__rx_driver.get_tune_delay(), tune_hook_actual)
			
			def tune_hook_actual():
				if self.source is not this_source:
					return
				update_input_freqs()
				for key in self._receivers:
					self._update_receiver_validity(key)
					# TODO: If multiple receivers change validity we'll do redundant reconnects in this loop; avoid that.
			
			if self.__source_tune_subscription is not None:
				self.__source_tune_subscription.unsubscribe()
			self.__source_tune_subscription = this_source.state()['freq'].subscribe(tune_hook)
			
			self.source = this_source
			self.__rx_driver = this_source.get_rx_driver()
			source_signal_type = self.__rx_driver.get_output_type()
			this_rate = source_signal_type.get_sample_rate()
			rate_changed = self.input_rate != this_rate
			self.input_rate = this_rate
			self.monitor.set_signal_type(source_signal_type)
			self.__clip_probe.set_window_and_reconnect(0.5 * this_rate)
			update_input_freqs()
		
		if rate_changed:
			log.msg('Flow graph: Changing sample rates')
			for receiver in self._receivers.itervalues():
				receiver.set_input_rate(self.input_rate)

		if self.__needs_reconnect:
			log.msg('Flow graph: Rebuilding connections')
			self.__needs_reconnect = False
			
			self._recursive_lock()
			self.disconnect_all()
			
			self.connect(
				self.__rx_driver,
				self.monitor)
			self.connect(
				self.__rx_driver,
				self.__clip_probe)
			
			# Determine audio bus rate.
			# The bus obviously does not need to be higher than the rate of any receiver, because that would be extraneous data. It also does not need to be higher than the rate of any queue, because no queue has use for the information.
			if len(self._receivers) > 0 and len(self.audio_queue_sinks) > 0:
				max_out_rate = max((receiver.get_output_type().get_sample_rate() for receiver in self._receivers.itervalues()))
				max_in_rate = max((queue_rate for (queue_rate, sink) in self.audio_queue_sinks.itervalues()))
				new_bus_rate = min(max_out_rate, max_in_rate)
				if new_bus_rate != self.__audio_bus_rate:
					self.__audio_bus_rate = new_bus_rate
					self.audio_resampler_cache.clear()
			
			# recreated each time because reusing an add_ff w/ different
			# input counts fails; TODO: report/fix bug
			audio_sums = [blocks.add_ff() for _ in xrange(self.__audio_channels)]
			
			audio_sum_index = 0
			for key, receiver in self._receivers.iteritems():
				self._receiver_valid[key] = receiver.get_is_valid()
				if self._receiver_valid[key]:
					if audio_sum_index >= 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.__rx_driver, receiver)
					receiver_rate = receiver.get_output_type().get_sample_rate()
					if receiver_rate == self.__audio_bus_rate:
						for ch in xrange(self.__audio_channels):
							self.connect(
								(receiver, ch),
								(audio_sums[ch], audio_sum_index))
					else:
						for ch in xrange(self.__audio_channels):
							self.connect(
								(receiver, ch),
								# TODO pool these resamplers
								make_resampler(receiver_rate, self.__audio_bus_rate),
								(audio_sums[ch], audio_sum_index))
					audio_sum_index += 1
			
			if audio_sum_index > 0:
				# connect audio output only if there is at least one input
				if len(self.audio_queue_sinks) > 0:
					used_resamplers = set()
					for (queue_rate, sink) in self.audio_queue_sinks.itervalues():
						if queue_rate == self.__audio_bus_rate:
							for ch in xrange(self.__audio_channels):
								self.connect(audio_sums[ch], (sink, ch))
						else:
							if queue_rate not in self.audio_resampler_cache:
								# Moderately expensive due to the internals using optfir
								log.msg('Flow graph: Constructing resampler for audio rate %i' % queue_rate)
								self.audio_resampler_cache[queue_rate] = tuple(
									make_resampler(self.__audio_bus_rate, queue_rate)
									for _ in xrange(self.__audio_channels))
							resamplers = self.audio_resampler_cache[queue_rate]
							used_resamplers.add(resamplers)
							for ch in xrange(self.__audio_channels):
								self.connect(resamplers[ch], (sink, ch))
					for resamplers in used_resamplers:
						for ch in xrange(self.__audio_channels):
							self.connect(audio_sums[ch], resamplers[ch])
				else:
					# no stream sinks, gnuradio requires a dummy sink
					for ch in xrange(self.__audio_channels):
						self.connect(audio_sums[ch], blocks.null_sink(gr.sizeof_float))
		
			self._recursive_unlock()
			log.msg('Flow graph: ...done reconnecting.')

	def _update_receiver_validity(self, key):
		receiver = self._receivers[key]
		if receiver.get_is_valid() != self._receiver_valid[key]:
			self.__needs_reconnect = True
			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

	@exported_value(ctor=bool)
	def get_unpaused(self):
		return self.__unpaused
	
	@setter
	def set_unpaused(self, value):
		self.__unpaused = bool(value)
		self.__start_or_stop()
	
	def __start_or_stop(self):
		# TODO: We should also run if at least one client is watching the spectrum or demodulators' cell-based outputs, but there's no good way to recognize that yet.
		should_run = self.__unpaused and len(self.audio_queue_sinks) > 0
		if should_run != self.__running:
			if should_run:
				self.start()
			else:
				self.stop()
				self.wait()

	@exported_value(ctor_fn=lambda self:
		Enum({k: v.get_name() or k for (k, v) in self._sources.iteritems()}))
	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()

	def _make_receiver(self, mode, state, key):
		facet = ContextForReceiver(self, key)
		receiver = Receiver(
			mode=mode,
			input_rate=self.input_rate,
			input_center_freq=self.input_freq,
			audio_channels=self.__audio_channels,
			context=facet,
		)
		receiver.state_from_json(state)
		# until _enabled, ignore any callbacks resulting from the state_from_json initialization
		facet._enabled = True
		return receiver
	
	@exported_value(ctor=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''
	
	@exported_value(ctor=int)
	def get_input_rate(self):
		return self.input_rate

	@exported_value(ctor=int)
	def get_audio_bus_rate(self):
		'''
		Not visible externally; for diagnostic purposes only.
		'''
		return self.__audio_bus_rate
	
	@exported_value(ctor=float)
	def get_cpu_use(self):
		cur_wall_time = time.time()
		elapsed_wall = cur_wall_time - self.last_wall_time
		if elapsed_wall > 0.5:
			cur_cpu_time = time.clock()
			elapsed_cpu = cur_cpu_time - self.last_cpu_time
			self.last_wall_time = cur_wall_time
			self.last_cpu_time = cur_cpu_time
			self.last_cpu_use = round(elapsed_cpu / elapsed_wall, 2)
		return self.last_cpu_use
	
	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 _trigger_reconnect(self):
		self.__needs_reconnect = True
		self._do_connect()
	
	def _recursive_lock_hook(self):
		for source in self._sources.itervalues():
			source.notify_reconnecting_or_restarting()
예제 #6
0
파일: top.py 프로젝트: nunb/shinysdr
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()
예제 #7
0
    def __init__(self, devices={}, audio_config=None, stereo=True):
        if not len(devices) > 0:
            raise ValueError('Must have at least one RF device')
        #for key, audio_device in audio_devices.iteritems():
        #    if key == CLIENT_AUDIO_DEVICE:
        #        raise ValueError('The name %r for an audio device is reserved' % (key,))
        #    if not audio_device.can_transmit():
        #        raise ValueError('Audio device %r is not an output' % (key,))
        if audio_config is not None:
            # quick kludge placeholder -- currently a Device-device can't be stereo so we have a placeholder thing
            audio_device_name, audio_sample_rate = audio_config
            audio_devices = {
                'server': (audio_sample_rate,
                           audio.sink(audio_sample_rate, audio_device_name,
                                      False))
            }
        else:
            audio_devices = {}

        gr.top_block.__init__(self, "SDR top block")
        self.__unpaused = True  # user state
        self.__running = False  # actually started

        # Configuration
        # TODO: device refactoring: Remove vestigial 'accessories'
        self._sources = {
            k: d
            for k, d in devices.iteritems() if d.can_receive()
        }
        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

        # Audio early setup
        self.__audio_devices = audio_devices  # must be before contexts

        # Blocks etc.
        # TODO: device refactoring: remove 'source' concept (which is currently a device)
        self.source = None
        self.__rx_driver = None
        self.__source_tune_subscription = None
        self.monitor = MonitorSink(
            signal_type=SignalType(
                sample_rate=10000,
                kind='IQ'),  # dummy value will be updated in _do_connect
            context=Context(self))
        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)

        # Audio stream bits
        audio_destination_dict = {
            key: 'Server' or key
            for key, device in audio_devices.iteritems()
        }  # temp name till we have proper device objects
        audio_destination_dict[
            CLIENT_AUDIO_DEVICE] = 'Client'  # TODO reconsider name
        self.__audio_destination_type = Enum(audio_destination_dict,
                                             strict=True)
        self.__audio_channels = 2 if stereo else 1
        self.audio_queue_sinks = {}
        self.__audio_buses = {
            key: BusPlumber(self, self.__audio_channels)
            for key in audio_destination_dict
        }

        # Flags, other state
        self.__needs_reconnect = True
        self.input_rate = None
        self.input_freq = None
        self.receiver_key_counter = 0
        self.receiver_default_state = {}
        self.last_wall_time = time.time()
        self.last_cpu_time = time.clock()
        self.last_cpu_use = 0

        self._do_connect()
예제 #8
0
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')
        #for key, audio_device in audio_devices.iteritems():
        #    if key == CLIENT_AUDIO_DEVICE:
        #        raise ValueError('The name %r for an audio device is reserved' % (key,))
        #    if not audio_device.can_transmit():
        #        raise ValueError('Audio device %r is not an output' % (key,))
        if audio_config is not None:
            # quick kludge placeholder -- currently a Device-device can't be stereo so we have a placeholder thing
            audio_device_name, audio_sample_rate = audio_config
            audio_devices = {
                'server': (audio_sample_rate,
                           audio.sink(audio_sample_rate, audio_device_name,
                                      False))
            }
        else:
            audio_devices = {}

        gr.top_block.__init__(self, "SDR top block")
        self.__unpaused = True  # user state
        self.__running = False  # actually started

        # Configuration
        # TODO: device refactoring: Remove vestigial 'accessories'
        self._sources = {
            k: d
            for k, d in devices.iteritems() if d.can_receive()
        }
        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

        # Audio early setup
        self.__audio_devices = audio_devices  # must be before contexts

        # Blocks etc.
        # TODO: device refactoring: remove 'source' concept (which is currently a device)
        self.source = None
        self.__rx_driver = None
        self.__source_tune_subscription = None
        self.monitor = MonitorSink(
            signal_type=SignalType(
                sample_rate=10000,
                kind='IQ'),  # dummy value will be updated in _do_connect
            context=Context(self))
        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)

        # Audio stream bits
        audio_destination_dict = {
            key: 'Server' or key
            for key, device in audio_devices.iteritems()
        }  # temp name till we have proper device objects
        audio_destination_dict[
            CLIENT_AUDIO_DEVICE] = 'Client'  # TODO reconsider name
        self.__audio_destination_type = Enum(audio_destination_dict,
                                             strict=True)
        self.__audio_channels = 2 if stereo else 1
        self.audio_queue_sinks = {}
        self.__audio_buses = {
            key: BusPlumber(self, self.__audio_channels)
            for key in audio_destination_dict
        }

        # Flags, other state
        self.__needs_reconnect = True
        self.input_rate = None
        self.input_freq = None
        self.receiver_key_counter = 0
        self.receiver_default_state = {}
        self.last_wall_time = time.time()
        self.last_cpu_time = time.clock()
        self.last_cpu_use = 0

        self._do_connect()

    def add_receiver(self, mode, key=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

        receiver = self._make_receiver(mode, defaults, key)

        self._receivers[key] = receiver
        self._receiver_valid[key] = False

        self.__needs_reconnect = True
        self._do_connect()

        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 = True
        self._do_connect()

    def add_audio_queue(self, queue, queue_rate):
        # TODO: place limit on maximum requested sample rate
        self.audio_queue_sinks[queue] = (queue_rate,
                                         AudioQueueSink(
                                             channels=self.__audio_channels,
                                             queue=queue))

        self.__needs_reconnect = True
        self._do_connect()
        self.__start_or_stop()

    def remove_audio_queue(self, queue):
        del self.audio_queue_sinks[queue]

        self.__start_or_stop()
        self.__needs_reconnect = True
        self._do_connect()

    def get_audio_channels(self):
        '''
        Return the number of channels (which will be 1 or 2) in audio queue outputs.
        '''
        return self.__audio_channels

    def _do_connect(self):
        """Do all reconfiguration operations in the proper order."""
        rate_changed = False
        if self.source is not self._sources[self.source_name]:
            log.msg('Flow graph: Switching RF source')
            self.__needs_reconnect = True

            this_source = self._sources[self.source_name]

            def update_input_freqs():
                freq = this_source.get_freq()
                self.input_freq = freq
                self.monitor.set_input_center_freq(freq)
                for receiver in self._receivers.itervalues():
                    receiver.set_input_center_freq(freq)

            def tune_hook():
                # 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.__rx_driver.get_tune_delay(),
                                  tune_hook_actual)

            def tune_hook_actual():
                if self.source is not this_source:
                    return
                update_input_freqs()
                for key in self._receivers:
                    self._update_receiver_validity(key)
                    # TODO: If multiple receivers change validity we'll do redundant reconnects in this loop; avoid that.

            if self.__source_tune_subscription is not None:
                self.__source_tune_subscription.unsubscribe()
            self.__source_tune_subscription = this_source.state(
            )['freq'].subscribe(tune_hook)

            self.source = this_source
            self.__rx_driver = this_source.get_rx_driver()
            source_signal_type = self.__rx_driver.get_output_type()
            this_rate = source_signal_type.get_sample_rate()
            rate_changed = self.input_rate != this_rate
            self.input_rate = this_rate
            self.monitor.set_signal_type(source_signal_type)
            self.__clip_probe.set_window_and_reconnect(0.5 * this_rate)
            update_input_freqs()

        if rate_changed:
            log.msg('Flow graph: Changing sample rates')
            for receiver in self._receivers.itervalues():
                receiver.set_input_rate(self.input_rate)

        if self.__needs_reconnect:
            log.msg('Flow graph: Rebuilding connections')
            self.__needs_reconnect = False

            self._recursive_lock()
            self.disconnect_all()

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

            # Filter receivers
            bus_inputs = defaultdict(lambda: [])
            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 receiver.get_audio_destination() not in self.__audio_buses:
                    log.err(
                        'Flow graph: receiver audio destination %r is not available'
                        % (receiver.get_audio_destination(), ))
                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.__rx_driver, receiver)
                rrate = receiver.get_output_type().get_sample_rate()
                bus_inputs[receiver.get_audio_destination()].append(
                    (rrate, receiver))

            for key, bus in self.__audio_buses.iteritems():
                if key == CLIENT_AUDIO_DEVICE:
                    outputs = self.audio_queue_sinks.itervalues()
                else:
                    outputs = [self.__audio_devices[key]]
                bus.connect(inputs=bus_inputs[key], outputs=outputs)

            self._recursive_unlock()
            log.msg('Flow graph: ...done reconnecting.')

    def _update_receiver_validity(self, key):
        receiver = self._receivers[key]
        if receiver.get_is_valid() != self._receiver_valid[key]:
            self.__needs_reconnect = True
            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

    @exported_value(ctor=bool)
    def get_unpaused(self):
        return self.__unpaused

    @setter
    def set_unpaused(self, value):
        self.__unpaused = bool(value)
        self.__start_or_stop()

    def __start_or_stop(self):
        # TODO: We should also run if at least one client is watching the spectrum or demodulators' cell-based outputs, but there's no good way to recognize that yet.
        should_run = self.__unpaused and len(self.audio_queue_sinks) > 0
        if should_run != self.__running:
            if should_run:
                self.start()
            else:
                self.stop()
                self.wait()

    @exported_value(ctor_fn=lambda self: Enum(
        {k: v.get_name() or k
         for (k, v) in self._sources.iteritems()}))
    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()

    def _make_receiver(self, mode, state, key):
        facet = ContextForReceiver(self, key)
        receiver = Receiver(
            mode=mode,
            input_rate=self.input_rate,
            input_center_freq=self.input_freq,
            audio_channels=self.__audio_channels,
            audio_destination=CLIENT_AUDIO_DEVICE,  # TODO match others
            context=facet,
        )
        receiver.state_from_json(state)
        # until _enabled, ignore any callbacks resulting from the state_from_json initialization
        facet._enabled = True
        return receiver

    @exported_value(ctor=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''

    @exported_value(ctor=int)
    def get_input_rate(self):
        return self.input_rate

    @exported_value()
    def get_audio_bus_rate(self):
        '''
        Not visible externally; for diagnostic purposes only.
        '''
        return [b.get_current_rate() for b in self.__audio_buses.itervalues()]

    @exported_value(ctor=float)
    def get_cpu_use(self):
        cur_wall_time = time.time()
        elapsed_wall = cur_wall_time - self.last_wall_time
        if elapsed_wall > 0.5:
            cur_cpu_time = time.clock()
            elapsed_cpu = cur_cpu_time - self.last_cpu_time
            self.last_wall_time = cur_wall_time
            self.last_cpu_time = cur_cpu_time
            self.last_cpu_use = round(elapsed_cpu / elapsed_wall, 2)
        return self.last_cpu_use

    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_audio_destination_type(self):
        '''for ContextForReceiver only'''
        return self.__audio_destination_type

    def _trigger_reconnect(self):
        self.__needs_reconnect = True
        self._do_connect()

    def _recursive_lock_hook(self):
        for source in self._sources.itervalues():
            source.notify_reconnecting_or_restarting()
예제 #9
0
파일: top.py 프로젝트: ptanguy/shinysdr
class Top(gr.top_block, ExportedState, RecursiveLockBlockMixin):

    def __init__(self, devices={}, audio_config=None, features=_stub_features):
        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 = {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=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().subscribe(self.__start_or_stop_later)
        self.__clip_probe = MaxProbe()
        
        # Receiver blocks (multiple, eventually)
        self._receivers = {}
        self._receiver_valid = {}
        
        # collections
        # TODO: No longer necessary to have these non-underscore names
        self.sources = CollectionState(self._sources)
        self.receivers = ReceiverCollection(self._receivers, self)
        self.accessories = CollectionState(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().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()
        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.__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.
                    for ch in xrange(0, self.__audio_manager.get_channels()):
                        self.connect((receiver, ch), blocks.null_sink(gr.sizeof_float))
                    # 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):
        # 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()
    
    @exported_block()
    def get_monitor(self):
        return self.monitor
    
    @exported_block(persists=False)
    def get_sources(self):
        return self.sources
    
    @exported_block(persists=False)
    def get_source(self):
        return self.source  # TODO no need for this now...?
    
    @exported_block()
    def get_receivers(self):
        return self.receivers
    
    @exported_block(persists=False)
    def get_accessories(self):
        return self.accessories
    
    @exported_block()
    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)
    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_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()
예제 #10
0
파일: top.py 프로젝트: amberadams/shinysdr
class Top(gr.top_block, ExportedState):

	def __init__(self, sources={}):
		gr.top_block.__init__(self, "SDR top block")
		self.__unpaused = True  # user state
		self.__running = False  # actually started
		self.__lock_count = 0

		# Configuration
		self._sources = dict(sources)
		self.source_name = self._sources.keys()[0]  # arbitrary valid initial value
		self.audio_rate = audio_rate = 44100

		# Blocks etc.
		self.source = None
		self.monitor = MonitorSink(
			sample_rate=10000,  # dummy value will be updated in _do_connect
			complex_in=True,
			context=Context(self))
		
		# Receiver blocks (multiple, eventually)
		self._receivers = {}
		self._receiver_valid = {}

		# kludge for using collection like block - TODO: better architecture
		self.sources = CollectionState(self._sources)
		self.receivers = ReceiverCollection(self._receivers, self)
		
		# Audio stream bits
		self.audio_resampler_cache = {}
		self.audio_queue_sinks = {}
		
		# Flags, other state
		self.__needs_reconnect = True
		self.input_rate = None
		self.input_freq = None
		self.receiver_key_counter = 0
		self.receiver_default_state = {}
		self.last_wall_time = time.time()
		self.last_cpu_time = time.clock()
		self.last_cpu_use = 0
		
		self._do_connect()

	def add_receiver(self, mode, key=None):
		if len(self._receivers) >= 100:
			# Prevent storage-usage DoS attack
			raise Error('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
		
		receiver = self._make_receiver(mode, defaults, key)
		
		self._receivers[key] = receiver
		self._receiver_valid[key] = False
		
		self.__needs_reconnect = True
		self._do_connect()
		
		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 = True
		self._do_connect()

	def add_audio_queue(self, queue, queue_rate):
		# TODO: place limit on maximum requested sample rate
		sink = blocks.message_sink(
			gr.sizeof_float * _num_audio_channels,
			queue,
			True)
		interleaver = blocks.streams_to_vector(gr.sizeof_float, _num_audio_channels)
		# TODO: bundle the interleaver and sink in a hier block so it doesn't have to be reconnected
		self.audio_queue_sinks[queue] = (queue_rate, interleaver, sink)
		
		self.__needs_reconnect = True
		self._do_connect()
		self.__start_or_stop()
	
	def remove_audio_queue(self, queue):
		del self.audio_queue_sinks[queue]
		
		self.__start_or_stop()
		self.__needs_reconnect = True
		self._do_connect()

	def _do_connect(self):
		"""Do all reconfiguration operations in the proper order."""
		rate_changed = False
		if self.source is not self._sources[self.source_name]:
			log.msg('Flow graph: Switching RF source')
			self.__needs_reconnect = True
			
			def tune_hook():
				reactor.callLater(self.source.get_tune_delay(), tune_hook_actual)
			
			def tune_hook_actual():
				if self.source is not this_source:
					return
				freq = this_source.get_freq()
				self.input_freq = freq
				self.monitor.set_input_center_freq(freq)
				for key, receiver in self._receivers.iteritems():
					receiver.set_input_center_freq(freq)
					self._update_receiver_validity(key)
					# TODO: If multiple receivers change validity we'll do redundant reconnects in this loop; avoid that.

			this_source = self._sources[self.source_name]
			this_source.set_tune_hook(tune_hook)
			self.source = this_source
			this_rate = this_source.get_sample_rate()
			rate_changed = self.input_rate != this_rate
			self.input_rate = this_rate
			self.input_freq = this_source.get_freq()
			for key, receiver in self._receivers.iteritems():
				receiver.set_input_center_freq(self.input_freq)
		
		if rate_changed:
			log.msg('Flow graph: Changing sample rates')
			self.monitor.set_sample_rate(self.input_rate)
			for receiver in self._receivers.itervalues():
				receiver.set_input_rate(self.input_rate)

		if self.__needs_reconnect:
			log.msg('Flow graph: Rebuilding connections')
			self.__needs_reconnect = False
			
			self._recursive_lock()
			self.disconnect_all()
			
			self.connect(
				self.source,
				self.monitor)
			
			# recreated each time because reusing an add_ff w/ different
			# input counts fails; TODO: report/fix bug
			audio_sum_l = blocks.add_ff()
			audio_sum_r = blocks.add_ff()
			
			audio_sum_index = 0
			for key, receiver in self._receivers.iteritems():
				self._receiver_valid[key] = receiver.get_is_valid()
				if self._receiver_valid[key]:
					if audio_sum_index >= 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.source, receiver)
					self.connect((receiver, 0), (audio_sum_l, audio_sum_index))
					self.connect((receiver, 1), (audio_sum_r, audio_sum_index))
					audio_sum_index += 1
			
			if audio_sum_index > 0:
				# connect audio output only if there is at least one input
				if len(self.audio_queue_sinks) > 0:
					used_resamplers = set()
					for (queue_rate, interleaver, sink) in self.audio_queue_sinks.itervalues():
						if queue_rate == self.audio_rate:
							self.connect(audio_sum_l, (interleaver, 0))
							self.connect(audio_sum_r, (interleaver, 1))
						else:
							if queue_rate not in self.audio_resampler_cache:
								# Moderately expensive due to the internals using optfir
								log.msg('Flow graph: Constructing resampler for audio rate %i' % queue_rate)
								self.audio_resampler_cache[queue_rate] = (
									make_resampler(self.audio_rate, queue_rate),
									make_resampler(self.audio_rate, queue_rate)
								)
							resamplers = self.audio_resampler_cache[queue_rate]
							used_resamplers.add(resamplers)
							self.connect(resamplers[0], (interleaver, 0))
							self.connect(resamplers[1], (interleaver, 1))
						self.connect(interleaver, sink)
					for resamplers in used_resamplers:
						self.connect(audio_sum_l, resamplers[0])
						self.connect(audio_sum_r, resamplers[1])
				else:
					# no stream sinks, gnuradio requires a dummy sink
					self.connect(audio_sum_l, blocks.null_sink(gr.sizeof_float))
					self.connect(audio_sum_r, blocks.null_sink(gr.sizeof_float))
		
			self._recursive_unlock()
			log.msg('Flow graph: ...done reconnecting.')

	def _update_receiver_validity(self, key):
		receiver = self._receivers[key]
		if receiver.get_is_valid() != self._receiver_valid[key]:
			self.__needs_reconnect = True
			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'))

	def start(self):
		# trigger reconnect/restart notification
		self._recursive_lock()
		self._recursive_unlock()
		
		super(Top, self).start()
		self.__running = True

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

	@exported_value(ctor=bool)
	def get_unpaused(self):
		return self.__unpaused
	
	@setter
	def set_unpaused(self, value):
		self.__unpaused = bool(value)
		self.__start_or_stop()
	
	def __start_or_stop(self):
		# TODO: We should also run if at least one client is watching the spectrum or demodulators' cell-based outputs, but there's no good way to recognize that yet.
		should_run = self.__unpaused and len(self.audio_queue_sinks) > 0
		if should_run != self.__running:
			if should_run:
				self.start()
			else:
				self.stop()
				self.wait()

	@exported_value(ctor_fn=lambda self:
		Enum({k: str(v) for (k, v) in self._sources.iteritems()}))
	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()

	def _make_receiver(self, mode, state, key):
		facet = ContextForReceiver(self, key)
		receiver = Receiver(
			mode=mode,
			input_rate=self.input_rate,
			input_center_freq=self.input_freq,
			audio_rate=self.audio_rate,
			context=facet,
		)
		receiver.state_from_json(state)
		# until _enabled, ignore any callbacks resulting from the state_from_json initialization
		facet._enabled = True
		return receiver

	@exported_value(ctor=int)
	def get_input_rate(self):
		return self.input_rate

	@exported_value(ctor=int)
	def get_audio_rate(self):
		return self.audio_rate
	
	@exported_value(ctor=float)
	def get_cpu_use(self):
		cur_wall_time = time.time()
		elapsed_wall = cur_wall_time - self.last_wall_time
		if elapsed_wall > 0.5:
			cur_cpu_time = time.clock()
			elapsed_cpu = cur_cpu_time - self.last_cpu_time
			self.last_wall_time = cur_wall_time
			self.last_cpu_time = cur_cpu_time
			self.last_cpu_use = round(elapsed_cpu / elapsed_wall, 2)
		return self.last_cpu_use

	def _recursive_lock(self):
		# gnuradio uses a non-recursive lock, which is not adequate for our purposes because we want to make changes locally or globally without worrying about having a single lock entry point
		if self.__lock_count == 0:
			self.lock()
			for source in self._sources.itervalues():
				source.notify_reconnecting_or_restarting()
		self.__lock_count += 1

	def _recursive_unlock(self):
		self.__lock_count -= 1
		if self.__lock_count == 0:
			self.unlock()
예제 #11
0
파일: top.py 프로젝트: gstark307/shinysdr
    def __init__(self, devices={}, audio_config=None, stereo=True):
        if not len(devices) > 0:
            raise ValueError('Must have at least one RF device')
        #for key, audio_device in audio_devices.iteritems():
        #    if key == CLIENT_AUDIO_DEVICE:
        #        raise ValueError('The name %r for an audio device is reserved' % (key,))
        #    if not audio_device.can_transmit():
        #        raise ValueError('Audio device %r is not an output' % (key,))
        if audio_config is not None:
            # quick kludge placeholder -- currently a Device-device can't be stereo so we have a placeholder thing
            # pylint: disable=unpacking-non-sequence
            audio_device_name, audio_sample_rate = audio_config
            audio_devices = {'server': (audio_sample_rate, audio.sink(audio_sample_rate, audio_device_name, False))}
        else:
            audio_devices = {}
        
        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
        
        # Audio early setup
        self.__audio_devices = audio_devices  # must be before contexts

        # Blocks etc.
        # TODO: device refactoring: remove 'source' concept (which is currently a device)
        self.source = None
        self.__rx_driver = None
        self.__source_tune_subscription = 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)
        
        # Audio stream bits
        audio_destination_dict = {key: 'Server' or key for key, device in audio_devices.iteritems()}  # temp name till we have proper device objects
        audio_destination_dict[CLIENT_AUDIO_DEVICE] = 'Client'  # TODO reconsider name
        self.__audio_destination_type = Enum(audio_destination_dict, strict=True)
        self.__audio_channels = 2 if stereo else 1
        self.audio_queue_sinks = {}
        self.__audio_buses = {key: BusPlumber(self, self.__audio_channels) for key in audio_destination_dict}
        
        # Flags, other state
        self.__needs_reconnect = True
        self.input_rate = None
        self.input_freq = None
        self.receiver_key_counter = 0
        self.receiver_default_state = {}
        self.last_wall_time = time.time()
        self.last_cpu_time = time.clock()
        self.last_cpu_use = 0
        
        self._do_connect()
예제 #12
0
파일: top.py 프로젝트: gstark307/shinysdr
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')
        #for key, audio_device in audio_devices.iteritems():
        #    if key == CLIENT_AUDIO_DEVICE:
        #        raise ValueError('The name %r for an audio device is reserved' % (key,))
        #    if not audio_device.can_transmit():
        #        raise ValueError('Audio device %r is not an output' % (key,))
        if audio_config is not None:
            # quick kludge placeholder -- currently a Device-device can't be stereo so we have a placeholder thing
            # pylint: disable=unpacking-non-sequence
            audio_device_name, audio_sample_rate = audio_config
            audio_devices = {'server': (audio_sample_rate, audio.sink(audio_sample_rate, audio_device_name, False))}
        else:
            audio_devices = {}
        
        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
        
        # Audio early setup
        self.__audio_devices = audio_devices  # must be before contexts

        # Blocks etc.
        # TODO: device refactoring: remove 'source' concept (which is currently a device)
        self.source = None
        self.__rx_driver = None
        self.__source_tune_subscription = 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)
        
        # Audio stream bits
        audio_destination_dict = {key: 'Server' or key for key, device in audio_devices.iteritems()}  # temp name till we have proper device objects
        audio_destination_dict[CLIENT_AUDIO_DEVICE] = 'Client'  # TODO reconsider name
        self.__audio_destination_type = Enum(audio_destination_dict, strict=True)
        self.__audio_channels = 2 if stereo else 1
        self.audio_queue_sinks = {}
        self.__audio_buses = {key: BusPlumber(self, self.__audio_channels) for key in audio_destination_dict}
        
        # Flags, other state
        self.__needs_reconnect = True
        self.input_rate = None
        self.input_freq = None
        self.receiver_key_counter = 0
        self.receiver_default_state = {}
        self.last_wall_time = time.time()
        self.last_cpu_time = time.clock()
        self.last_cpu_use = 0
        
        self._do_connect()

    def add_receiver(self, mode, key=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
        
        receiver = self._make_receiver(mode, defaults, key)
        
        self._receivers[key] = receiver
        self._receiver_valid[key] = False
        
        self.__needs_reconnect = True
        self._do_connect()
        
        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 = True
        self._do_connect()

    def add_audio_queue(self, queue, queue_rate):
        # TODO: place limit on maximum requested sample rate
        self.audio_queue_sinks[queue] = (queue_rate,
            AudioQueueSink(channels=self.__audio_channels, queue=queue))
        
        self.__needs_reconnect = True
        self._do_connect()
        self.__start_or_stop()
    
    def remove_audio_queue(self, queue):
        del self.audio_queue_sinks[queue]
        
        self.__start_or_stop()
        self.__needs_reconnect = True
        self._do_connect()
    
    def get_audio_channels(self):
        '''
        Return the number of channels (which will be 1 or 2) in audio queue outputs.
        '''
        return self.__audio_channels

    def _do_connect(self):
        """Do all reconfiguration operations in the proper order."""
        rate_changed = False
        if self.source is not self._sources[self.source_name]:
            log.msg('Flow graph: Switching RF source')
            self.__needs_reconnect = True

            this_source = self._sources[self.source_name]
            
            def update_input_freqs():
                freq = this_source.get_freq()
                self.input_freq = freq
                self.monitor.set_input_center_freq(freq)
                for receiver in self._receivers.itervalues():
                    receiver.set_input_center_freq(freq)
            
            def tune_hook():
                # 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.__rx_driver.get_tune_delay(), tune_hook_actual)
            
            def tune_hook_actual():
                if self.source is not this_source:
                    return
                update_input_freqs()
                for key in self._receivers:
                    self._update_receiver_validity(key)
                    # TODO: If multiple receivers change validity we'll do redundant reconnects in this loop; avoid that.
            
            if self.__source_tune_subscription is not None:
                self.__source_tune_subscription.unsubscribe()
            self.__source_tune_subscription = this_source.state()['freq'].subscribe(tune_hook)
            
            self.source = this_source
            self.__rx_driver = this_source.get_rx_driver()
            source_signal_type = self.__rx_driver.get_output_type()
            this_rate = source_signal_type.get_sample_rate()
            rate_changed = self.input_rate != this_rate
            self.input_rate = this_rate
            self.monitor.set_signal_type(source_signal_type)
            self.__clip_probe.set_window_and_reconnect(0.5 * this_rate)
            update_input_freqs()
        
        if rate_changed:
            log.msg('Flow graph: Changing sample rates')
            for receiver in self._receivers.itervalues():
                receiver.set_input_rate(self.input_rate)

        if self.__needs_reconnect:
            log.msg('Flow graph: Rebuilding connections')
            self.__needs_reconnect = False
            
            self._recursive_lock()
            self.disconnect_all()
            
            self.connect(
                self.__rx_driver,
                self.monitor)
            self.connect(
                self.__rx_driver,
                self.__clip_probe)

            # Filter receivers
            bus_inputs = defaultdict(lambda: [])
            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 receiver.get_audio_destination() not in self.__audio_buses:
                    log.err('Flow graph: receiver audio destination %r is not available' % (receiver.get_audio_destination(),))
                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.__rx_driver, receiver)
                rrate = receiver.get_output_type().get_sample_rate()
                bus_inputs[receiver.get_audio_destination()].append((rrate, receiver))
            
            self.__has_a_useful_receiver = False
            for key, bus in self.__audio_buses.iteritems():
                inputs = bus_inputs[key]
                if key == CLIENT_AUDIO_DEVICE:
                    outputs = self.audio_queue_sinks.itervalues()
                    noutputs = len(self.audio_queue_sinks)
                else:
                    outputs = [self.__audio_devices[key]]
                    noutputs = 1
                if len(inputs) > 0 and noutputs > 0:
                    self.__has_a_useful_receiver = True
                bus.connect(
                    inputs=inputs,
                    outputs=outputs)
            
            self._recursive_unlock()
            log.msg('Flow graph: ...done reconnecting.')
            
            self.__start_or_stop()

    def _update_receiver_validity(self, key):
        receiver = self._receivers[key]
        if receiver.get_is_valid() != self._receiver_valid[key]:
            self.__needs_reconnect = True
            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(ctor_fn=lambda self:
        Enum({k: v.get_name() or k for (k, v) in self._sources.iteritems()}))
    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()

    def _make_receiver(self, mode, state, key):
        facet = ContextForReceiver(self, key)
        receiver = Receiver(
            mode=mode,
            input_rate=self.input_rate,
            input_center_freq=self.input_freq,
            audio_channels=self.__audio_channels,
            audio_destination=CLIENT_AUDIO_DEVICE,  # TODO match others
            context=facet,
        )
        receiver.state_from_json(state)
        # until _enabled, ignore any callbacks resulting from the state_from_json initialization
        facet._receiver = receiver
        facet._enabled = True
        return receiver
    
    @exported_value(ctor=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''
    
    @exported_value(ctor=int)
    def get_input_rate(self):
        return self.input_rate

    @exported_value()
    def get_audio_bus_rate(self):
        '''
        Not visible externally; for diagnostic purposes only.
        '''
        return [b.get_current_rate() for b in self.__audio_buses.itervalues()]
    
    @exported_value(ctor=float)
    def get_cpu_use(self):
        cur_wall_time = time.time()
        elapsed_wall = cur_wall_time - self.last_wall_time
        if elapsed_wall > 0.5:
            cur_cpu_time = time.clock()
            elapsed_cpu = cur_cpu_time - self.last_cpu_time
            self.last_wall_time = cur_wall_time
            self.last_cpu_time = cur_cpu_time
            self.last_cpu_use = round(elapsed_cpu / elapsed_wall, 2)
        return self.last_cpu_use
    
    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_audio_destination_type(self):
        '''for ContextForReceiver only'''
        return self.__audio_destination_type
    
    def _trigger_reconnect(self):
        self.__needs_reconnect = True
        self._do_connect()
    
    def _recursive_lock_hook(self):
        for source in self._sources.itervalues():
            source.notify_reconnecting_or_restarting()