def _add_plugin_resources(client_resource): # Plugin resources and plugin info load_list_css = [] load_list_js = [] mode_table = {} plugin_resources = Resource() client_resource.putChild('plugins', plugin_resources) for resource_def in getPlugins(IClientResourceDef, shinysdr.plugins): # Add the plugin's resource to static serving plugin_resources.putChild(resource_def.key, resource_def.resource) plugin_resource_url = '/client/plugins/' + urllib.quote(resource_def.key, safe='') + '/' # Tell the client to load the plugins # TODO constrain path values to be relative (not on a different origin, to not leak urls) if resource_def.load_css_path is not None: load_list_css.append(plugin_resource_url + resource_def.load_cs_path) if resource_def.load_js_path is not None: # TODO constrain value to be in the directory load_list_js.append(plugin_resource_url + resource_def.load_js_path) for mode_def in get_modes(): mode_table[mode_def.mode] = { u'label': mode_def.label, u'can_transmit': mode_def.mod_class is not None } # Client gets info about plugins through this resource client_resource.putChild('plugin-index.json', static.Data(_serialize({ u'css': load_list_css, u'js': load_list_js, u'modes': mode_table, }).encode('utf-8'), 'application/json'))
def _add_plugin_resources(client_resource): # Plugin resources and plugin info load_list_css = [] load_list_js = [] mode_table = {} plugin_resources = Resource() client_resource.putChild('plugins', plugin_resources) for resource_def in getPlugins(IClientResourceDef, shinysdr.plugins): # Add the plugin's resource to static serving plugin_resources.putChild(resource_def.key, resource_def.resource) plugin_resource_url = '/client/plugins/' + urllib.quote( resource_def.key, safe='') + '/' # Tell the client to load the plugins # TODO constrain path values to be relative (not on a different origin, to not leak urls) if resource_def.load_css_path is not None: load_list_css.append(plugin_resource_url + resource_def.load_cs_path) if resource_def.load_js_path is not None: # TODO constrain value to be in the directory load_list_js.append(plugin_resource_url + resource_def.load_js_path) for mode_def in get_modes(): mode_table[mode_def.mode] = { u'info_enum_row': mode_def.info.to_json(), u'can_transmit': mode_def.mod_class is not None } # Client gets info about plugins through this resource client_resource.putChild( 'plugin-index.json', static.Data( _serialize({ u'css': load_list_css, u'js': load_list_js, u'modes': mode_table, }).encode('utf-8'), 'application/json'))
class Receiver(gr.hier_block2, ExportedState): implements(IReceiver) def __init__(self, mode, freq_absolute=100.0, freq_relative=None, freq_linked_to_device=False, audio_destination=None, device_name=None, audio_gain=-6, audio_pan=0, audio_channels=0, context=None): assert audio_channels == 1 or audio_channels == 2 assert audio_destination is not None assert device_name is not None gr.hier_block2.__init__( # str() because insists on non-unicode self, str('%s receiver' % (mode, )), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(audio_channels, audio_channels, gr.sizeof_float * 1), ) if lookup_mode(mode) is None: # TODO: communicate back to client if applicable log.msg('Unknown mode %r in Receiver(); using AM' % (mode, )) mode = 'AM' # Provided by caller self.context = context self.__audio_channels = audio_channels # cached info from device self.__device_name = device_name # Simple state self.mode = mode self.audio_gain = audio_gain self.audio_pan = min(1, max(-1, audio_pan)) self.__audio_destination = audio_destination # Receive frequency. self.__freq_linked_to_device = bool(freq_linked_to_device) if self.__freq_linked_to_device and freq_relative is not None: self.__freq_relative = float(freq_relative) self.__freq_absolute = self.__freq_relative + self.__get_device( ).get_freq() else: self.__freq_absolute = float(freq_absolute) self.__freq_relative = self.__freq_absolute - self.__get_device( ).get_freq() # Blocks self.__rotator = blocks.rotator_cc() self.__demodulator = self.__make_demodulator(mode, {}) self.__update_demodulator_info() self.__audio_gain_blocks = [ blocks.multiply_const_ff(0.0) for _ in xrange(self.__audio_channels) ] self.probe_audio = analog.probe_avg_mag_sqrd_f( 0, alpha=10.0 / 44100) # TODO adapt to output audio rate # Other internals self.__last_output_type = None self.__update_rotator( ) # initialize rotator, also in case of __demod_tunable self.__update_audio_gain() self.__do_connect(reason=u'initialization') def __update_demodulator_info(self): self.__demod_tunable = ITunableDemodulator.providedBy( self.__demodulator) output_type = self.__demodulator.get_output_type() assert isinstance(output_type, SignalType) # TODO: better expression of this condition assert output_type.get_kind() == 'STEREO' or output_type.get_kind( ) == 'MONO' or output_type.get_kind() == 'NONE' self.__demod_output = output_type.get_kind() != 'NONE' self.__demod_stereo = output_type.get_kind() == 'STEREO' self.__output_type = SignalType( kind='STEREO', sample_rate=output_type.get_sample_rate() if self.__demod_output else 0) def __do_connect(self, reason): #log.msg(u'receiver do_connect: %s' % (reason,)) self.context.lock() try: self.disconnect_all() # Connect input of demodulator if self.__demod_tunable: self.connect(self, self.__demodulator) else: self.connect(self, self.__rotator, self.__demodulator) if self.__demod_output: # Connect output of demodulator self.connect((self.__demodulator, 0), self.__audio_gain_blocks[0]) # left or mono if self.__audio_channels == 2: self.connect( (self.__demodulator, 1 if self.__demod_stereo else 0), self.__audio_gain_blocks[1]) else: if self.__demod_stereo: self.connect((self.__demodulator, 1), blocks.null_sink(gr.sizeof_float)) # Connect output of receiver for ch in xrange(self.__audio_channels): self.connect(self.__audio_gain_blocks[ch], (self, ch)) # Level meter # TODO: should mix left and right or something self.connect((self.__demodulator, 0), self.probe_audio) else: # Dummy output, ignored by containing block source_of_nothing = blocks.vector_source_f([]) for ch in xrange(0, self.__audio_channels): self.connect(source_of_nothing, (self, ch)) if self.__output_type != self.__last_output_type: self.__last_output_type = self.__output_type self.context.changed_needed_connections(u'changed output type') finally: self.context.unlock() def get_output_type(self): return self.__output_type def changed_device_freq(self): if self.__freq_linked_to_device: self.__freq_absolute = self.__freq_relative + self.__get_device( ).get_freq() else: self.__freq_relative = self.__freq_absolute - self.__get_device( ).get_freq() self.__update_rotator() # note does not revalidate() because the caller will handle that @exported_block() def get_demodulator(self): return self.__demodulator @exported_value(type_fn=lambda self: self.context.get_rx_device_type()) def get_device_name(self): return self.__device_name @setter def set_device_name(self, value): value = self.context.get_rx_device_type()(value) if self.__device_name != value: self.__device_name = value self.changed_device_freq() # freq self._rebuild_demodulator( reason=u'changed device, thus maybe sample rate') # rate self.context.changed_needed_connections(u'changed device') # type construction is deferred because we don't want loading this file to trigger loading plugins @exported_value( type_fn=lambda self: Enum({d.mode: d.label for d in get_modes()})) def get_mode(self): return self.mode @setter def set_mode(self, mode): mode = unicode(mode) if mode == self.mode: return if self.__demodulator and self.__demodulator.can_set_mode(mode): self.__demodulator.set_mode(mode) self.mode = mode else: self._rebuild_demodulator(mode=mode, reason=u'changed mode') # TODO: rename rec_freq to just freq @exported_value(type=float, parameter='freq_absolute') def get_rec_freq(self): return self.__freq_absolute @setter def set_rec_freq(self, absolute): absolute = float(absolute) if self.__freq_linked_to_device: # Temporarily violating the (device freq + relative freq = absolute freq) invariant, which will be restored below by changing the device freq. self.__freq_absolute = absolute else: self.__freq_absolute = absolute self.__freq_relative = absolute - self.__get_device().get_freq() self.__update_rotator() if self.__freq_linked_to_device: # TODO: reconsider whether we should be giving commands directly to the device, vs. going through the context. self.__get_device().set_freq(self.__freq_absolute - self.__freq_relative) else: self.context.revalidate(tuning=True) @exported_value(type=bool) def get_freq_linked_to_device(self): return self.__freq_linked_to_device @setter def set_freq_linked_to_device(self, value): self.__freq_linked_to_device = bool(value) # TODO: support non-audio demodulators at which point these controls should be optional @exported_value(parameter='audio_gain', type=Range([(-30, 20)], strict=False)) def get_audio_gain(self): return self.audio_gain @setter def set_audio_gain(self, value): self.audio_gain = value self.__update_audio_gain() @exported_value(type_fn=lambda self: Range( [(-1, 1)] if self.__audio_channels > 1 else [(0, 0)], strict=True)) def get_audio_pan(self): return self.audio_pan @setter def set_audio_pan(self, value): self.audio_pan = value self.__update_audio_gain() @exported_value( type_fn=lambda self: self.context.get_audio_destination_type()) def get_audio_destination(self): return self.__audio_destination @setter def set_audio_destination(self, value): if self.__audio_destination != value: self.__audio_destination = value self.context.changed_needed_connections(u'changed destination') @exported_value(type=bool) def get_is_valid(self): if self.__demodulator is None: return False half_sample_rate = self.__get_device().get_rx_driver().get_output_type( ).get_sample_rate() / 2 demod_shape = self.__demodulator.get_band_filter_shape() valid_bandwidth_lower = -half_sample_rate - self.__freq_relative valid_bandwidth_upper = half_sample_rate - self.__freq_relative return valid_bandwidth_lower <= min(0, demod_shape['low']) and \ valid_bandwidth_upper >= max(0, demod_shape['high']) # Note that the receiver cannot measure RF power because we don't know what the channel bandwidth is; we have to leave that to the demodulator. @exported_value(type=Range([(_audio_power_minimum_dB, 0)], strict=False)) def get_audio_power(self): if self.get_is_valid(): return todB( max(_audio_power_minimum_amplitude, self.probe_audio.level())) else: # will not be receiving samples, so probe's value will be meaningless return _audio_power_minimum_dB def __update_rotator(self): device = self.__get_device() sample_rate = device.get_rx_driver().get_output_type().get_sample_rate( ) if self.__demod_tunable: # TODO: Method should perhaps be renamed to convey that it is relative self.__demodulator.set_rec_freq(self.__freq_relative) else: self.__rotator.set_phase_inc( rotator_inc(rate=sample_rate, shift=-self.__freq_relative)) def __get_device(self): return self.context.get_device(self.__device_name) # called from facet def _rebuild_demodulator(self, mode=None, reason='<unspecified>'): self.__rebuild_demodulator_nodirty(mode) self.__do_connect(reason=u'demodulator rebuilt: %s' % (reason, )) # TODO write a test for this! #self.context.revalidaate(tuning=False) # in case our bandwidth changed def __rebuild_demodulator_nodirty(self, mode=None): if self.__demodulator is None: defaults = {} else: defaults = self.__demodulator.state_to_json() if mode is None: mode = self.mode self.__demodulator = self.__make_demodulator(mode, defaults) self.__update_demodulator_info() self.__update_rotator() self.mode = mode # Replace blocks downstream of the demodulator so as to flush samples that are potentially at a different sample rate and would therefore be audibly wrong. Caller will handle reconnection. self.__audio_gain_blocks = [ blocks.multiply_const_ff(0.0) for _ in xrange(self.__audio_channels) ] self.__update_audio_gain() def __make_demodulator(self, mode, state): """Returns the demodulator.""" t0 = time.time() mode_def = lookup_mode(mode) if mode_def is None: # TODO: Better handling, like maybe a dummy demod raise ValueError('Unknown mode: ' + mode) clas = mode_def.demod_class state = state.copy() # don't modify arg if 'mode' in state: del state[ 'mode'] # don't switch back to the mode we just switched from facet = ContextForDemodulator(self) init_kwargs = dict(mode=mode, input_rate=self.__get_device().get_rx_driver(). get_output_type().get_sample_rate(), context=facet) demodulator = unserialize_exported_state(ctor=clas, state=state, kwargs=init_kwargs) # until _enabled, ignore any callbacks resulting from unserialization calling setters facet._enabled = True log.msg('Constructed %s demodulator: %i ms.' % (mode, (time.time() - t0) * 1000)) return demodulator def __update_audio_gain(self): gain_lin = dB(self.audio_gain) if self.__audio_channels == 2: pan = self.audio_pan # TODO: Determine correct computation for panning. http://en.wikipedia.org/wiki/Pan_law seems relevant but was short on actual formulas. May depend on headphones vs speakers? This may be correct already for headphones -- it sounds nearly-flat to me. self.__audio_gain_blocks[0].set_k(gain_lin * (1 - pan)) self.__audio_gain_blocks[1].set_k(gain_lin * (1 + pan)) else: self.__audio_gain_blocks[0].set_k(gain_lin)
class Receiver(gr.hier_block2, ExportedState): implements(IReceiver) def __init__(self, mode, input_rate=0, input_center_freq=0, rec_freq=100.0, audio_destination=None, audio_gain=-6, audio_pan=0, audio_channels=0, context=None): assert input_rate > 0 assert audio_channels == 1 or audio_channels == 2 assert audio_destination is not None gr.hier_block2.__init__( # str() because insists on non-unicode self, str('%s receiver' % (mode, )), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(audio_channels, audio_channels, gr.sizeof_float * 1), ) if lookup_mode(mode) is None: # TODO: communicate back to client if applicable log.msg('Unknown mode %r in Receiver(); using AM' % (mode, )) mode = 'AM' # Provided by caller self.context = context self.input_rate = input_rate self.input_center_freq = input_center_freq self.__audio_channels = audio_channels # Simple state self.mode = mode self.rec_freq = rec_freq self.audio_gain = audio_gain self.audio_pan = min(1, max(-1, audio_pan)) self.__audio_destination = audio_destination # Blocks self.__rotator = blocks.rotator_cc() self.demodulator = self.__make_demodulator(mode, {}) self.__update_demodulator_info() self.__audio_gain_blocks = [ blocks.multiply_const_ff(0.0) for _ in xrange(self.__audio_channels) ] self.probe_audio = analog.probe_avg_mag_sqrd_f( 0, alpha=10.0 / 44100) # TODO adapt to output audio rate # Other internals self.__last_output_type = None self.__update_rotator( ) # initialize rotator, also in case of __demod_tunable self.__update_audio_gain() self.__do_connect() def state_def(self, callback): super(Receiver, self).state_def(callback) # TODO decoratorify callback(BlockCell(self, 'demodulator')) def __update_demodulator_info(self): self.__demod_tunable = ITunableDemodulator.providedBy(self.demodulator) output_type = self.demodulator.get_output_type() assert isinstance(output_type, SignalType) # TODO: better expression of this condition assert output_type.get_kind() == 'STEREO' or output_type.get_kind( ) == 'MONO' or output_type.get_kind() == 'NONE' self.__demod_output = output_type.get_kind() != 'NONE' self.__demod_stereo = output_type.get_kind() == 'STEREO' self.__output_type = SignalType( kind='STEREO', sample_rate=output_type.get_sample_rate() if self.__demod_output else _dummy_audio_rate) def __do_connect(self): self.context.lock() try: self.disconnect_all() # Connect input of demodulator if self.__demod_tunable: self.connect(self, self.demodulator) else: self.connect(self, self.__rotator, self.demodulator) if self.__demod_output: # Connect output of demodulator self.connect((self.demodulator, 0), self.__audio_gain_blocks[0]) # left or mono if self.__audio_channels == 2: self.connect( (self.demodulator, 1 if self.__demod_stereo else 0), self.__audio_gain_blocks[1]) else: if self.__demod_stereo: self.connect((self.demodulator, 1), blocks.null_sink(gr.sizeof_float)) # Connect output of receiver for ch in xrange(self.__audio_channels): self.connect(self.__audio_gain_blocks[ch], (self, ch)) # Level meter # TODO: should mix left and right or something self.connect((self.demodulator, 0), self.probe_audio) else: # Dummy output. # TODO: Teach top block about no-audio so we don't have to have a dummy output. throttle = blocks.throttle(gr.sizeof_float, _dummy_audio_rate) throttle.set_max_output_buffer(_dummy_audio_rate // 10) # ensure smooth output self.connect( analog.sig_source_f(0, analog.GR_CONST_WAVE, 0, 0, 0), throttle) for ch in xrange(self.__audio_channels): self.connect(throttle, (self, ch)) if self.__output_type != self.__last_output_type: self.__last_output_type = self.__output_type self.context.changed_output_type_or_destination() finally: self.context.unlock() def get_output_type(self): return self.__output_type def set_input_rate(self, value): value = int(value) if self.input_rate == value: return self.input_rate = value self._rebuild_demodulator() def set_input_center_freq(self, value): self.input_center_freq = value self.__update_rotator() # note does not revalidate() because the caller will handle that # type construction is deferred because we don't want loading this file to trigger loading plugins @exported_value( ctor_fn=lambda self: Enum({d.mode: d.label for d in get_modes()})) def get_mode(self): return self.mode @setter def set_mode(self, mode): mode = unicode(mode) if self.demodulator and self.demodulator.can_set_mode(mode): self.demodulator.set_mode(mode) self.mode = mode else: self._rebuild_demodulator(mode=mode) # TODO: rename rec_freq to just freq @exported_value(ctor=float) def get_rec_freq(self): return self.rec_freq @setter def set_rec_freq(self, rec_freq): self.rec_freq = float(rec_freq) self.__update_rotator() self.context.revalidate() # TODO: support non-audio demodulators at which point these controls should be optional @exported_value(ctor=Range([(-30, 20)], strict=False)) def get_audio_gain(self): return self.audio_gain @setter def set_audio_gain(self, value): self.audio_gain = value self.__update_audio_gain() @exported_value(ctor_fn=lambda self: Range( [(-1, 1)] if self.__audio_channels > 1 else [(0, 0)], strict=True)) def get_audio_pan(self): return self.audio_pan @setter def set_audio_pan(self, value): self.audio_pan = value self.__update_audio_gain() @exported_value( parameter='audio_destination', ctor_fn=lambda self: self.context.get_audio_destination_type()) def get_audio_destination(self): return self.__audio_destination @setter def set_audio_destination(self, value): if self.__audio_destination != value: self.__audio_destination = value self.context.changed_output_type_or_destination() @exported_value(ctor=bool) def get_is_valid(self): valid_bandwidth = self.input_rate / 2 - abs(self.rec_freq - self.input_center_freq) return self.demodulator is not None and valid_bandwidth >= self.demodulator.get_half_bandwidth( ) # Note that the receiver cannot measure RF power because we don't know what the channel bandwidth is; we have to leave that to the demodulator. @exported_value(ctor=Range([(_audio_power_minimum_dB, 0)], strict=False)) def get_audio_power(self): if self.get_is_valid(): return todB( max(_audio_power_minimum_amplitude, self.probe_audio.level())) else: # will not be receiving samples, so probe's value will be meaningless return _audio_power_minimum_dB def __update_rotator(self): offset = self.rec_freq - self.input_center_freq if self.__demod_tunable: self.demodulator.set_rec_freq(offset) else: self.__rotator.set_phase_inc( rotator_inc(rate=self.input_rate, shift=-offset)) # called from facet def _rebuild_demodulator(self, mode=None): self.__rebuild_demodulator_nodirty(mode) self.__do_connect() def __rebuild_demodulator_nodirty(self, mode=None): if self.demodulator is None: defaults = {} else: defaults = self.demodulator.state_to_json() if mode is None: mode = self.mode self.demodulator = self.__make_demodulator(mode, defaults) self.__update_demodulator_info() self.__update_rotator() self.mode = mode # Replace blocks downstream of the demodulator so as to flush samples that are potentially at a different sample rate and would therefore be audibly wrong. Caller will handle reconnection. self.__audio_gain_blocks = [ blocks.multiply_const_ff(0.0) for _ in xrange(self.__audio_channels) ] self.__update_audio_gain() def __make_demodulator(self, mode, state): '''Returns the demodulator.''' mode_def = lookup_mode(mode) if mode_def is None: # TODO: Better handling, like maybe a dummy demod raise ValueError('Unknown mode: ' + mode) clas = mode_def.demod_class state = state.copy() # don't modify arg if 'mode' in state: del state[ 'mode'] # don't switch back to the mode we just switched from facet = ContextForDemodulator(self) init_kwargs = dict(mode=mode, input_rate=self.input_rate, context=facet) for sh_key, sh_ctor in mode_def.shared_objects.iteritems(): init_kwargs[sh_key] = self.context.get_shared_object(sh_ctor) demodulator = unserialize_exported_state(ctor=clas, state=state, kwargs=init_kwargs) # until _enabled, ignore any callbacks resulting from unserialization calling setters facet._enabled = True return demodulator def __update_audio_gain(self): gain_lin = dB(self.audio_gain) if self.__audio_channels == 2: pan = self.audio_pan # TODO: Determine correct computation for panning. http://en.wikipedia.org/wiki/Pan_law seems relevant but was short on actual formulas. May depend on headphones vs speakers? This may be correct already for headphones -- it sounds nearly-flat to me. self.__audio_gain_blocks[0].set_k(gain_lin * (1 - pan)) self.__audio_gain_blocks[1].set_k(gain_lin * (1 + pan)) else: self.__audio_gain_blocks[0].set_k(gain_lin)