def _install_cell(self, name, is_level, writable, callback, caps): # this is a function for the sake of the closure variables if name == 'Frequency': cell_name = 'freq' # consistency with our naming scheme elsewhere, also IHasFrequency else: cell_name = name if is_level: # TODO: Use range info from hamlib if available if name == 'STRENGTH level': vtype = Range([(-54, 50)], strict=False) elif name == 'SWR level': vtype = Range([(1, 30)], strict=False) elif name == 'RFPOWER level': vtype = Range([(0, 100)], strict=False) else: vtype = Range([(-10, 10)], strict=False) elif name == 'Mode' or name == 'TX Mode': # kludge vtype = Enum({x: x for x in caps['Mode list'].strip().split(' ')}) elif name == 'VFO' or name == 'TX VFO': vtype = Enum({x: x for x in caps['VFO list'].strip().split(' ')}) else: vtype = self._info[name] def updater(strval): try: if vtype is bool: value = bool(int(strval)) else: value = vtype(strval) except ValueError: value = unicode(strval) cell.set_internal(value) def actually_write_value(value): if vtype is bool: self._ehs_set(name, str(int(value))) else: self._ehs_set(name, str(vtype(value))) cell = LooseCell( key=cell_name, value='placeholder', type=vtype, writable=writable, persists=False, post_hook=actually_write_value, label=name) # TODO: supply label values from _info table self._cell_updaters[name] = updater updater(self._ehs_get(name)) callback(cell)
def __init__(self, graph, audio_config, stereo=True): #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 = {} self.__audio_devices = audio_devices 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) self.__audio_channels = 2 if stereo else 1 self.__audio_queue_sinks = {} self.__audio_buses = { key: BusPlumber(graph, self.__audio_channels) for key in audio_destination_dict }
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 setUp(self): self.endpoint = _StringEndpoint() self.device = Controller( reactor=the_reactor, endpoint=self.endpoint, elements=[ Command('cmd_name', 'cmd_text'), Command('unicode_cmd', u'façade'), Selector('enum_name', Enum({u'enum_text1': u'enum_label1', u'enum_text2': u'enum_label2'}, strict=False)) ], encoding='UTF-8') self.proxy = self.device.get_components_dict()['controller']
def __init__(self, osmo_device, source, profile, name, tuning): gr.hier_block2.__init__( self, 'RX ' + name, gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__osmo_device = osmo_device self.__source = source self.__profile = profile self.__name = name self.__tuning = tuning self.__antenna_type = Enum( { unicode(name): unicode(name) for name in self.__source.get_antennas() }, strict=True) self.connect(self.__source, self) self.__gains = Gains(source) # Misc state self.dc_state = DCOffsetOff self.iq_state = IQBalanceOff source.set_dc_offset_mode(self.dc_state, ch) # no getter, set to known state source.set_iq_balance_mode(self.iq_state, ch) # no getter, set to known state # Blocks self.__state_while_inactive = {} self.__placeholder = blocks.vector_source_c([]) sample_rate = float(source.get_sample_rate()) self.__signal_type = SignalType(kind='IQ', sample_rate=sample_rate) self.__usable_bandwidth = tuning.calc_usable_bandwidth(sample_rate)
import csv import json import os import os.path import urllib from twisted.python import log from twisted.web import http from twisted.web import resource from shinysdr.types import Enum, to_value_type _NO_DEFAULT = object() _json_columns = { u'type': (Enum({'channel': 'channel', 'band': 'band'}), 'channel'), u'lowerFreq': (to_value_type(float), _NO_DEFAULT), u'upperFreq': (to_value_type(float), _NO_DEFAULT), u'mode': (to_value_type(unicode), u''), u'label': (to_value_type(unicode), u''), u'notes': (to_value_type(unicode), u''), u'location': (lambda x: x, None), # TODO missing constraint } _LOWEST_RKEY = 1 class DatabaseModel(object): __dirty = False def __init__(self, reactor, records, pathname=None, writable=False):
def test_Enum_strict(self): _testType(self, Enum({u'a': u'a', u'b': u'b'}, strict=True), [(u'a', u'a'), ('a', u'a')], [u'c', 999])
class _HamlibRig(_HamlibProxy): implements(IRig, IHasFrequency) _server_name = 'rigctld' _dummy_command = 'get_freq' _info = { 'Frequency': (Range([(0, 9999999999)], integer=True)), 'Mode': (_modes), 'Passband': (_passbands), 'VFO': (_vfos), 'RIT': (int), 'XIT': (int), 'PTT': (bool), 'DCD': (bool), 'Rptr Shift': (Enum({ '+': '+', '-': '-', 'None': 'None' }, strict=False)), 'Rptr Offset': (int), 'CTCSS Tone': (int), 'DCS Code': (str), 'CTCSS Sql': (int), 'DCS Sql': (str), 'TX Frequency': (int), 'TX Mode': (_modes), 'TX Passband': (_passbands), 'Split': (bool), 'TX VFO': (_vfos), 'Tuning Step': (int), 'Antenna': (int), } _commands = { 'freq': ['Frequency'], 'mode': ['Mode', 'Passband'], 'vfo': ['VFO'], 'rit': ['RIT'], 'xit': ['XIT'], # 'ptt': ['PTT'], # writing disabled until when we're more confident in correct functioning 'rptr_shift': ['Rptr Shift'], 'rptr_offs': ['Rptr Offset'], 'ctcss_tone': ['CTCSS Tone'], 'dcs_code': ['DCS Code'], 'ctcss_sql': ['CTCSS Sql'], 'dcs_sql': ['DCS Sql'], 'split_freq': ['TX Frequency'], 'split_mode': ['TX Mode', 'TX Passband'], 'split_vfo': ['Split', 'TX VFO'], 'ts': ['Tuning Step'], # TODO: describe func, level, parm 'ant': ['Antenna'], 'powerstat': ['Power Stat'], } def poll_fast(self, send): # likely to be set by hw controls send('get_freq') send('get_mode') # received signal info send('get_dcd') def poll_slow(self, send): send('get_vfo') send('get_rit') send('get_xit') send('get_ptt') send('get_rptr_shift') send('get_rptr_offs') send('get_ctcss_tone') send('get_dcs_code') send('get_split_freq') send('get_split_mode') send('get_split_vfo') send('get_ts')
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)
def test_values(self): enum = Enum({u'a': u'adesc'}) self.assertEquals(enum.get_table(), {u'a': EnumRow(u'adesc', associated_key=u'a')})
RIG_EPROTO = -7 RIG_ERJCTED = -8 RIG_ETRUNC = -9 RIG_ENAVAIL = -10 RIG_ENTARGET = -11 RIG_BUSERROR = -12 RIG_BUSBUSY = -13 RIG_EARG = -14 RIG_EVFO = -15 RIG_EDOM = -16 _modes = Enum( { x: x for x in [ 'USB', 'LSB', 'CW', 'CWR', 'RTTY', 'RTTYR', 'AM', 'FM', 'WFM', 'AMS', 'PKTLSB', 'PKTUSB', 'PKTFM', 'ECSSUSB', 'ECSSLSB', 'FAX', 'SAM', 'SAL', 'SAH', 'DSB' ] }, strict=False) _vfos = Enum( { 'VFOA': 'VFO A', 'VFOB': 'VFO B', 'VFOC': 'VFO C', 'currVFO': 'currVFO', 'VFO': 'VFO', 'MEM': 'MEM', 'Main': 'Main', 'Sub': 'Sub',
self.split_block = blocks.complex_to_float(1) self.connect(self, self.band_filter_block, self.rf_squelch_block, self.split_block) self.connect(self.band_filter_block, self.rf_probe_block) self.connect_audio_output((self.split_block, 0), (self.split_block, 1)) pluginDef_iq = ModeDef(mode='IQ', info='Raw I/Q', demod_class=IQDemodulator) _am_lower_cutoff_freq = 40 _am_audio_bandwidth = 7500 _am_demod_method_type = Enum({ u'async': u'Asynchronous', u'lsb': u'Lower sideband', u'usb': u'Upper sideband', u'stereo': u'ISB stereo', }) class AMDemodulator(SimpleAudioDemodulator): """Amplitude modulation (AM) demodulator.""" __demod_rate = 16000 def __init__(self, context, demod_method=u'async', **kwargs): SimpleAudioDemodulator.__init__(self, context=context, stereo=True, audio_rate=self.__demod_rate, demod_rate=self.__demod_rate,
def test_strict_by_default(self): _testType(self, Enum({ u'a': u'a', u'b': u'b' }), [(u'a', u'a'), ('a', u'a')], [u'c', 999])
def test_Enum_lenient(self): _testType(self, Enum({u'a': u'a', u'b': u'b'}, strict=False), [(u'a', u'a'), ('a', u'a'), u'c', (999, u'999')], [])
class _OsmoSDRRXDriver(ExportedState, gr.hier_block2): implements(IRXDriver) # Note: Docs for gr-osmosdr are in comments at gr-osmosdr/lib/source_iface.h def __init__(self, source, name, sample_rate, tuning): gr.hier_block2.__init__( self, name, gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__name = name self.__tuning = tuning self.__source = source self.connect(self.__source, self) self.gains = Gains(source) # Misc state self.dc_state = 0 self.iq_state = 0 source.set_dc_offset_mode(self.dc_state, ch) # no getter, set to known state source.set_iq_balance_mode(self.iq_state, ch) # no getter, set to known state sample_rate = float(source.get_sample_rate()) self.__signal_type = SignalType(kind='IQ', sample_rate=sample_rate) self.__usable_bandwidth = tuning.calc_usable_bandwidth(sample_rate) def state_def(self, callback): super(_OsmoSDRRXDriver, self).state_def(callback) # TODO make this possible to be decorator style callback(BlockCell(self, 'gains')) @exported_value(ctor=SignalType) def get_output_type(self): return self.__signal_type # implement IRXDriver def get_tune_delay(self): return 0.25 # TODO: make configurable and/or account for as many factors as we can # implement IRXDriver def get_usable_bandwidth(self): return self.__usable_bandwidth # implement IRXDriver def close(self): # Not found to be strictly necessary, because Device will drop this driver, but hey. self.__source = None self.disconnect_all() @exported_value(ctor=float) def get_correction_ppm(self): return self.__tuning.get_correction_ppm() @setter def set_correction_ppm(self, value): self.__tuning.set_correction_ppm(value) @exported_value(ctor_fn=lambda self: convert_osmosdr_range( self.__source.get_gain_range(ch), strict=False)) def get_gain(self): return self.__source.get_gain(ch) @setter def set_gain(self, value): self.__source.set_gain(float(value), ch) @exported_value(ctor=bool) def get_agc(self): return bool(self.__source.get_gain_mode(ch)) @setter def set_agc(self, value): self.__source.set_gain_mode(bool(value), ch) @exported_value(ctor_fn=lambda self: Enum({ unicode(name): unicode(name) for name in self.__source.get_antennas() })) def get_antenna(self): return unicode(self.__source.get_antenna(ch)) # TODO review whether set_antenna is safe to expose # Note: dc_cancel has a 'manual' mode we are not yet exposing @exported_value(ctor=bool) def get_dc_cancel(self): return bool(self.dc_state) @setter def set_dc_cancel(self, value): self.dc_state = bool(value) if self.dc_state: mode = 2 # automatic mode else: mode = 0 self.__source.set_dc_offset_mode(mode, ch) # Note: iq_balance has a 'manual' mode we are not yet exposing @exported_value(ctor=bool) def get_iq_balance(self): return bool(self.iq_state) @setter def set_iq_balance(self, value): self.iq_state = bool(value) if self.iq_state: mode = 2 # automatic mode else: mode = 0 self.__source.set_iq_balance_mode(mode, ch) # add_zero because zero means automatic setting based on sample rate. # TODO: Display automaticness in the UI rather than having a zero value. @exported_value(ctor_fn=lambda self: convert_osmosdr_range( self.__source.get_bandwidth_range(ch), add_zero=True)) def get_bandwidth(self): return self.__source.get_bandwidth(ch) @setter def set_bandwidth(self, value): self.__source.set_bandwidth(float(value), ch) def notify_reconnecting_or_restarting(self): pass
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()
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()
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)
# Audio copy output unconverter = blocks.short_to_float(vlen=1, scale=int_scale) self.connect(to_short, unconverter) self.connect(unconverter, self) def get_input_type(self): return SignalType(kind='MONO', sample_rate=pipe_rate) def get_output_type(self): return SignalType(kind='MONO', sample_rate=pipe_rate) _aprs_squelch_type = Enum( { u'mute': u'Muted', u'ctcss': 'Voice Alert', u'monitor': u'Monitor' }, strict=True) class APRSDemodulator(gr.hier_block2, ExportedState): """ Demod and parse APRS. """ def __init__(self, context): gr.hier_block2.__init__( self, self.__class__.__name__, gr.io_signature(1, 1, gr.sizeof_float * 1), gr.io_signature(1, 1, gr.sizeof_float * 1),
def __row(self, row): return Enum({u'key': row}).get_table()[u'key']