class AppRoot(ExportedState): def __init__(self, devices, audio_config, read_only_dbs, writable_db, features): self.__receive_flowgraph = Top(devices=devices, audio_config=audio_config, features=features) # TODO: only one session while we sort out other things self.__session = Session(receive_flowgraph=self.__receive_flowgraph, read_only_dbs=read_only_dbs, writable_db=writable_db, features=features) @exported_value(type=ReferenceT(), changes='never') def get_receive_flowgraph(self): # TODO needs to go away return self.__receive_flowgraph @exported_value(type=ReferenceT(), persists=True, changes='never') def get_devices(self): """Return all existant devices. This exists only for persistence purposes. """ return self.__receive_flowgraph.get_sources() # TODO: should become something more like 'create new session' def get_session(self): return self.__session def close_all_devices(self): self.__receive_flowgraph.close_all_devices()
class DuplicateReferenceSpecimen(ExportedState): """Helper for TestStateStream""" def __init__(self): self.foo = self.bar = nullExportedState @exported_value(type=ReferenceT(), changes='explicit') def get_foo(self): return self.foo @exported_value(type=ReferenceT(), changes='explicit') def get_bar(self): return self.bar
def __init__(self, send, root_object, root_url, subscription_context=the_subscription_context): self.__subscription_context = subscription_context self._send = send self.__root_object = root_object self._cell = Cell(self, '_root_object', type=ReferenceT(), changes='never') self._lastSerial = 0 root_registration = _StateStreamObjectRegistration( ssi=self, subscription_context=self.__subscription_context, obj=self._cell, serial=0, url=root_url, refcount=0) self._registered_objs = {self._cell: root_registration} self.__registered_serials = { root_registration.serial: root_registration } self._send_batch = [] self.__batch_delay = None self.__root_url = root_url root_registration.send_now_if_needed()
class DSDDemodulator(gr.hier_block2, ExportedState): def __init__(self, mode, input_rate=0, context=None): assert input_rate > 0 gr.hier_block2.__init__(self, type(self).__name__, gr.io_signature(1, 1, gr.sizeof_gr_complex), gr.io_signature(1, 1, gr.sizeof_float)) # TODO: Retry telling the NFMDemodulator to have its output rate be pipe_rate instead of using a resampler. Something went wrong when trying that before. Same thing is done in multimon.py self.__fm_demod = NFMDemodulator( mode='NFM', input_rate=input_rate, no_audio_filter=True, # don't remove CTCSS tone tau=None) # no deemphasis assert self.__fm_demod.get_output_type().get_kind() == 'MONO' fm_audio_rate = self.__fm_demod.get_output_type().get_sample_rate() self.__output_type = SignalType(kind='MONO', sample_rate=8000) self.connect(self, self.__fm_demod, make_resampler(fm_audio_rate, _demod_rate), dsd_block_ff(), self) @exported_value(type=ReferenceT(), changes='never') def get_fm_demod(self): return self.__fm_demod def get_output_type(self): return self.__output_type @exported_value(type=BandShape, changes='never') def get_band_shape(self): return self.__fm_demod.get_band_shape()
class _SimulatedTransmitter(gr.hier_block2, ExportedState): """provides frequency parameters""" def __init__(self, modulator, audio_rate, rf_rate, freq): modulator = IModulator(modulator) gr.hier_block2.__init__( self, 'SimulatedChannel', gr.io_signature(1, 1, gr.sizeof_float * 1), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) self.__freq = freq self.__rf_rate = rf_rate self.__modulator = modulator modulator_input_type = modulator.get_input_type() if modulator_input_type.get_kind() == 'MONO': audio_resampler = make_resampler(audio_rate, modulator_input_type.get_sample_rate()) self.connect(self, audio_resampler, modulator) elif modulator_input_type.get_kind() == 'NONE': self.connect(self, blocks.null_sink(gr.sizeof_float)) else: raise Exception('don\'t know how to supply input of type %s' % modulator_input_type) rf_resampler = rational_resampler.rational_resampler_ccf( interpolation=int(rf_rate), decimation=int(modulator.get_output_type().get_sample_rate())) self.__rotator = blocks.rotator_cc(rotator_inc(rate=rf_rate, shift=freq)) self.__mult = blocks.multiply_const_cc(dB(-10)) self.connect(modulator, rf_resampler, self.__rotator, self.__mult, self) @exported_value(type=ReferenceT(), changes='never') def get_modulator(self): return self.__modulator @exported_value( type_fn=lambda self: RangeT([(-self.__rf_rate / 2, self.__rf_rate / 2)], unit=units.Hz, strict=False), changes='this_setter', label='Frequency') def get_freq(self): return self.__freq @setter def set_freq(self, value): self.__freq = float(value) self.__rotator.set_phase_inc(rotator_inc(rate=self.__rf_rate, shift=self.__freq)) @exported_value( type=RangeT([(-50.0, 0.0)], unit=units.dB, strict=False), changes='this_setter', label='Gain') def get_gain(self): return to_dB(self.__mult.k().real) @setter def set_gain(self, value): self.__mult.set_k(dB(value))
def __init__(self, initial_state={}, dynamic=False, member_type=ReferenceT()): # pylint: disable=dangerous-default-value self.__member_type = member_type self.__cells = {} self._shape_subscription = lambda: None self._dynamic = True for key in initial_state: self[key] = initial_state[key] self._dynamic = dynamic
class BlockCellSpecimen(ExportedState): """Helper for TestBlockCell""" block = None def __init__(self, block): self.__block = block @exported_value(type=ReferenceT(), changes='explicit') def get_block(self): return self.__block def replace_block(self, block): self.__block = block self.state_changed('block')
class FMAPRSDemodulator(gr.hier_block2, ExportedState): def __init__(self, mode, input_rate=0, context=None): assert input_rate > 0 assert context is not None gr.hier_block2.__init__( self, str(mode) + b' (FM + Multimon-NG) demodulator', gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(1, 1, gr.sizeof_float * 1), ) self.mode = mode self.input_rate = input_rate # FM demod # TODO: Retry telling the NFMDemodulator to have its output rate be pipe_rate instead of using a resampler. Something went wrong when trying that before. Same thing is done in dsd.py self.fm_demod = NFMDemodulator( mode='NFM', input_rate=input_rate, no_audio_filter=True, # don't remove CTCSS tone tau=None) # no deemphasis assert self.fm_demod.get_output_type().get_kind() == 'MONO' fm_audio_rate = self.fm_demod.get_output_type().get_sample_rate() # Subprocess self.mm_demod = APRSDemodulator(context=context) mm_audio_rate = self.mm_demod.get_input_type().get_sample_rate() # Output self.connect( self, self.fm_demod, make_resampler(fm_audio_rate, mm_audio_rate), self.mm_demod, self) def _close(self): # TODO: This never gets called except in tests. Do this better, like by having an explicit life cycle for demodulators. self.mm_demod._close() @exported_value(type=BandShape, changes='never') def get_band_shape(self): return self.fm_demod.get_band_shape() def get_output_type(self): return self.mm_demod.get_output_type() @exported_value(type=ReferenceT(), changes='never') def get_mm_demod(self): return self.mm_demod
class ValueAndBlockSpecimen(ExportedState): """Helper for TestExportedState""" def __init__(self, block=nullExportedState, value=0): self.__value = value self.__block = block @exported_value(type=ReferenceT(), changes='never') def get_block(self): return self.__block @exported_value(type=float, parameter='value', changes='this_setter') def get_value(self): return self.__value @setter def set_value(self, value): self.__value = value
class CellIdentitySpecimen(ExportedState): """Helper for TestCellIdentity""" __value = 1 def __init__(self): self.__block = ExportedState() # force worst-case def state_is_dynamic(self): return True @exported_value(changes='never') def get_value(self): return 9 @exported_value(type=ReferenceT(), changes='never') def get_block(self): return self.__block
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), gr.io_signature(1, 1, gr.sizeof_float * audio_channels)) 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_block = blocks.multiply_const_vff([0.0] * 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: # Construct stereo-to-mono conversion (used at least for level probe) if self.__demod_stereo: splitter = blocks.vector_to_streams(gr.sizeof_float, 2) mono_audio = blocks.multiply_matrix_ff(((0.5, 0.5), )) self.connect(self.__demodulator, splitter) self.connect((splitter, 0), (mono_audio, 0)) self.connect((splitter, 1), (mono_audio, 1)) else: mono_audio = self.__demodulator # Connect mono audio to level probe self.connect(mono_audio, self.probe_audio) # Connect demodulator to output gain control, converting as needed if (self.__audio_channels == 2) == self.__demod_stereo: # stereo to stereo or mono to mono self.connect(self.__demodulator, self.__audio_gain_block) elif self.__audio_channels == 2 and not self.__demod_stereo: # mono to stereo duplicator = blocks.streams_to_vector(gr.sizeof_float, 2) self.connect(self.__demodulator, (duplicator, 0)) self.connect(self.__demodulator, (duplicator, 1)) self.connect(duplicator, self.__audio_gain_block) elif self.__audio_channels == 1 and self.__demod_stereo: # stereo to mono self.connect(mono_audio, self.__audio_gain_block) else: raise Exception('shouldn\'t happen') # Connect gain control to output of receiver self.connect(self.__audio_gain_block, self) else: # Dummy output, ignored by containing block self.connect( blocks.vector_source_f([], vlen=self.__audio_channels), self) 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 self.state_changed('rec_freq') self.state_changed('is_valid') @exported_value(type=ReferenceT(), changes='explicit') def get_demodulator(self): return self.__demodulator @exported_value(type_fn=lambda self: self.context.get_rx_device_type(), changes='this_setter', label='RF source') 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: EnumT({d.mode: d.info for d in get_modes()}), changes='this_setter', label='Mode') 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=QuantityT(units.Hz), parameter='freq_absolute', changes='explicit', label='Frequency') 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) self.state_changed('rec_freq') self.state_changed('is_valid') @exported_value( type=bool, changes='this_setter', label='Follow device', description= 'When this receiver\'s frequency or the device\'s frequency is changed, maintain the relative offset between them.' ) 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=RangeT([(-30, 20)], unit=units.dB, strict=False), changes='this_setter', label='Volume') 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: RangeT( [(-1, 1)] if self.__audio_channels > 1 else [(0, 0)], strict=True), changes='this_setter', label='Pan') 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(), changes='this_setter', label='Audio destination') 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, changes='explicit') 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. # TODO: document what we are using as the reference level. It's not dBFS because we're floating-point and before the gain stage. @exported_value(type=RangeT([(_audio_power_minimum_dB, 0)], unit=units.dB, strict=False), changes='continuous', label='Audio power') def get_audio_power(self): if self.get_is_valid(): return to_dB( 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 showing that revalidate is needed and works self.context.revalidate(tuning=False) # in case our bandwidth changed self.state_changed('is_valid') 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 self.state_changed('demodulator') # 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_block = blocks.multiply_const_vff( [0.0] * 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: Instead of left-to-left and right-to-right, panning other than center should mix left and right content. (A "pan law" defines the proper mix.) This implies a matrix multiplication type operation. self.__audio_gain_block.set_k([ gain_lin * (1 - pan), gain_lin * (1 + pan), ]) else: self.__audio_gain_block.set_k([gain_lin])
class Device(ExportedState): """ A Device aggregates the functions of one or more pieces of radio hardware or drivers for same; particularly: * receiver * transmitter (not yet implemented) * VFO For example, if one is using a sound card-based transceiver, then there would be an audio-source, an audio-sink, and a separate interface to the VFO and other hardware controls. These are completely unrelated as far as the operating system and GNU Radio are concerned, but the Device object aggregates all of those so that the user interface can display them as properly related and control them in sync. """ # pylint: disable=no-member # (confused by nullExportedState) def __init__(self, name=None, rx_driver=nullExportedState, tx_driver=nullExportedState, vfo_cell=None, components={}): # pylint: disable=dangerous-default-value """ rx_driver -- may be nullExportedState tx_driver -- may be nullExportedState vfo_cell -- may be None """ if vfo_cell is None: vfo_cell = _stub_vfo assert isinstance(vfo_cell.type(), RangeT) # TODO: Consider using an unconditional wrapper around the VFO cell which sets the cell metadata consistently. self.__name = name self.__vfo_cell = vfo_cell self.rx_driver = IRXDriver(rx_driver) if rx_driver is not nullExportedState else nullExportedState self.tx_driver = ITXDriver(tx_driver) if tx_driver is not nullExportedState else nullExportedState coerced_components = {} for key, component in components.iteritems(): coerced_components[key] = IComponent(component) self.__components = CellDict(initial_state=coerced_components) self.__components_state = CollectionState(self.__components) self.__transmitting = False def get_name(self): return self.__name def state_def(self): for d in super(Device, self).state_def(): yield d yield 'freq', self.__vfo_cell def can_receive(self): return self.rx_driver is not nullExportedState def can_transmit(self): return self.tx_driver is not nullExportedState def can_tune(self): return self.__vfo_cell is not _stub_vfo @exported_value(type=ReferenceT(), changes='never') def get_rx_driver(self): return self.rx_driver @exported_value(type=ReferenceT(), changes='never') def get_tx_driver(self): return self.tx_driver @exported_value(type=ReferenceT(), changes='never') def get_components(self): return self.__components_state def get_vfo_cell(self): return self.__vfo_cell def get_components_dict(self): """Do not mutate the dictionary returned.""" return self.__components def get_freq(self): """ Get the frequency from the VFO cell. (Convenience/consistency equivalent to self.state()['freq'].get.) """ return self.__vfo_cell.get() def set_freq(self, value): """ Set the frequency in the VFO cell. (Convenience/consistency equivalent to self.state()['freq'].set.) """ return self.__vfo_cell.set(value) def set_transmitting(self, value, midpoint_hook=lambda: None): """ Start or stop transmitting. This may involve flowgraph reconfiguration, and as such the caller is responsible for locking or stopping the flowgraph(s) around this call. If there is no TX driver, then this has no effect. The output of the RX driver while transmitting is undefined; it may produce no samples, produce meaningless samples at the normal rate, or be unaffected (full duplex). """ value = bool(value) if not self.can_transmit() or value == self.__transmitting: midpoint_hook() return self.__transmitting = value self.tx_driver.set_transmitting(value, midpoint_hook) def close(self): """ Instruct the drivers to perform a clean shutdown, and discard them. """ if self.rx_driver is not nullExportedState: self.rx_driver.close() self.rx_driver = nullExportedState if self.tx_driver is not nullExportedState: self.tx_driver.close() self.tx_driver = nullExportedState for key, component in self.__components.iteritems(): component.close() self.__components[key] = nullExportedState def notify_reconnecting_or_restarting(self): if self.rx_driver is not nullExportedState: self.rx_driver.notify_reconnecting_or_restarting() if self.tx_driver is not nullExportedState: self.tx_driver.notify_reconnecting_or_restarting()
class _ElecraftRadio(ExportedState): # TODO: Tell protocol to do no/less polling when nobody is looking. def __init__(self, protocol): self.__protocol = protocol self.__rx_main = _ElecraftReceiver(protocol, False) self.__rx_sub = _ElecraftReceiver(protocol, True) self.__init_center_cell() def __init_center_cell(self): base_freq_cell = self.__rx_main.state()[_FREQ_CELL_KEY] mode_cell = self.__rx_main.state()['MD'] sidetone_cell = self.state()['CW'] submode_cell = self.state()['DT'] iq_offset_cell = LooseCell(value=0.0, type=float, writable=True) self.__iq_center_cell = ViewCell( base=base_freq_cell, get_transform=lambda x: x + iq_offset_cell.get(), set_transform=lambda x: x - iq_offset_cell.get(), type=base_freq_cell.type(), # runtime variable... writable=True, persists=base_freq_cell.metadata().persists) def changed_iq(_value=None): # TODO this is KX3-specific mode = mode_cell.get() if mode == 'CW': iq_offset = sidetone_cell.get() elif mode == 'CW-REV': iq_offset = -sidetone_cell.get() elif mode == 'AM' or mode == 'FM': iq_offset = 11000.0 elif mode == 'DATA' or mode == 'DATA-REV': submode = submode_cell.get() if submode == 0: # "DATA A", SSB with less processing iq_offset = 0.0 # ??? elif submode == 1: # "AFSK A", SSB with RTTY style filter iq_offset = 0.0 # ??? elif submode == 2: # "FSK D", RTTY iq_offset = 900.0 elif submode == 3: # "PSK D", PSK31 iq_offset = 1000.0 # I think so... else: iq_offset = 0 # fallback if mode == 'DATA-REV': iq_offset = -iq_offset else: # USB, LSB, other iq_offset = 0.0 iq_offset_cell.set(iq_offset) self.__iq_center_cell.changed_transform() # TODO bad practice mode_cell._subscribe_immediate(changed_iq) sidetone_cell._subscribe_immediate(changed_iq) submode_cell._subscribe_immediate(changed_iq) changed_iq() def state_def(self): """overrides ExportedState""" for d in super(_ElecraftRadio, self).state_def(): yield d for d in _st.install_cells(self, self.__protocol, is_sub=None): yield d def close(self): """implements IComponent""" self.__protocol.transport.loseConnection() def iq_center_cell(self): """Made available for Device creation; not a well-thought-out interface.""" return self.__iq_center_cell def get_direct_protocol(self): """For experimental use only.""" return self.__protocol @exported_value(type=NoticeT(always_visible=False), changes='continuous') # TODO better changes def get_errors(self): error = self.__protocol.get_communication_error() if not error: return u'' elif error == u'not_responding': return u'Radio not responding.' elif error == u'bad_data': return u'Bad data from radio.' else: return six.text_type(error) @exported_value(type=ReferenceT(), changes='never') def get_rx_main(self): return self.__rx_main @exported_value(type=ReferenceT(), changes='never') def get_rx_sub(self): return self.__rx_sub
class Top(gr.top_block, ExportedState, RecursiveLockBlockMixin): def __init__(self, devices={}, audio_config=None, features=_STUB_FEATURES): # pylint: disable=dangerous-default-value if len(devices) <= 0: raise ValueError('Must have at least one RF device') gr.top_block.__init__(self, type(self).__name__) self.__running = False # duplicate of GR state we can't reach, see __start_or_stop self.__has_a_useful_receiver = False # Configuration # TODO: device refactoring: Remove vestigial 'accessories' self._sources = CellDict({k: d for k, d in devices.iteritems() if d.can_receive()}) self._accessories = accessories = {k: d for k, d in devices.iteritems() if not d.can_receive()} for key in self._sources: # arbitrary valid initial value self.source_name = key break self.__rx_device_type = EnumT({k: v.get_name() or k for (k, v) in self._sources.iteritems()}) # Audio early setup self.__audio_manager = AudioManager( # must be before contexts graph=self, audio_config=audio_config, stereo=features['stereo']) # Blocks etc. # TODO: device refactoring: remove 'source' concept (which is currently a device) # TODO: remove legacy no-underscore names, maybe get rid of self.source self.source = None self.__monitor_rx_driver = None self.monitor = MonitorSink( signal_type=SignalType(sample_rate=10000, kind='IQ'), # dummy value will be updated in _do_connect context=Context(self)) self.monitor.get_interested_cell().subscribe2(self.__start_or_stop_later, the_subscription_context) self.__clip_probe = MaxProbe() # Receiver blocks (multiple, eventually) self._receivers = CellDict(dynamic=True) self._receiver_valid = {} # collections # TODO: No longer necessary to have these non-underscore names self.sources = CollectionState(CellDict(self._sources)) self.receivers = ReceiverCollection(self._receivers, self) self.accessories = CollectionState(CellDict(accessories)) self.__telemetry_store = TelemetryStore() # Flags, other state self.__needs_reconnect = [u'initialization'] self.__in_reconnect = False self.receiver_key_counter = 0 self.receiver_default_state = {} # Initialization def hookup_vfo_callback(k, d): # function so as to not close over loop variable d.get_vfo_cell().subscribe2(lambda value: self.__device_vfo_callback(k), the_subscription_context) for k, d in devices.iteritems(): hookup_vfo_callback(k, d) self._do_connect() def state_def(self): for d in super(Top, self).state_def(): yield d yield 'clip_warning', self.__clip_probe.state()['clip_warning'] def add_receiver(self, mode, key=None, state=None): if len(self._receivers) >= 100: # Prevent storage-usage DoS attack raise Exception('Refusing to create more than 100 receivers') if key is not None: assert key not in self._receivers else: while True: key = base26(self.receiver_key_counter) self.receiver_key_counter += 1 if key not in self._receivers: break if len(self._receivers) > 0: arbitrary = self._receivers.itervalues().next() defaults = arbitrary.state_to_json() else: defaults = self.receiver_default_state combined_state = defaults.copy() for do_not_use_default in ['device_name', 'freq_linked_to_device']: if do_not_use_default in combined_state: del combined_state[do_not_use_default] if state is not None: combined_state.update(state) facet = ContextForReceiver(self, key) receiver = unserialize_exported_state(Receiver, kwargs=dict( mode=mode, audio_channels=self.__audio_manager.get_channels(), device_name=self.source_name, audio_destination=self.__audio_manager.get_default_destination(), # TODO match others context=facet, ), state=combined_state) facet._receiver = receiver self._receivers[key] = receiver self._receiver_valid[key] = False self.__needs_reconnect.append(u'added receiver ' + key) self._do_connect() # until _enabled, the facet ignores any reconnect/rebuild-triggering callbacks facet._enabled = True return (key, receiver) def delete_receiver(self, key): assert key in self._receivers receiver = self._receivers[key] # save defaults for use if about to become empty if len(self._receivers) == 1: self.receiver_default_state = receiver.state_to_json() del self._receivers[key] del self._receiver_valid[key] self.__needs_reconnect.append(u'removed receiver ' + key) self._do_connect() # TODO move these methods to a facet of AudioManager def add_audio_queue(self, queue, queue_rate): self.__audio_manager.add_audio_queue(queue, queue_rate) self.__needs_reconnect.append(u'added audio queue') self._do_connect() self.__start_or_stop() def remove_audio_queue(self, queue): self.__audio_manager.remove_audio_queue(queue) self.__start_or_stop() self.__needs_reconnect.append(u'removed audio queue') self._do_connect() def get_audio_queue_channels(self): """ Return the number of channels (which will be 1 or 2) in audio queue outputs. """ return self.__audio_manager.get_channels() def _do_connect(self): """Do all reconfiguration operations in the proper order.""" if self.__in_reconnect: raise Exception('reentrant reconnect or _do_connect crashed') self.__in_reconnect = True t0 = time.time() if self.source is not self._sources[self.source_name]: log.msg('Flow graph: Switching RF device to %s' % (self.source_name)) self.__needs_reconnect.append(u'switched device') this_source = self._sources[self.source_name] self.source = this_source self.state_changed('source') self.__monitor_rx_driver = this_source.get_rx_driver() monitor_signal_type = self.__monitor_rx_driver.get_output_type() self.monitor.set_signal_type(monitor_signal_type) self.monitor.set_input_center_freq(this_source.get_freq()) self.__clip_probe.set_window_and_reconnect(0.5 * monitor_signal_type.get_sample_rate()) if self.__needs_reconnect: log.msg(u'Flow graph: Rebuilding connections because: %s' % (', '.join(self.__needs_reconnect),)) self.__needs_reconnect = [] self._recursive_lock() self.disconnect_all() self.connect( self.__monitor_rx_driver, self.monitor) self.connect( self.__monitor_rx_driver, self.__clip_probe) # Filter receivers audio_rs = self.__audio_manager.reconnecting() n_valid_receivers = 0 has_non_audio_receiver = False for key, receiver in self._receivers.iteritems(): self._receiver_valid[key] = receiver.get_is_valid() if not self._receiver_valid[key]: continue if not self.__audio_manager.validate_destination(receiver.get_audio_destination()): log.err('Flow graph: receiver audio destination %r is not available' % (receiver.get_audio_destination(),)) continue n_valid_receivers += 1 if n_valid_receivers > 6: # Sanity-check to avoid burning arbitrary resources # TODO: less arbitrary constant; communicate this restriction to client log.err('Flow graph: Refusing to connect more than 6 receivers') break self.connect(self._sources[receiver.get_device_name()].get_rx_driver(), receiver) receiver_output_type = receiver.get_output_type() if receiver_output_type.get_sample_rate() <= 0: # Demodulator has no output, but receiver has a dummy output, so connect it to something to satisfy flow graph structure. self.connect(receiver, blocks.null_sink(gr.sizeof_float * self.__audio_manager.get_channels())) # Note that we have a non-audio receiver which may be useful even if there is no audio output has_non_audio_receiver = True else: assert receiver_output_type.get_kind() == 'STEREO' audio_rs.input(receiver, receiver_output_type.get_sample_rate(), receiver.get_audio_destination()) self.__has_a_useful_receiver = audio_rs.finish_bus_connections() or \ has_non_audio_receiver self._recursive_unlock() # (this is in an if block but it can't not execute if anything else did) log.msg('Flow graph: ...done reconnecting (%i ms).' % ((time.time() - t0) * 1000,)) self.__start_or_stop_later() self.__in_reconnect = False def __device_vfo_callback(self, device_key): reactor.callLater( self._sources[device_key].get_rx_driver().get_tune_delay(), self.__device_vfo_changed, device_key) def __device_vfo_changed(self, device_key): device = self._sources[device_key] freq = device.get_freq() if self.source is device: self.monitor.set_input_center_freq(freq) for rec_key, receiver in self._receivers.iteritems(): if receiver.get_device_name() == device_key: receiver.changed_device_freq() self._update_receiver_validity(rec_key) # TODO: If multiple receivers change validity we'll do redundant reconnects in this loop; avoid that. def _update_receiver_validity(self, key): receiver = self._receivers[key] if receiver.get_is_valid() != self._receiver_valid[key]: self.__needs_reconnect.append(u'receiver %s validity changed' % (key,)) self._do_connect() @exported_value(type=ReferenceT(), changes='never') def get_monitor(self): return self.monitor @exported_value(type=ReferenceT(), persists=False, changes='never') def get_sources(self): return self.sources @exported_value(type=ReferenceT(), persists=False, changes='explicit') def get_source(self): return self.source # TODO no need for this now...? @exported_value(type=ReferenceT(), changes='never') def get_receivers(self): return self.receivers # TODO the concept of 'accessories' is old and needs to go away, but we don't have a flexible enough UI to replace it with just devices since only one device can be looked-at at a time so far. @exported_value(type=ReferenceT(), persists=False, changes='never') def get_accessories(self): return self.accessories @exported_value(type=ReferenceT(), changes='never', label='Telemetry') def get_telemetry_store(self): return self.__telemetry_store def start(self, **kwargs): # pylint: disable=arguments-differ # trigger reconnect/restart notification self._recursive_lock() self._recursive_unlock() super(Top, self).start(**kwargs) self.__running = True def stop(self): super(Top, self).stop() self.__running = False def __start_or_stop(self): # TODO: Improve start/stop conditions: # # * run if a client is watching an audio-having receiver's cell-based outputs (e.g. VOR) but not listening to audio # # * don't run if no client is watching a pure telemetry receiver # (maybe a user preference since having a history when you connect is useful) # # Both of these refinements require becoming aware of cell subscriptions. should_run = ( self.__has_a_useful_receiver or self.monitor.get_interested_cell().get()) if should_run != self.__running: if should_run: self.start() else: self.stop() self.wait() def __start_or_stop_later(self, unused_subscription_value=None): reactor.callLater(0, self.__start_or_stop) def close_all_devices(self): """Close all devices in preparation for a clean shutdown. Makes this top block unusable""" for device in self._sources.itervalues(): device.close() for device in self._accessories.itervalues(): device.close() self.stop() self.wait() @exported_value( type_fn=lambda self: self.__rx_device_type, changes='this_setter', label='RF source') def get_source_name(self): return self.source_name @setter def set_source_name(self, value): if value == self.source_name: return if value not in self._sources: raise ValueError('Source %r does not exist' % (value,)) self.source_name = value self._do_connect() def _get_rx_device_type(self): """for ContextForReceiver only""" return self.__rx_device_type def _get_audio_destination_type(self): """for ContextForReceiver only""" return self.__audio_manager.get_destination_type() def _trigger_reconnect(self, reason): self.__needs_reconnect.append(reason) self._do_connect() def _recursive_lock_hook(self): for source in self._sources.itervalues(): source.notify_reconnecting_or_restarting()
class DSDDemodulator(gr.hier_block2, ExportedState): def __init__(self, mode, input_rate=0, uvquality=3, context=None): assert input_rate > 0 gr.hier_block2.__init__(self, type(self).__name__, gr.io_signature(1, 1, gr.sizeof_gr_complex), gr.io_signature(1, 1, gr.sizeof_float)) self.__context = context self.__output_type = SignalType(kind='MONO', sample_rate=8000) self.__uvquality = uvquality # TODO: Retry telling the NFMDemodulator to have its output rate be _demod_rate instead of using a resampler. Something went wrong when trying that before. Same thing is done in multimon.py self.__fm_demod = NFMDemodulator( mode='NFM', input_rate=input_rate, no_audio_filter=True, # don't remove CTCSS tone tau=None) # no deemphasis assert self.__fm_demod.get_output_type().get_kind() == 'MONO' fm_audio_rate = self.__fm_demod.get_output_type().get_sample_rate() self.__resampler = make_resampler(fm_audio_rate, _demod_rate) self.__do_connect() def __do_connect(self): self.__context.lock() try: self.disconnect_all() if _available_version == 1: # backwards compatibility decoder = dsd_block_ff() else: decoder = dsd_block_ff( # TODO: Add controls to choose frame and mod options at runtime — need to be able to get the enum info from gr-dsd, which may not even be currently available. frame=dsd.dsd_FRAME_AUTO_DETECT, mod=dsd.dsd_MOD_AUTO_SELECT, uvquality=self.__uvquality, errorbars=_debug_print, verbosity=2 if _debug_print else 0) self.connect(self, self.__fm_demod, self.__resampler, decoder, self) finally: self.__context.unlock() @exported_value(type=ReferenceT(), changes='never') def get_fm_demod(self): return self.__fm_demod if _available_version >= 2: @exported_value(type=_uvquality_range, changes='this_setter', label='Unvoiced speech quality', parameter='uvquality') def get_uvquality(self): return self.__uvquality @setter def set_uvquality(self, value): value = _uvquality_range(value) if self.__uvquality != value: self.__uvquality = value self.__do_connect() def get_output_type(self): return self.__output_type @exported_value(type=BandShape, changes='never') def get_band_shape(self): return self.__fm_demod.get_band_shape()
class _SimulatedRXDriver(ExportedState, gr.hier_block2): # TODO: be not hardcoded; for now this is convenient audio_rate = 1e4 rf_rate = 200e3 def __init__(self, name): gr.hier_block2.__init__( self, name, gr.io_signature(0, 0, 0), gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), ) rf_rate = self.rf_rate audio_rate = self.audio_rate self.__noise_level = -22 self.__transmitters = CellDict(dynamic=True) self.__transmitters_cs = CollectionState(self.__transmitters) self.__bus = blocks.add_vcc(1) self.__channel_model = channels.channel_model( noise_voltage=dB(self.__noise_level), frequency_offset=0, epsilon=1.01, # TODO: expose this parameter # taps=..., # TODO: apply something here? ) self.__rotator = blocks.rotator_cc() self.__throttle = blocks.throttle(gr.sizeof_gr_complex, rf_rate) self.connect(self.__bus, self.__throttle, self.__channel_model, self.__rotator, self) signals = [] def add_modulator(freq, key, mode_or_modulator_ctor, **kwargs): if isinstance(mode_or_modulator_ctor, type): mode = None ctor = mode_or_modulator_ctor else: mode = mode_or_modulator_ctor mode_def = lookup_mode(mode) if mode_def is None: # missing plugin, say return ctor = mode_def.mod_class context = None # TODO implement context modulator = ctor(context=context, mode=mode, **kwargs) tx = _SimulatedTransmitter(modulator, audio_rate, rf_rate, freq) self.connect(audio_signal, tx) signals.append(tx) self.__transmitters[key] = tx # Audio input signal pitch = analog.sig_source_f(audio_rate, analog.GR_SAW_WAVE, -1, 2000, 1000) audio_signal = vco = blocks.vco_f(audio_rate, 1, 1) self.connect(pitch, vco) # Channels add_modulator(0.0, 'usb', 'USB') add_modulator(10e3, 'am', 'AM') add_modulator(30e3, 'fm', 'NFM') add_modulator(-30e3, 'vor1', 'VOR', angle=0) add_modulator(-60e3, 'vor2', 'VOR', angle=math.pi / 2) add_modulator( 50e3, 'rtty', 'RTTY', message='The quick brown fox jumped over the lazy dog.\n') add_modulator(80e3, 'chirp', ChirpModulator) bus_input = 0 for signal in signals: self.connect(signal, (self.__bus, bus_input)) bus_input = bus_input + 1 self.__signal_type = SignalType(kind='IQ', sample_rate=rf_rate) self.__usable_bandwidth = RangeT([(-rf_rate / 2, rf_rate / 2)]) @exported_value(type=ReferenceT(), changes='never') def get_transmitters(self): return self.__transmitters_cs # implement IRXDriver @exported_value(type=SignalType, changes='never') def get_output_type(self): return self.__signal_type def _set_sim_freq(self, freq): self.__rotator.set_phase_inc( rotator_inc(rate=self.rf_rate, shift=-freq)) # implement IRXDriver def get_tune_delay(self): return 0.0 # implement IRXDriver def get_usable_bandwidth(self): return self.__usable_bandwidth # implement IRXDriver def close(self): pass @exported_value(type=RangeT([(-50, 0)]), changes='this_setter', label='White noise') def get_noise_level(self): return self.__noise_level @setter def set_noise_level(self, value): self.__channel_model.set_noise_voltage(dB(value)) self.__noise_level = value def notify_reconnecting_or_restarting(self): # The throttle block runs on a clock which does not stop when the flowgraph stops; resetting the sample rate restarts the clock. # The necessity of this kludge has been filed as a gnuradio bug at <http://gnuradio.org/redmine/issues/649> self.__throttle.set_sample_rate(self.__throttle.sample_rate())
class _OsmoSDRRXDriver(ExportedState, gr.hier_block2): # Note: Docs for gr-osmosdr are in comments at gr-osmosdr/lib/source_iface.h def __init__(self, osmo_device, source, profile, name, tuning): gr.hier_block2.__init__( self, b'RX ' + str(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 = EnumT({unicode(name): unicode(name) for name in self.__source.get_antennas()}, strict=True) self.connect(self.__source, self) self.__gains = Gains(source, self) # State of the source that there are no getters for, so we must keep our own copy of self.__track_dc_offset_mode = DCOffsetOff self.__track_iq_balance_mode = IQBalanceOff source.set_dc_offset_mode(self.__track_dc_offset_mode, ch) source.set_iq_balance_mode(self.__track_iq_balance_mode, ch) # 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) @exported_value(type=SignalType, changes='never') def get_output_type(self): return self.__signal_type # implement IRXDriver def get_tune_delay(self): return self.__profile.tune_delay # implement IRXDriver def get_usable_bandwidth(self): return self.__usable_bandwidth # implement IRXDriver def close(self): self._stop_rx() self.__tuning = None @exported_value( type=QuantityT(unit=units.ppm), changes='this_setter', label='Freq.corr.') 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(type=ReferenceT(), changes='never') def get_gains(self): return self.__gains @exported_value( type_fn=lambda self: convert_osmosdr_range( self.__source.get_gain_range(ch), unit=units.dB, strict=False), changes='this_setter', label='Gain') def get_gain(self): if self.__source is None: return 0.0 return self.__source.get_gain(ch) @setter def set_gain(self, value): self.__source.set_gain(float(value), ch) # The single gain and individual-stage gain controls have an unspecified relationship to each other. Thus, changing one must poll the other. self.__gains.state_changed() @exported_value( type_fn=lambda self: bool if self.__profile.agc else ConstantT(False), changes='this_setter', label='AGC on') def get_agc(self): if self.__source is None: return False return bool(self.__source.get_gain_mode(ch)) @setter def set_agc(self, value): self.__source.set_gain_mode(bool(value), ch) @exported_value( type_fn=lambda self: self.__antenna_type, changes='this_setter', label='Antenna') def get_antenna(self): if self.__source is None: return '' return unicode(self.__source.get_antenna(ch)) @setter def set_antenna(self, value): # TODO we should have a provision for restricting antenna selection when transmit is possible to avoid hardware damage self.__source.set_antenna(str(self.__antenna_type(value)), ch) # Note: dc_offset_mode has a 'manual' mode we are not yet exposing, which is why the internal tracking is an enum integer but the exported value is a boolean @exported_value( type_fn=lambda self: bool if self.__profile.dc_cancel else ConstantT(False), changes='this_setter', label='Use DC cancellation') def get_dc_cancel(self): return bool(self.__track_dc_offset_mode) @setter def set_dc_cancel(self, value): if value: mode = DCOffsetAutomatic else: mode = DCOffsetOff self.__source.set_dc_offset_mode(mode, ch) self.__track_dc_offset_mode = mode # Note: iq_balance_mode has a 'manual' mode we are not yet exposing, which is why the internal tracking is an enum integer but the exported value is a boolean @exported_value(type=bool, # TODO: detect gr-iqbal changes='this_setter', label='Use IQ balancer') def get_iq_balance(self): return bool(self.__track_iq_balance_mode) @setter def set_iq_balance(self, value): if value: mode = IQBalanceAutomatic else: mode = IQBalanceOff self.__source.set_iq_balance_mode(mode, ch) self.__track_iq_balance_mode = mode # 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( type_fn=lambda self: convert_osmosdr_range( self.__source.get_bandwidth_range(ch), unit=units.Hz, add_zero=True), changes='this_setter', label='Analog bandwidth', description='Bandwidth of the analog antialiasing filter.') def get_bandwidth(self): if self.__source is None: return 0.0 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 # link to tx driver def _stop_rx(self): self.disconnect_all() self.__state_while_inactive = self.state_to_json() self.__tuning.set_block(None) self.__gains.close() self.__source = None self.connect(self.__placeholder, self) # link to tx driver def _start_rx(self): self.disconnect_all() self.__source = osmosdr.source('numchan=1 ' + self.__osmo_device) self.__source.set_sample_rate(self.__signal_type.get_sample_rate()) self.__tuning.set_block(self.__source) self.__gains = Gains(self.__source, self) self.connect(self.__source, self) self.state_from_json(self.__state_while_inactive)
class RTTYDemodulator(gr.hier_block2, ExportedState): implements(IDemodulator) __filter_low = 1500 __filter_high = 2500 __transition = 100 def __init__(self, mode, input_rate=0, context=None): assert input_rate > 0 gr.hier_block2.__init__( self, 'RTTY demodulator', gr.io_signature(1, 1, gr.sizeof_gr_complex * 1), gr.io_signature(1, 1, gr.sizeof_float * 1), ) self.__text = u'' baud = _DEFAULT_BAUD # TODO param self.baud = baud demod_rate = 6000 # TODO optimize this value self.samp_rate = demod_rate # TODO rename self.__channel_filter = MultistageChannelFilter( input_rate=input_rate, output_rate=demod_rate, cutoff_freq=self.__filter_high, transition_width=self.__transition) # TODO optimize filter band self.__sharp_filter = grfilter.fir_filter_ccc( 1, firdes.complex_band_pass(1.0, demod_rate, self.__filter_low, self.__filter_high, self.__transition, firdes.WIN_HAMMING)) self.fsk_demod = RTTYFSKDemodulator(input_rate=demod_rate, baud=baud) self.__real = blocks.complex_to_real(vlen=1) self.__char_queue = gr.msg_queue(limit=100) self.char_sink = blocks.message_sink(gr.sizeof_char, self.__char_queue, True) self.connect( self, self.__channel_filter, self.__sharp_filter, self.fsk_demod, rtty.rtty_decode_ff(rate=demod_rate, baud=baud, polarity=False), self.char_sink) self.connect( self.__sharp_filter, self.__real, self) def can_set_mode(self, mode): """implement IDemodulator""" return False @exported_value(changes='never') def get_band_filter_shape(self): """implement IDemodulator""" return { 'low': self.__filter_low, 'high': self.__filter_high, 'width': self.__transition } def get_output_type(self): """implement IDemodulator""" return SignalType(kind='MONO', sample_rate=self.samp_rate) @exported_value(type=ReferenceT(), changes='never') def get_fsk_demod(self): return self.fsk_demod @exported_value(type=unicode, changes='continuous') def get_text(self): # pylint: disable=no-member queue = self.__char_queue # we would use .delete_head_nowait() but it returns a crashy wrapper instead of a sensible value like None. So implement a test (which is safe as long as we're the only reader) if not queue.empty_p(): message = queue.delete_head() if message.length() > 0: bitstring = message.to_string() else: bitstring = '' # avoid crash bug textstring = self.__text textstring += bitstring self.__text = textstring[-20:] return self.__text