class SpecAController(QtCore.QObject): """ The controller for the rtsa-gui. Issues commands to device, stores and broadcasts changes to GUI state. """ _dut = None _sweep_device = None _capture_device = None _plot_state = None _state = None _recording_file = None _playback_file = None _playback_sweep_data = None _user_xrange_control_enabled = True _pending_user_xrange = None _applying_user_xrange = False device_change = QtCore.Signal(object) state_change = QtCore.Signal(SpecAState, list) capture_receive = QtCore.Signal(SpecAState, float, float, object, object, object, object) options_change = QtCore.Signal(dict, list) def __init__(self): super(SpecAController, self).__init__() self._dsp_options = {} self._options = {} def set_device(self, dut=None, playback_filename=None): """ Detach any currenly attached device and stop playback then optionally attach to a new device or playback file. :param dut: a :class:`pyrf.thinkrf.WSA` or None :param playback_filename: recorded VRT data filename or None :param playback_scheduler: function to schedule each playback capture """ if self._playback_file: self._playback_file.close() self._playback_file = None if self._dut: self._dut.disconnect() if playback_filename: self._playback_file = open(playback_filename, 'rb') self._playback_started = False self._playback_context = {} vrt_packet = self._playback_vrt(auto_rewind=False) state_json = vrt_packet.fields['speca'] # support old playback files if state_json['device_identifier'] == 'unknown': state_json['device_identifier'] = 'ThinkRF,WSA5000 v3,,' dut = Playback(state_json['device_class'], state_json['device_identifier']) self._sweep_device = SweepDevice(dut) self._capture_device = CaptureDevice(dut) elif dut: dut.reset() self._sweep_device = SweepDevice(dut, self.process_sweep) self._capture_device = CaptureDevice(dut, self.process_capture) state_json = dict( dut.properties.SPECA_DEFAULTS, device_identifier=dut.device_id) self._dut = dut if not dut: return self.device_change.emit(dut) self._apply_complete_settings(state_json, bool(self._playback_file)) self.start_capture() def start_recording(self, filename=None): """ Start a new recording with filename, or autogenerated filename. Stops previous recording if one in progress. Does nothing if we're currently playing a recording. """ if self._playback_file: return self.stop_recording() if not filename: names = glob.glob('recording-*.vrt') last_index = -1 for n in names: try: last_index = max(last_index, int(n[10:-4])) except ValueError: pass filename = 'recording-%04d.vrt' % (last_index + 1) self._recording_file = open(filename, 'wb') self._dut.set_recording_output(self._recording_file) self._dut.inject_recording_state(self._state.to_json_object()) def stop_recording(self): """ Stop recording or do nothing if not currently recording. """ if not self._recording_file: return self._dut.set_recording_output(None) self._recording_file.close() self._recording_file = None def _apply_pending_user_xrange(self): if self._pending_user_xrange: self._applying_user_xrange = True start, stop = self._pending_user_xrange self._pending_user_xrange = None self.apply_settings( center=int((start + stop) / 2.0 / self._dut.properties.TUNING_RESOLUTION) * self._dut.properties.TUNING_RESOLUTION, span=stop - start) else: self._applying_user_xrange = False def read_block(self): self._apply_pending_user_xrange() device_set = dict(self._state.device_settings) device_set['decimation'] = self._state.decimation device_set['fshift'] = self._state.fshift device_set['rfe_mode'] = self._state.rfe_mode() device_set['freq'] = self._state.center self._capture_device.configure_device(device_set) self._capture_device.capture_time_domain( self._state.mode, self._state.center, self._state.rbw) def read_sweep(self): self._apply_pending_user_xrange() device_set = dict(self._state.device_settings) device_set.pop('pll_reference') device_set.pop('iq_output_path') device_set.pop('trigger') self._sweep_device.capture_power_spectrum( self._state.center - self._state.span / 2.0, self._state.center + self._state.span / 2.0, self._state.rbw, device_set, mode=self._state.rfe_mode()) def start_capture(self): if self._playback_file: self.schedule_playback() elif self._state.sweeping(): self.read_sweep() else: self.read_block() def schedule_playback(self): if not self._playback_started: QtCore.QTimer.singleShot(0, self._playback_step) self._playback_started = True def _playback_step(self): if not self._playback_file: self._playback_started = False return QtCore.QTimer.singleShot(PLAYBACK_STEP_MSEC, self._playback_step) while True: pkt = self._playback_vrt() if pkt.is_context_packet(): if 'speca' in pkt.fields: self._playback_sweep_data = None state_json = pkt.fields['speca'] self._apply_complete_settings(state_json, playback=True) else: self._playback_context.update(pkt.fields) continue if self._state.sweeping(): if not self._playback_sweep_step(pkt): continue return break usable_bins = compute_usable_bins( self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, self._state.fshift) usable_bins, fstart, fstop = adjust_usable_fstart_fstop( self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, self._state.center, pkt.spec_inv, usable_bins) pow_data = compute_fft( self._dut, pkt, self._playback_context, **self._dsp_options) if not self._options.get('show_attenuated_edges'): pow_data, usable_bins, fstart, fstop = ( trim_to_usable_fstart_fstop( pow_data, usable_bins, fstart, fstop)) self.capture_receive.emit( self._state, fstart, fstop, pkt, pow_data, usable_bins, None) def _playback_sweep_start(self): """ ready a new playback sweep data array """ nans = np.ones(int(self._state.span / self._state.rbw)) * np.nan self._playback_sweep_data = nans def _playback_sweep_step(self, pkt): """ process one data packet from a recorded sweep and plot collected data after receiving complete sweep. returns True if data was plotted on this step. """ if self._playback_sweep_data is None: self._playback_sweep_start() last_center = None else: last_center = self._playback_sweep_last_center sweep_start = float(self._state.center - self._state.span / 2) sweep_stop = float(self._state.center + self._state.span / 2) step_center = self._playback_context['rffreq'] updated_plot = False if last_center is not None and last_center >= step_center: # starting a new sweep, plot the data we have self.capture_receive.emit( self._state, sweep_start, sweep_stop, None, self._playback_sweep_data, None, None) updated_plot = True self._playback_sweep_start() self._playback_sweep_last_center = step_center usable_bins = compute_usable_bins( self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, self._state.fshift) usable_bins, fstart, fstop = adjust_usable_fstart_fstop( self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, step_center, pkt.spec_inv, usable_bins) pow_data = compute_fft( self._dut, pkt, self._playback_context, **self._dsp_options) pow_data, usable_bins, fstart, fstop = ( trim_to_usable_fstart_fstop( pow_data, usable_bins, fstart, fstop)) clip_left = max(sweep_start, fstart) clip_right = min(sweep_stop, fstop) sweep_points = len(self._playback_sweep_data) point_left = int((clip_left - sweep_start) * sweep_points / ( sweep_stop - sweep_start)) point_right = int((clip_right - sweep_start) * sweep_points / ( sweep_stop - sweep_start)) xvalues = np.linspace(clip_left, clip_right, point_right - point_left) if point_left >= point_right: logger.info('received sweep step outside sweep: %r, %r' % ((fstart, fstop), (sweep_start, sweep_stop))) else: self._playback_sweep_data[point_left:point_right] = np.interp( xvalues, np.linspace(fstart, fstop, len(pow_data)), pow_data) return updated_plot def _playback_vrt(self, auto_rewind=True): """ Return the next VRT packet in the playback file """ reader = vrt_packet_reader(self._playback_file.read) data = None try: while True: data = reader.send(data) except StopIteration: pass except ValueError: return None if data == '' and auto_rewind: self._playback_file.seek(0) data = self._playback_vrt(auto_rewind=False) return None if data == '' else data def process_capture(self, fstart, fstop, data): # store usable bins before next call to capture_time_domain usable_bins = list(self._capture_device.usable_bins) # only read data if WSA digitizer is used if 'DIGITIZER' in self._state.device_settings['iq_output_path']: if self._state.sweeping(): self.read_sweep() return self.read_block() if 'reflevel' in data['context_pkt']: self._ref_level = data['context_pkt']['reflevel'] pow_data = compute_fft( self._dut, data['data_pkt'], data['context_pkt'], ref=self._ref_level, **self._dsp_options) if not self._options.get('show_attenuated_edges'): pow_data, usable_bins, fstart, fstop = ( trim_to_usable_fstart_fstop( pow_data, usable_bins, fstart, fstop)) self.capture_receive.emit( self._state, fstart, fstop, data['data_pkt'], pow_data, usable_bins, None) def process_sweep(self, fstart, fstop, data): sweep_segments = list(self._sweep_device.sweep_segments) if not self._state.sweeping(): self.read_block() return self.read_sweep() if len(data) > 2: self.pow_data = data self.iq_data = None if not self._options.get('show_sweep_steps'): sweep_segments = None self.capture_receive.emit( self._state, fstart, fstop, None, self.pow_data, None, sweep_segments) def _state_changed(self, state, changed): """ Emit signal and handle special cases where extra work is needed in response to a state change. """ if not state.sweeping(): # force span to correct value for the mode given if state.decimation > 1: span = (float(self._dut.properties.FULL_BW[state.rfe_mode()]) / state.decimation * self._dut.properties.DECIMATED_USABLE) else: span = self._dut.properties.USABLE_BW[state.rfe_mode()] state = SpecAState(state, span=span) changed = [x for x in changed if x != 'span'] if not self._state or span != self._state.span: changed.append('span') self._state = state # start capture loop again when user switches output path # back to the internal digitizer XXX: very WSA5000-specific if 'device_settings.iq_output_path' in changed: if state.device_settings.get('iq_output_path') == 'DIGITIZER': self.start_capture() elif state.device_settings.get('iq_output_path') == 'CONNECTOR': if state.sweeping(): state.mode = self._dut.properties.RFE_MODES[0] if self._recording_file: self._dut.inject_recording_state(state.to_json_object()) self.state_change.emit(state, changed) def apply_device_settings(self, **kwargs): """ Apply device-specific settings and trigger a state change event. :param kwargs: keyword arguments of SpecAState.device_settings """ device_settings = dict(self._state.device_settings, **kwargs) state = SpecAState(self._state, device_settings=device_settings) if device_settings.get('iq_output_path') == 'CONNECTOR' or 'trigger' in kwargs: self._capture_device.configure_device(device_settings) changed = ['device_settings.%s' % s for s in kwargs] self._state_changed(state, changed) def apply_settings(self, **kwargs): """ Apply state settings and trigger a state change event. :param kwargs: keyword arguments of SpecAState attributes """ if self._state is None: logger.warn('apply_settings with _state == None: %r' % kwargs) return state = SpecAState(self._state, **kwargs) self._state_changed(state, kwargs.keys()) def _apply_complete_settings(self, state_json, playback): """ Apply state setting changes from a complete JSON object. Used for initial settings and applying settings from a recording. """ if self._state: old = self._state.to_json_object() old['playback'] = self._state.playback else: old = {} changed = [ key for key, value in state_json.iteritems() if old.get(key) != value ] if old.get('playback') != playback: changed.append('playback') if 'device_settings' in changed: changed.remove('device_settings') oset = old.get('device_settings', {}) dset = state_json['device_settings'] changed.extend([ 'device_settings.%s' % key for key, value in dset.iteritems() if oset.get(key) != value]) state = SpecAState.from_json_object(state_json, playback) self._state_changed(state, changed) def apply_options(self, **kwargs): """ Apply menu options and signal the change :param kwargs: keyword arguments of the dsp options """ self._options.update(kwargs) self.options_change.emit(dict(self._options), kwargs.keys()) for key, value in kwargs.iteritems(): if key.startswith('dsp.'): self._dsp_options[key[4:]] = value if 'free_plot_adjustment' in kwargs: self.enable_user_xrange_control( not kwargs['free_plot_adjustment']) def get_options(self): return dict(self._options) def enable_user_xrange_control(self, enable): self._user_xrange_control_enabled = enable if not enable: self._pending_user_xrange = None def user_xrange_changed(self, start, stop): if self._user_xrange_control_enabled: self._pending_user_xrange = start, stop def applying_user_xrange(self): return self._applying_user_xrange
class MainPanel(QtGui.QWidget): """ The spectrum view and controls """ def __init__(self, main_window): self._main_window = main_window self.ref_level = 0 self.dut = None self.control_widgets = [] super(MainPanel, self).__init__() screen = QtGui.QDesktopWidget().screenGeometry() self.setMinimumWidth(screen.width() * 0.8) self.setMinimumHeight(screen.height() * 0.6) self._vrt_context = {} self._reactor = self._get_reactor() def _get_reactor(self): # late import because installReactor is being used from twisted.internet import reactor return reactor @inlineCallbacks def open_device(self, name, ok): if not ok: self._main_window.show() return if self.dut: self.dut.disconnect() self._main_window.show() dut = WSA(connector=TwistedConnector(self._reactor)) yield dut.connect(name) self.dev_set = { 'attenuator': 1, 'freq':2450e6, 'decimation': 1, 'fshift': 0, 'rfe_mode': 'SH', 'iq_output_path': 'DIGITIZER'} self.rbw = RBW_VALUES[4] self.enable_mhold = False self.mhold = [] self.dut = dut self.dut_prop = self.dut.properties self.cap_dut = CaptureDevice(dut, async_callback=self.receive_capture, device_settings=self.dev_set) self.initUI() self.enable_controls() self.read_block() def read_block(self): rbw = self.rbw if self.dev_set['rfe_mode'] == 'ZIF': rbw = self.rbw * 2 self.cap_dut.capture_time_domain(self.dev_set['rfe_mode'], self.dev_set['freq'], rbw) def receive_capture(self, fstart, fstop, data): # store usable bins before next call to capture_time_domain self.usable_bins = list(self.cap_dut.usable_bins) self.sweep_segments = None self.read_block() if 'reflevel' in data['context_pkt']: self.ref_level = data['context_pkt']['reflevel'] self.pow_data = compute_fft(self.dut, data['data_pkt'], data['context_pkt'], ref = self.ref_level) self.raw_data = data['data_pkt'] self.update_plot() def initUI(self): grid = QtGui.QGridLayout() grid.setSpacing(5) plot_width = 8 for x in range(plot_width): grid.setColumnMinimumWidth(x, 250) grid.setColumnMinimumWidth(plot_width + 1, 260) grid.addWidget(self._plot_layout(), 0,0,5,plot_width) controls_layout = QtGui.QVBoxLayout() controls_layout.setSpacing(1) controls_layout.addWidget(self._if_attenuator()) controls_layout.addWidget(self._hdr_gain()) controls_layout.addWidget(self._center_freq()) controls_layout.addWidget(self._rbw_controls()) controls_layout.addWidget(self._mode_controls()) controls_layout.addWidget(self._maxh_controls()) controls_layout.addStretch() grid.addLayout(controls_layout, 0, plot_width + 1, 0, 5) self._grid = grid self.setLayout(grid) def _plot_layout(self): # create spectral plot self.window = pg.PlotWidget(name='pyrf_plot') self.window.showGrid(True, True) self.window.setYRange(PLOT_YMIN ,PLOT_YMAX) self.fft_curve = self.window.plot(pen = 'g') self.mhold_curve = self.window.plot(pen = 'y') # create IQ plot widget self.iq_window = pg.PlotWidget(name='IQ Plot') self.i_curve = self.iq_window.plot(pen = 'r') self.q_curve = self.iq_window.plot(pen = 'g') # create split vsplit = QtGui.QSplitter() vsplit.setOrientation(QtCore.Qt.Vertical) vsplit.addWidget(self.window) vsplit.addWidget(self.iq_window) self._plot_layout = vsplit return self._plot_layout def _center_freq(self): grid, widget = self.create_grid_and_widget('Freq') freq_edit = QtGui.QLineEdit(str(self.dev_set['freq'] / float(M))) self._freq_edit = freq_edit self.control_widgets.append(self._freq_edit) def freq_change(): self.dev_set['freq'] = float(freq_edit.text()) * M self.cap_dut.configure_device(self.dev_set) freq_edit.returnPressed.connect(lambda: freq_change()) grid.addWidget(freq_edit, 0,1,0,1) widget.setLayout(grid) return widget def _if_attenuator(self): grid, widget = self.create_grid_and_widget('IF Attenuator') if_attenuator = QtGui.QLineEdit('25') self.control_widgets.append(if_attenuator) def atten_change(): self.dut.var_attenuator(int(if_attenuator.text())) if_attenuator.returnPressed.connect(lambda: atten_change()) grid.addWidget(if_attenuator, 0,1,0,1) widget.setLayout(grid) return widget def _hdr_gain(self): grid, widget = self.create_grid_and_widget('hdr Attenuator') hdr_attenuator = QtGui.QLineEdit('25') self.control_widgets.append(hdr_attenuator) def atten_change(): self.dut.hdr_gain(int(hdr_attenuator.text())) hdr_attenuator.returnPressed.connect(lambda: atten_change()) grid.addWidget(hdr_attenuator, 0,1,0,1) widget.setLayout(grid) return widget def _mode_controls(self): grid, widget = self.create_grid_and_widget('Mode') mode = QtGui.QComboBox(self) mode.addItems(MODES) def new_mode(): self.dev_set['rfe_mode'] = MODES[mode.currentIndex()] self.dut.rfe_mode(self.dev_set['rfe_mode']) mode.setCurrentIndex(1) mode.currentIndexChanged.connect(new_mode) grid.addWidget(mode, 0,1,0,1) widget.setLayout(grid) self.control_widgets.append(mode) return widget def _rbw_controls(self): grid, widget = self.create_grid_and_widget('Sample Size') rbw = QtGui.QComboBox(self) rbw.setToolTip("Change the RBW of the FFT plot") self._hdr_points_values = HDR_RBW_VALUES self._rbw_box = rbw rbw.addItems([str(p) + ' ' for p in SAMPLE_VALUES]) def new_rbw(): self.rbw = RBW_VALUES[rbw.currentIndex()] if self.dev_set['rfe_mode'] == 'HDR': self.rbw = HDR_RBW_VALUES[rbw.currentIndex()] rbw.setCurrentIndex(3) rbw.currentIndexChanged.connect(new_rbw) grid.addWidget(rbw, 0,1,0,1) widget.setLayout(grid) self.control_widgets.append(self._rbw_box) return widget def _maxh_controls(self): grid, widget = self.create_grid_and_widget('Max Hold') max_hold = QtGui.QCheckBox(self) def change_max_hold(): self.enable_mhold = max_hold.isChecked() max_hold.clicked.connect(change_max_hold) grid.addWidget(max_hold, 0,1,0,1) widget.setLayout(grid) return widget def create_grid_and_widget(self, name): grid = QtGui.QGridLayout() widget = QtGui.QWidget() if name is not None: grid.addWidget(QtGui.QLabel(name), 0,0,0,1) widget.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) return grid, widget def update_plot(self): self.update_trace() def update_trace(self): freq_range = np.linspace(self.dev_set['freq'] -62.5, self.dev_set['freq'] + 62.5, len(self.pow_data)) self.fft_curve.setData(freq_range, self.pow_data) if self.enable_mhold: if len(self.mhold) != len(self.pow_data): self.mhold = self.pow_data else: self.mhold = np.maximum(self.mhold, self.pow_data) self.mhold_curve.setData(freq_range, self.mhold) else: self.mhold_curve.setData([], []) self.mhold = [] i_data, q_data, stream_id, spec_inv = _decode_data_pkts(self.raw_data) self.i_curve.setData(i_data) if q_data is not None: self.q_curve.setData(q_data) else: self.q_curve.setData([]) def enable_controls(self): for item in self.control_widgets: item.setEnabled(True) def disable_controls(self): for item in self.control_widgets: item.setEnabled(False)
class MainPanel(QtGui.QWidget): """ The spectrum view and controls """ def __init__(self, main_window): self._main_window = main_window self.ref_level = 0 self.dut = None self.control_widgets = [] super(MainPanel, self).__init__() screen = QtGui.QDesktopWidget().screenGeometry() self.setMinimumWidth(screen.width() * 0.8) self.setMinimumHeight(screen.height() * 0.6) self._vrt_context = {} self._reactor = self._get_reactor() def _get_reactor(self): # late import because installReactor is being used from twisted.internet import reactor return reactor @inlineCallbacks def open_device(self, name, ok): if not ok: self._main_window.show() return if self.dut: self.dut.disconnect() self._main_window.show() dut = WSA(connector=TwistedConnector(self._reactor)) yield dut.connect(name) self.dev_set = { 'attenuator': 1, 'freq': 2450e6, 'decimation': 1, 'fshift': 0, 'rfe_mode': 'SH', 'iq_output_path': 'DIGITIZER' } self.rbw = RBW_VALUES[4] self.enable_mhold = False self.mhold = [] self.dut = dut self.dut_prop = self.dut.properties self.cap_dut = CaptureDevice(dut, async_callback=self.receive_capture, device_settings=self.dev_set) self.initUI() self.enable_controls() self.read_block() def read_block(self): rbw = self.rbw if self.dev_set['rfe_mode'] == 'ZIF': rbw = self.rbw * 2 self.cap_dut.capture_time_domain(self.dev_set['rfe_mode'], self.dev_set['freq'], rbw) def receive_capture(self, fstart, fstop, data): # store usable bins before next call to capture_time_domain self.usable_bins = list(self.cap_dut.usable_bins) self.sweep_segments = None self.read_block() if 'reflevel' in data['context_pkt']: self.ref_level = data['context_pkt']['reflevel'] self.pow_data = compute_fft(self.dut, data['data_pkt'], data['context_pkt'], ref=self.ref_level) self.raw_data = data['data_pkt'] self.update_plot() def initUI(self): grid = QtGui.QGridLayout() grid.setSpacing(5) plot_width = 8 for x in range(plot_width): grid.setColumnMinimumWidth(x, 250) grid.setColumnMinimumWidth(plot_width + 1, 260) grid.addWidget(self._plot_layout(), 0, 0, 5, plot_width) controls_layout = QtGui.QVBoxLayout() controls_layout.setSpacing(1) controls_layout.addWidget(self._if_attenuator()) controls_layout.addWidget(self._hdr_gain()) controls_layout.addWidget(self._center_freq()) controls_layout.addWidget(self._rbw_controls()) controls_layout.addWidget(self._mode_controls()) controls_layout.addWidget(self._maxh_controls()) controls_layout.addStretch() grid.addLayout(controls_layout, 0, plot_width + 1, 0, 5) self._grid = grid self.setLayout(grid) def _plot_layout(self): # create spectral plot self.window = pg.PlotWidget(name='pyrf_plot') self.window.showGrid(True, True) self.window.setYRange(PLOT_YMIN, PLOT_YMAX) self.fft_curve = self.window.plot(pen='g') self.mhold_curve = self.window.plot(pen='y') # create IQ plot widget self.iq_window = pg.PlotWidget(name='IQ Plot') self.i_curve = self.iq_window.plot(pen='r') self.q_curve = self.iq_window.plot(pen='g') # create split vsplit = QtGui.QSplitter() vsplit.setOrientation(QtCore.Qt.Vertical) vsplit.addWidget(self.window) vsplit.addWidget(self.iq_window) self._plot_layout = vsplit return self._plot_layout def _center_freq(self): grid, widget = self.create_grid_and_widget('Freq') freq_edit = QtGui.QLineEdit(str(self.dev_set['freq'] / float(M))) self._freq_edit = freq_edit self.control_widgets.append(self._freq_edit) def freq_change(): self.dev_set['freq'] = float(freq_edit.text()) * M self.cap_dut.configure_device(self.dev_set) freq_edit.returnPressed.connect(lambda: freq_change()) grid.addWidget(freq_edit, 0, 1, 0, 1) widget.setLayout(grid) return widget def _if_attenuator(self): grid, widget = self.create_grid_and_widget('IF Attenuator') if_attenuator = QtGui.QLineEdit('25') self.control_widgets.append(if_attenuator) def atten_change(): self.dut.var_attenuator(int(if_attenuator.text())) if_attenuator.returnPressed.connect(lambda: atten_change()) grid.addWidget(if_attenuator, 0, 1, 0, 1) widget.setLayout(grid) return widget def _hdr_gain(self): grid, widget = self.create_grid_and_widget('hdr Attenuator') hdr_attenuator = QtGui.QLineEdit('25') self.control_widgets.append(hdr_attenuator) def atten_change(): self.dut.hdr_gain(int(hdr_attenuator.text())) hdr_attenuator.returnPressed.connect(lambda: atten_change()) grid.addWidget(hdr_attenuator, 0, 1, 0, 1) widget.setLayout(grid) return widget def _mode_controls(self): grid, widget = self.create_grid_and_widget('Mode') mode = QtGui.QComboBox(self) mode.addItems(MODES) def new_mode(): self.dev_set['rfe_mode'] = MODES[mode.currentIndex()] self.dut.rfe_mode(self.dev_set['rfe_mode']) mode.setCurrentIndex(1) mode.currentIndexChanged.connect(new_mode) grid.addWidget(mode, 0, 1, 0, 1) widget.setLayout(grid) self.control_widgets.append(mode) return widget def _rbw_controls(self): grid, widget = self.create_grid_and_widget('Sample Size') rbw = QtGui.QComboBox(self) rbw.setToolTip("Change the RBW of the FFT plot") self._hdr_points_values = HDR_RBW_VALUES self._rbw_box = rbw rbw.addItems([str(p) + ' ' for p in SAMPLE_VALUES]) def new_rbw(): self.rbw = RBW_VALUES[rbw.currentIndex()] if self.dev_set['rfe_mode'] == 'HDR': self.rbw = HDR_RBW_VALUES[rbw.currentIndex()] rbw.setCurrentIndex(3) rbw.currentIndexChanged.connect(new_rbw) grid.addWidget(rbw, 0, 1, 0, 1) widget.setLayout(grid) self.control_widgets.append(self._rbw_box) return widget def _maxh_controls(self): grid, widget = self.create_grid_and_widget('Max Hold') max_hold = QtGui.QCheckBox(self) def change_max_hold(): self.enable_mhold = max_hold.isChecked() max_hold.clicked.connect(change_max_hold) grid.addWidget(max_hold, 0, 1, 0, 1) widget.setLayout(grid) return widget def create_grid_and_widget(self, name): grid = QtGui.QGridLayout() widget = QtGui.QWidget() if name is not None: grid.addWidget(QtGui.QLabel(name), 0, 0, 0, 1) widget.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) return grid, widget def update_plot(self): self.update_trace() def update_trace(self): freq_range = np.linspace(self.dev_set['freq'] - 62.5, self.dev_set['freq'] + 62.5, len(self.pow_data)) self.fft_curve.setData(freq_range, self.pow_data) if self.enable_mhold: if len(self.mhold) != len(self.pow_data): self.mhold = self.pow_data else: self.mhold = np.maximum(self.mhold, self.pow_data) self.mhold_curve.setData(freq_range, self.mhold) else: self.mhold_curve.setData([], []) self.mhold = [] i_data, q_data, stream_id, spec_inv = _decode_data_pkts(self.raw_data) self.i_curve.setData(i_data) if q_data is not None: self.q_curve.setData(q_data) else: self.q_curve.setData([]) def enable_controls(self): for item in self.control_widgets: item.setEnabled(True) def disable_controls(self): for item in self.control_widgets: item.setEnabled(False)
class MainPanel(QtGui.QWidget): """ The spectrum view and controls """ def __init__(self, main_window,output_file): self._main_window = main_window self.ref_level = 0 self.dut = None self.control_widgets = [] self._output_file = output_file super(MainPanel, self).__init__() screen = QtGui.QDesktopWidget().screenGeometry() self.setMinimumWidth(screen.width() * 0.7) self.setMinimumHeight(screen.height() * 0.6) self.plot_state = None # plot window self._plot = Plot(self) self._marker_trace = None self._vrt_context = {} self.initUI() self.disable_controls() self.ref_level = 0 self._reactor = self._get_reactor() def _get_reactor(self): # late import because installReactor is being used from twisted.internet import reactor return reactor def open_device_dialog(self): name, ok = QtGui.QInputDialog.getText(self, 'Open Device', 'Enter a hostname or IP address:') while True: if not ok: return try: self.open_device(name) break except socket.error: name, ok = QtGui.QInputDialog.getText(self, 'Open Device', 'Connection Failed, please try again\n\n' 'Enter a hostname or IP address:') @inlineCallbacks def open_device(self, name): if self.dut: self.dut.disconnect() dut = WSA(connector=TwistedConnector(self._reactor)) yield dut.connect(name) if hasattr(dut.properties, 'MINIMUM_FW_VERSION') and parse_version( dut.fw_version) < parse_version(dut.properties.MINIMUM_FW_VERSION): too_old = QtGui.QMessageBox() too_old.setText('Your device firmware version is {0}' ' but this application is expecting at least version' ' {1}. Some features may not work properly'.format( dut.fw_version, dut.properties.MINIMUM_FW_VERSION)) too_old.exec_() if self._output_file: dut.set_capture_output(self._output_file) self.dut = dut self.plot_state = gui_config.PlotState(dut.properties) self.dut_prop = self.dut.properties self.sweep_dut = SweepDevice(dut, self.receive_sweep) self.cap_dut = CaptureDevice(dut, async_callback=self.receive_capture, device_settings=self.plot_state.dev_set) self._dev_group.configure(self.dut.properties) self.enable_controls() cu._select_center_freq(self) self._rbw_box.setCurrentIndex(3) self._plot.const_window.show() self._plot.iq_window.show() self.plot_state.enable_block_mode(self) self.read_block() def read_sweep(self): device_set = dict(self.plot_state.dev_set) device_set.pop('rfe_mode') device_set.pop('freq') device_set.pop('decimation') device_set.pop('fshift') device_set.pop('iq_output_path') self.sweep_dut.capture_power_spectrum(self.plot_state.fstart, self.plot_state.fstop, self.plot_state.rbw, device_set, mode=self._sweep_mode) def read_block(self): self.cap_dut.capture_time_domain(self.plot_state.rbw) def receive_capture(self, fstart, fstop, data): # store usable bins before next call to capture_time_domain self.usable_bins = list(self.cap_dut.usable_bins) self.sweep_segments = None # only read data if WSA digitizer is used if self.plot_state.dev_set['iq_output_path'] == 'DIGITIZER': if not self.plot_state.block_mode: self.read_sweep() return self.read_block() if 'reflevel' in data['context_pkt']: self.ref_level = data['context_pkt']['reflevel'] self.pow_data = compute_fft(self.dut, data['data_pkt'], data['context_pkt'], ref = self.ref_level) self.raw_data = data['data_pkt'] self.update_plot() def receive_sweep(self, fstart, fstop, data): self.sweep_segments = list(self.sweep_dut.sweep_segments) self.usable_bins = None if self.plot_state.block_mode: self.read_block() return self.read_sweep() if len(data) > 2: self.pow_data = data self.iq_data = None self.update_plot() def keyPressEvent(self, event): if self.dut: hotkey_util(self, event) def mousePressEvent(self, event): if self.dut: marker = self._plot.markers[self._marker_tab.currentIndex()] trace = self._plot.traces[marker.trace_index] if event.button() == QtCore.Qt.MouseButton.LeftButton: click_pos = event.pos().x() - 68 plot_window_width = self._plot.window.width() - 68 if click_pos < plot_window_width and click_pos > 0: window_freq = self._plot.view_box.viewRange()[0] window_bw = (window_freq[1] - window_freq[0]) click_freq = ((float(click_pos) / float(plot_window_width)) * float(window_bw)) + window_freq[0] index = find_nearest_index(click_freq, trace.freq_range) self._plot.markers[self._marker_tab.currentIndex()].data_index = index def initUI(self): grid = QtGui.QGridLayout() grid.setSpacing(10) for x in range(8): grid.setColumnMinimumWidth(x, 300) # add plot widget plot_width = 8 grid.addWidget(self._plot_layout(),0,0,13,plot_width) self.marker_labels = [] marker_label, delta_label, diff_label = self._marker_labels() self.marker_labels.append(marker_label) self.marker_labels.append(delta_label) grid.addWidget(marker_label, 0, 1, 1, 2) grid.addWidget(delta_label, 0, 3, 1, 2) grid.addWidget(diff_label , 0, 5, 1, 2) y = 0 x = plot_width controls_layout = QtGui.QVBoxLayout() controls_layout.addWidget(self._trace_controls()) controls_layout.addWidget(self._plot_controls()) controls_layout.addWidget(self._device_controls()) controls_layout.addWidget(self._freq_controls()) controls_layout.addStretch() grid.addLayout(controls_layout, y, x, 13, 5) self._grid = grid self.update_freq() self.setLayout(grid) def _plot_layout(self): vsplit = QtGui.QSplitter() vsplit.setOrientation(QtCore.Qt.Vertical) vsplit.addWidget(self._plot.window) hsplit = QtGui.QSplitter() hsplit.addWidget(self._plot.const_window) hsplit.addWidget(self._plot.iq_window) self._plot.const_window.heightForWidth(1) self._plot.const_window.hide() self._plot.iq_window.hide() vsplit.addWidget(hsplit) self._plot_layout = vsplit return self._plot_layout def _trace_controls(self): trace_group = QtGui.QGroupBox("Traces") self._trace_group = trace_group trace_controls_layout = QtGui.QVBoxLayout() # first row will contain the tabs first_row = QtGui.QHBoxLayout() # add tabs for each trace trace_tab = QtGui.QTabBar() count = 0 for (trace,(r,g,b)) in zip(labels.TRACES, colors.TRACE_COLORS): trace_tab.addTab(trace) color = QtGui.QColor() color.setRgb(r,g,b) pixmap = QtGui.QPixmap(10,10) pixmap.fill(color) icon = QtGui.QIcon(pixmap) trace_tab.setTabIcon(count,icon) count += 1 self._trace_tab = trace_tab trace_tab.currentChanged.connect(lambda: cu._trace_tab_change(self)) self.control_widgets.append(self._trace_tab) first_row.addWidget(trace_tab) # second row contains the tab attributes second_row = QtGui.QHBoxLayout() max_hold, min_hold, write, store, blank = self._trace_items() second_row.addWidget(max_hold) second_row.addWidget(min_hold) second_row.addWidget(write) second_row.addWidget(blank) second_row.addWidget(store) trace_controls_layout.addLayout(first_row) trace_controls_layout.addLayout(second_row) trace_group.setLayout(trace_controls_layout) return trace_group def _trace_items(self): trace_attr = {} store = QtGui.QCheckBox('Store') store.clicked.connect(lambda: cu._store_trace(self)) store.setEnabled(False) trace_attr['store'] = store max_hold = QtGui.QRadioButton('Max Hold') max_hold.clicked.connect(lambda: cu._max_hold(self)) trace_attr['max_hold'] = max_hold min_hold = QtGui.QRadioButton('Min Hold') min_hold.clicked.connect(lambda: cu._min_hold(self)) trace_attr['min_hold'] = min_hold write = QtGui.QRadioButton('Write') write.clicked.connect(lambda: cu._trace_write(self)) trace_attr['write'] = write blank = QtGui.QRadioButton('Blank') blank.clicked.connect(lambda: cu._blank_trace(self)) trace_attr['blank'] = blank self._trace_attr = trace_attr self._trace_attr['write'].click() return max_hold, min_hold, write, store, blank def _device_controls(self): self._dev_group = DeviceControlsWidget() self._connect_device_controls() self.control_widgets.append(self._dev_group) return self._dev_group def _connect_device_controls(self): def new_antenna(): self.plot_state.dev_set['antenna'] = (int(self._dev_group._antenna_box.currentText().split()[-1])) self.cap_dut.configure_device(self.plot_state.dev_set) def new_dec(): self.plot_state.dev_set['decimation'] = int( self._dev_group._dec_box.currentText().split(' ')[-1]) self.cap_dut.configure_device(self.plot_state.dev_set) self.update_freq() def new_freq_shift(): rfe_mode = 'ZIF' prop = self.dut.properties max_fshift = prop.MAX_FSHIFT[rfe_mode] try: if float(self._dev_group._freq_shift_edit.text()) * M < max_fshift: self.plot_state.dev_set['fshift'] = float(self._dev_group._freq_shift_edit.text()) * M else: self._dev_group._freq_shift_edit.setText(str(self.plot_state.dev_set['fshift'] / M)) except ValueError: self._dev_group._freq_shift_edit.setText(str(self.plot_state.dev_set['fshift'] / M)) return self.cap_dut.configure_device(self.plot_state.dev_set) def new_gain(): self.plot_state.dev_set['gain'] = self._dev_group._gain_box.currentText().split()[-1].lower().encode('ascii') self.cap_dut.configure_device(self.plot_state.dev_set) def new_ifgain(): self.plot_state.dev_set['ifgain'] = self._dev_group._ifgain_box.value() self.cap_dut.configure_device(self.plot_state.dev_set) def new_attenuator(): self.plot_state.dev_set['attenuator'] = self._dev_group._attenuator_box.isChecked() self.cap_dut.configure_device(self.plot_state.dev_set) def new_iq_path(): self.plot_state.dev_set['iq_output_path'] = str(self._dev_group._iq_output_box.currentText().split()[-1]) if self._dev_group._mode.currentIndex() > 2: self._dev_group._mode.setCurrentIndex(0) if self.plot_state.dev_set['iq_output_path'] == 'CONNECTOR': # disable unwanted controls cu._external_digitizer_mode(self) else: cu._internal_digitizer_mode(self) self.cap_dut.configure_device(self.plot_state.dev_set) def new_input_mode(): m = self._dev_group._mode.currentText() if not m: return if m.startswith('Sweep '): self._plot.const_window.hide() self._plot.iq_window.hide() self.plot_state.disable_block_mode(self) self._dev_group._dec_box.setEnabled(False) self._dev_group._freq_shift_edit.setEnabled(False) self._sweep_mode = m.split(' ',1)[1] return self._plot.const_window.show() self._plot.iq_window.show() self.plot_state.enable_block_mode(self) self.plot_state.dev_set['rfe_mode'] = str(m) cu._update_rbw_values(self) self.plot_state.bandwidth = self.dut_prop.FULL_BW[m] self.plot_state.update_freq_set( fcenter=float(self._freq_edit.text()) * M) cu._center_plot_view(self) self.cap_dut.configure_device(self.plot_state.dev_set) self._rbw_box.setCurrentIndex(4 if m == 'SH' else 3) cu._center_plot_view(self) if m == 'HDR': self._dev_group._dec_box.setEnabled(False) self._dev_group._freq_shift_edit.setEnabled(False) else: self._dev_group._dec_box.setEnabled(True) self._dev_group._freq_shift_edit.setEnabled(True) self._dev_group._antenna_box.currentIndexChanged.connect(new_antenna) self._dev_group._gain_box.currentIndexChanged.connect(new_gain) self._dev_group._dec_box.currentIndexChanged.connect(new_dec) self._dev_group._freq_shift_edit.returnPressed.connect(new_freq_shift) self._dev_group._ifgain_box.valueChanged.connect(new_ifgain) self._dev_group._attenuator_box.clicked.connect(new_attenuator) self._dev_group._mode.currentIndexChanged.connect(new_input_mode) self._dev_group._iq_output_box.currentIndexChanged.connect(new_iq_path) def _trigger_control(self): trigger = QtGui.QCheckBox("Trigger") trigger.setToolTip("[T]\nTurn the Triggers on/off") trigger.clicked.connect(lambda: cu._trigger_control(self)) self._trigger = trigger self.control_widgets.append(self._trigger) return trigger def _attenuator_control(self): attenuator = QtGui.QCheckBox("Attenuator") attenuator.setChecked(True) def new_attenuator(): self.plot_state.dev_set['attenuator'] = attenuator.isChecked() attenuator.clicked.connect(new_attenuator) self._attenuator_box = attenuator self.control_widgets.append(attenuator) return attenuator def _freq_controls(self): freq_group = QtGui.QGroupBox("Frequency Control") self._freq_group = freq_group freq_layout = QtGui.QVBoxLayout() fstart_hbox = QtGui.QHBoxLayout() fstart_bt, fstart_txt = self._fstart_controls() fstart_hbox.addWidget(fstart_bt) fstart_hbox.addWidget(fstart_txt) fstart_hbox.addWidget(QtGui.QLabel('MHz')) self._fstart_hbox = fstart_hbox cfreq_hbox = QtGui.QHBoxLayout() cfreq_bt, cfreq_txt = self._center_freq() cfreq_hbox.addWidget(cfreq_bt) cfreq_hbox.addWidget(cfreq_txt) cfreq_hbox.addWidget(QtGui.QLabel('MHz')) self._cfreq_hbox = cfreq_hbox bw_hbox = QtGui.QHBoxLayout() bw_bt, bw_txt = self._bw_controls() bw_hbox.addWidget(bw_bt) bw_hbox.addWidget(bw_txt) bw_hbox.addWidget(QtGui.QLabel('MHz')) self._bw_hbox = bw_hbox fstop_hbox = QtGui.QHBoxLayout() fstop_bt, fstop_txt = self._fstop_controls() fstop_hbox.addWidget(fstop_bt) fstop_hbox.addWidget(fstop_txt) fstop_hbox.addWidget(QtGui.QLabel('MHz')) self._fstop_hbox = fstop_hbox freq_inc_hbox = QtGui.QHBoxLayout() freq_inc_steps, freq_inc_plus, freq_inc_minus = self._freq_incr() freq_inc_hbox.addWidget(freq_inc_minus) freq_inc_hbox.addWidget(freq_inc_steps) freq_inc_hbox.addWidget(freq_inc_plus) self._freq_inc_hbox = freq_inc_hbox rbw_hbox = QtGui.QHBoxLayout() rbw = self._rbw_controls() rbw_hbox.addWidget(QtGui.QLabel('Resolution Bandwidth:')) rbw_hbox.addWidget(rbw) self._rbw_hbox = rbw_hbox freq_layout.addLayout(self._fstart_hbox) freq_layout.addLayout(self._cfreq_hbox) freq_layout.addLayout(self._bw_hbox) freq_layout.addLayout(self._fstop_hbox) freq_layout.addLayout(self._freq_inc_hbox) freq_layout.addLayout(self._rbw_hbox) freq_group.setLayout(freq_layout) self._freq_layout = freq_layout return freq_group def _center_freq(self): cfreq = QtGui.QPushButton('Center') cfreq.setToolTip("[2]\nTune the center frequency") self._cfreq = cfreq cfreq.clicked.connect(lambda: cu._select_center_freq(self)) freq_edit = QtGui.QLineEdit(str(gui_config.INIT_CENTER_FREQ / float(M))) self._freq_edit = freq_edit self.control_widgets.append(self._cfreq) self.control_widgets.append(self._freq_edit) def freq_change(): cu._select_center_freq(self) self.update_freq() self.update_freq_edit() freq_edit.returnPressed.connect(lambda: freq_change()) return cfreq, freq_edit def _freq_incr(self): steps = QtGui.QComboBox(self) steps.addItem("Adjust: 1 MHz") steps.addItem("Adjust: 2.5 MHz") steps.addItem("Adjust: 10 MHz") steps.addItem("Adjust: 25 MHz") steps.addItem("Adjust: 100 MHz") self.fstep = float(steps.currentText().split()[1]) def freq_step_change(): self.fstep = float(steps.currentText().split()[1]) steps.currentIndexChanged.connect(freq_step_change) steps.setCurrentIndex(2) self._fstep_box = steps def freq_step(factor): try: f = float(self._freq_edit.text()) except ValueError: return delta = float(steps.currentText().split()[1]) * factor self.update_freq(delta) self.update_freq_edit() freq_minus = QtGui.QPushButton('-') freq_minus.clicked.connect(lambda: freq_step(-1)) self._freq_minus = freq_minus freq_plus = QtGui.QPushButton('+') freq_plus.clicked.connect(lambda: freq_step(1)) self._freq_plus = freq_plus self.control_widgets.append(self._freq_plus) self.control_widgets.append(self._freq_minus) self.control_widgets.append(self._fstep_box) return steps, freq_plus, freq_minus def _bw_controls(self): bw = QtGui.QPushButton('Span') bw.setToolTip("[3]\nChange the bandwidth of the current plot") self._bw = bw bw.clicked.connect(lambda: cu._select_bw(self)) bw_edit = QtGui.QLineEdit(str(gui_config.INIT_BANDWIDTH / float(M))) def freq_change(): cu._select_bw(self) self.update_freq() self.update_freq_edit() bw_edit.returnPressed.connect(lambda: freq_change()) self._bw_edit = bw_edit self.control_widgets.append(self._bw_edit) self.control_widgets.append(self._bw) return bw, bw_edit def _fstart_controls(self): fstart = QtGui.QPushButton('Start') fstart.setToolTip("[1]\nTune the start frequency") self._fstart = fstart fstart.clicked.connect(lambda: cu._select_fstart(self)) f = gui_config.INIT_CENTER_FREQ - (gui_config.INIT_BANDWIDTH / 2) freq = QtGui.QLineEdit(str(f / float(M))) def freq_change(): cu._select_fstart(self) self.update_freq() self.update_freq_edit() freq.returnPressed.connect(lambda: freq_change()) self._fstart_edit = freq self.control_widgets.append(self._fstart) self.control_widgets.append(self._fstart_edit) return fstart, freq def _fstop_controls(self): fstop = QtGui.QPushButton('Stop') fstop.setToolTip("[4]Tune the stop frequency") self._fstop = fstop fstop.clicked.connect(lambda: cu._select_fstop(self)) f = gui_config.INIT_CENTER_FREQ + (gui_config.INIT_BANDWIDTH / 2) freq = QtGui.QLineEdit(str(f / float(M))) def freq_change(): cu._select_fstop(self) self.update_freq() self.update_freq_edit() freq.returnPressed.connect(lambda: freq_change()) self._fstop_edit = freq self.control_widgets.append(self._fstop) self.control_widgets.append(self._fstop_edit) return fstop, freq def _rbw_controls(self): rbw = QtGui.QComboBox(self) rbw.setToolTip("Change the RBW of the FFT plot") self._points_values = RBW_VALUES self._hdr_points_values = HDR_RBW_VALUES self._rbw_box = rbw rbw.addItems([str(p) + ' KHz' for p in self._points_values]) def new_rbw(): if not self.plot_state.dev_set['rfe_mode'] == 'HDR': self.plot_state.update_freq_set(rbw = 1e3 * self._points_values[rbw.currentIndex()]) else: self.plot_state.update_freq_set(rbw = self._hdr_points_values[rbw.currentIndex()]) rbw.setCurrentIndex(0) rbw.currentIndexChanged.connect(new_rbw) self.control_widgets.append(self._rbw_box) return rbw def update_freq(self, delta=None): if not self.dut: return prop = self.dut.properties rfe_mode = self.plot_state.dev_set['rfe_mode'] min_tunable = prop.MIN_TUNABLE[rfe_mode] max_tunable = prop.MAX_TUNABLE[rfe_mode] if delta == None: delta = 0 try: if self.plot_state.freq_sel == 'CENT': f = (float(self._freq_edit.text()) + delta) * M if f > max_tunable or f < min_tunable: return self.plot_state.update_freq_set(fcenter = f) self.cap_dut.configure_device(self.plot_state.dev_set) elif self.plot_state.freq_sel == 'FSTART': f = (float(self._fstart_edit.text()) + delta) * M if f > max_tunable or f <min_tunable or f > self.plot_state.fstop: return self.plot_state.update_freq_set(fstart = f) elif self.plot_state.freq_sel == 'FSTOP': f = (float(self._fstop_edit.text()) + delta) * M if f > max_tunable or f < min_tunable or f < self.plot_state.fstart: return self.plot_state.update_freq_set(fstop = f) elif self.plot_state.freq_sel == 'BW': f = (float(self._bw_edit.text()) + delta) * M if f < 0: return self.plot_state.update_freq_set(bw = f) for trace in self._plot.traces: try: trace.data = self.pow_data except AttributeError: break except ValueError: return if self.plot_state.trig: freq_region = self._plot.freqtrig_lines.getRegion() if (freq_region[0] < self.plot_state.fstart and freq_region[1] < self.plot_state.fstart) or (freq_region[0] > self.plot_state.fstop and freq_region[1] > self.plot_state.fstop): self._plot.freqtrig_lines.setRegion([self.plot_state.fstart,self.plot_state. fstop]) def update_freq_edit(self): self._fstop_edit.setText("%0.1f" % (self.plot_state.fstop/ 1e6)) self._fstart_edit.setText("%0.1f" % (self.plot_state.fstart/ 1e6)) self._freq_edit.setText("%0.1f" % (self.plot_state.center_freq / 1e6)) self._bw_edit.setText("%0.1f" % (self.plot_state.bandwidth / 1e6)) self._center_bt.click() def _plot_controls(self): plot_group = QtGui.QGroupBox("Plot Control") self._plot_group = plot_group plot_controls_layout = QtGui.QVBoxLayout() first_row = QtGui.QHBoxLayout() marker_tab = QtGui.QTabBar() for marker in labels.MARKERS: marker_tab.addTab(marker) marker_tab.currentChanged.connect(lambda: cu._marker_tab_change(self)) first_row.addWidget(marker_tab) self._marker_tab = marker_tab self.control_widgets.append(self._marker_tab) marker_check, marker_trace = self._marker_control() second_row = QtGui.QHBoxLayout() second_row.addWidget(marker_trace) second_row.addWidget(marker_check) third_row = QtGui.QHBoxLayout() third_row.addWidget(self._peak_control()) third_row.addWidget(self._center_control()) fourth_row = QtGui.QHBoxLayout() ref_level, ref_label, min_level, min_label = self._ref_controls() fourth_row.addWidget(ref_label) fourth_row.addWidget(ref_level) fourth_row.addWidget(min_label) fourth_row.addWidget(min_level) plot_controls_layout.addLayout(first_row) plot_controls_layout.addLayout(second_row) plot_controls_layout.addLayout(third_row) plot_controls_layout.addLayout(fourth_row) plot_group.setLayout(plot_controls_layout) return plot_group def _marker_control(self): marker_trace = QtGui.QComboBox() marker_trace.setEnabled(False) marker_trace.setMaximumWidth(50) marker_trace.currentIndexChanged.connect(lambda: cu._marker_trace_control(self)) update_marker_traces(marker_trace, self._plot.traces) self._marker_trace = marker_trace marker_check = QtGui.QCheckBox('Enabled') marker_check.clicked.connect(lambda: cu._marker_control(self)) self._marker_check = marker_check self.control_widgets.append(self._marker_check) return marker_check,marker_trace def _peak_control(self): peak = QtGui.QPushButton('Peak') peak.setToolTip("[P]\nFind peak of the selected spectrum") peak.clicked.connect(lambda: cu._find_peak(self)) self._peak = peak self.control_widgets.append(self._peak) return peak def _center_control(self): center = QtGui.QPushButton('Recenter') center.setToolTip("[C]\nCenter the Plot View around the available spectrum") center.clicked.connect(lambda: cu._center_plot_view(self)) self._center_bt = center self.control_widgets.append(self._center_bt) return center def _ref_controls(self): ref_level = QtGui.QLineEdit(str(PLOT_YMAX)) ref_level.returnPressed.connect(lambda: cu._change_ref_level(self)) self._ref_level = ref_level self.control_widgets.append(self._ref_level) ref_label = QtGui.QLabel('Reference Level: ') min_level = QtGui.QLineEdit(str(PLOT_YMIN)) min_level.returnPressed.connect(lambda: cu._change_min_level(self)) min_label = QtGui.QLabel('Minimum Level: ') self._min_level = min_level self.control_widgets.append(self._min_level) return ref_level, ref_label, min_level, min_label def _marker_labels(self): marker_label = QtGui.QLabel('') marker_label.setStyleSheet('color: %s;' % colors.TEAL) marker_label.setMinimumHeight(25) delta_label = QtGui.QLabel('') delta_label.setStyleSheet('color: %s;' % colors.TEAL) delta_label.setMinimumHeight(25) diff_label = QtGui.QLabel('') diff_label.setStyleSheet('color: %s;' % colors.WHITE) diff_label.setMinimumHeight(25) self._diff_lab = diff_label return marker_label,delta_label, diff_label def update_plot(self): self.plot_state.update_freq_range(self.plot_state.fstart, self.plot_state.fstop, len(self.pow_data)) self.update_trace() self.update_iq() self.update_marker() self.update_diff() def update_trace(self): for trace in self._plot.traces: trace.update_curve( self.plot_state.freq_range, self.pow_data, self.usable_bins, self.sweep_segments) def update_iq(self): if self.raw_data.stream_id == VRT_IFDATA_I14Q14: data = self.raw_data.data.numpy_array() i_data = np.array(data[:,0], dtype=float)/ZIF_BITS q_data = np.array(data[:,1], dtype=float)/ZIF_BITS self._plot.i_curve.setData(i_data) self._plot.q_curve.setData(q_data) self._plot.const_plot.clear() self._plot.const_plot.addPoints( x = i_data[0:CONST_POINTS], y = q_data[0:CONST_POINTS], symbol = 'o', size = 1, pen = 'y', brush = 'y') else: data = self.raw_data.data.numpy_array() i_data = np.array(data, dtype=float) if self.raw_data.stream_id == VRT_IFDATA_I14: i_data = i_data /ZIF_BITS self._plot.i_curve.setData(i_data) self._plot.q_curve.clear() self._plot.const_plot.clear() def update_trig(self): if self.plot_state.trig_set: freq_region = self._plot.freqtrig_lines.getRegion() self.plot_state.trig_set = TriggerSettings(TRIGGER_TYPE_LEVEL, min(freq_region), max(freq_region), self._plot.amptrig_line.value()) self.dut.trigger(self.plot_state.trig_set) def update_marker(self): for marker, marker_label in zip(self._plot.markers, self.marker_labels): if marker.enabled: trace = self._plot.traces[marker.trace_index] if not trace.blank: marker_label.setStyleSheet('color: rgb(%s, %s, %s);' % (trace.color[0], trace.color[1], trace.color[2])) marker.update_pos(trace.freq_range, trace.data) marker_text = 'Frequency: %0.2f MHz \n Power %0.2f dBm' % (trace.freq_range[marker.data_index]/1e6, trace.data[marker.data_index]) marker_label.setText(marker_text) else: marker_label.setText('') def update_diff(self): num_markers = 0 traces = [] data_indices = [] for marker in self._plot.markers: if marker.enabled == True: num_markers += 1 traces.append(self._plot.traces[marker.trace_index]) data_indices.append(marker.data_index) if num_markers == len(labels.MARKERS): freq_diff = np.abs((traces[0].freq_range[data_indices[0]]/1e6) - (traces[1].freq_range[data_indices[1]]/1e6)) power_diff = np.abs((traces[0].data[data_indices[0]]) - (traces[1].data[data_indices[1]])) delta_text = 'Delta : %0.1f MHz \nDelta %0.2f dBm' % (freq_diff, power_diff ) self._diff_lab.setText(delta_text) else: self._diff_lab.setText('') def enable_controls(self): for item in self.control_widgets: item.setEnabled(True) for key in self._trace_attr: self._trace_attr[key].setEnabled(True) def disable_controls(self): for item in self.control_widgets: item.setEnabled(False) for key in self._trace_attr: self._trace_attr[key].setEnabled(False)
class SpecAController(QtCore.QObject): """ The controller for the rtsa-gui. Issues commands to device, stores and broadcasts changes to GUI state. """ _dut = None _sweep_device = None _capture_device = None _plot_options = None _state = None _recording_file = None _csv_file = None _export_csv = False _playback_file = None _playback_sweep_data = None _pending_user_xrange = None _applying_user_xrange = False _user_xrange_control_enabled = True _single_capture = False device_change = QtCore.Signal(object) state_change = QtCore.Signal(SpecAState, list) capture_receive = QtCore.Signal(SpecAState, float, float, object, object, object, object) options_change = QtCore.Signal(dict, list) plot_change = QtCore.Signal(dict, list) def __init__(self, developer_mode=False): super(SpecAController, self).__init__() self._dsp_options = {} self._options = {} self._plot_options = {'cont_cap_mode': True, 'mouse_tune': True} self.developer_mode = developer_mode self.was_sweeping = False def set_device(self, dut=None, playback_filename=None): """ Detach any currenly attached device and stop playback then optionally attach to a new device or playback file. :param dut: a :class:`pyrf.thinkrf.WSA` or None :param playback_filename: recorded VRT data filename or None :param playback_scheduler: function to schedule each playback capture """ if self._playback_file: self._playback_file.close() self._playback_file = None if self._dut: self._dut.disconnect() if playback_filename: self._playback_file = open(playback_filename, 'rb') self._playback_started = False self._playback_context = {} vrt_packet = self._playback_vrt(auto_rewind=False) state_json = vrt_packet.fields['speca'] # support old playback files if state_json['device_identifier'] == 'unknown': state_json['device_identifier'] = 'ThinkRF,WSA5000 v3,0,0' dut = Playback(state_json['device_class'], state_json['device_identifier']) self._sweep_device = SweepDevice(dut) self._capture_device = CaptureDevice(dut) elif dut: dut.reset() self._sweep_device = SweepDevice(dut, self.process_sweep) self._capture_device = CaptureDevice(dut, self.process_capture) state_json = dict(dut.properties.SPECA_DEFAULTS, device_identifier=dut.device_id) self._dut = dut if not dut: return self.device_change.emit(dut) self._apply_complete_settings(state_json, bool(self._playback_file)) self.start_capture() def start_recording(self, filename): """ Start a new recording. Does nothing if we're currently playing a recording. """ if self._playback_file: return self.stop_recording() self._recording_file = open(filename, 'wb') self._dut.set_recording_output(self._recording_file) self._dut.inject_recording_state(self._state.to_json_object()) def stop_recording(self): """ Stop recording or do nothing if not currently recording. """ if not self._recording_file: return self._dut.set_recording_output(None) self._recording_file.close() self._recording_file = None def start_csv_export(self, filename): """ Start exporting datainto CSV file """ self._csv_file = open(filename, 'wb') self._csv_file.write('data,mode,fstart,fstop,size,timestamp\n') self._export_csv = True def stop_csv_export(self): """ Stop exporting data into CSV file """ self._csv_file.close() self._export_csv = False def _export_csv_file(self, mode, fstart, fstop, data): """ Save data to csv file """ time = datetime.isoformat(datetime.utcnow()) + 'Z' self._csv_file.write(',%s,%0.2f,%0.2f,%d,%s\n' % (mode, fstart, fstop, len(data), time)) for d in data: self._csv_file.write('%0.2f\n' % d) def _apply_pending_user_xrange(self): if self._pending_user_xrange: self._applying_user_xrange = True start, stop = self._pending_user_xrange self._pending_user_xrange = None self.apply_settings(center=int( (start + stop) / 2.0 / self._dut.properties.TUNING_RESOLUTION) * self._dut.properties.TUNING_RESOLUTION, span=stop - start) else: self._applying_user_xrange = False def read_block(self): if not (self._plot_options['cont_cap_mode'] or self._single_capture): return self._apply_pending_user_xrange() device_set = dict(self._state.device_settings) device_set['decimation'] = self._state.decimation device_set['fshift'] = self._state.fshift device_set['rfe_mode'] = self._state.rfe_mode() device_set['freq'] = self._state.center self._capture_device.configure_device(device_set) self._capture_device.capture_time_domain( self._state.mode, self._state.center, self._state.rbw, force_change=self.was_sweeping) self.was_sweeping = False self._single_capture = False def read_sweep(self): if not (self._plot_options['cont_cap_mode'] or self._single_capture): return self._apply_pending_user_xrange() device_set = dict(self._state.device_settings) # device_set.pop('pll_reference') device_set.pop('iq_output_path') device_set.pop('trigger') self._dut.pll_reference(device_set['pll_reference']) device_set.pop('pll_reference') self._sweep_device.capture_power_spectrum( self._state.center - self._state.span / 2.0, self._state.center + self._state.span / 2.0, self._state.rbw, device_set, mode=self._state.rfe_mode()) self.was_sweeping = True self._single_capture = False def start_capture(self, single=False): self._single_capture = single if self._playback_file: self.schedule_playback() elif self._state.sweeping(): self.read_sweep() else: self.read_block() def schedule_playback(self): if self._single_capture: self._playback_started = False if not self._playback_started: QtCore.QTimer.singleShot(0, self._playback_step) self._playback_started = True def _playback_step(self, single=False): if not (self._plot_options['cont_cap_mode'] or self._single_capture): return if not self._playback_file: self._playback_started = False return QtCore.QTimer.singleShot(PLAYBACK_STEP_MSEC, self._playback_step) while True: pkt = self._playback_vrt() if pkt.is_context_packet(): if 'speca' in pkt.fields: self._playback_sweep_data = None state_json = pkt.fields['speca'] self._apply_complete_settings(state_json, playback=True) else: self._playback_context.update(pkt.fields) continue if self._state.sweeping(): if not self._playback_sweep_step(pkt): continue self._single_capture = False return break usable_bins = compute_usable_bins(self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, self._state.fshift) usable_bins, fstart, fstop = adjust_usable_fstart_fstop( self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, self._state.center, pkt.spec_inv, usable_bins) pow_data = compute_fft(self._dut, pkt, self._playback_context, **self._dsp_options) if not self._options.get('show_attenuated_edges'): pow_data, usable_bins, fstart, fstop = ( trim_to_usable_fstart_fstop(pow_data, usable_bins, fstart, fstop)) if self._export_csv: self._export_csv_file(self._state.rfe_mode(), fstart, fstop, pow_data) self.capture_receive.emit(self._state, fstart, fstop, pkt, pow_data, usable_bins, None) def _playback_sweep_start(self): """ ready a new playback sweep data array """ nans = np.ones(int(self._state.span / self._state.rbw)) * np.nan self._playback_sweep_data = nans def _playback_sweep_step(self, pkt): """ process one data packet from a recorded sweep and plot collected data after receiving complete sweep. returns True if data was plotted on this step. """ if self._playback_sweep_data is None: self._playback_sweep_start() last_center = None else: last_center = self._playback_sweep_last_center sweep_start = float(self._state.center - self._state.span / 2) sweep_stop = float(self._state.center + self._state.span / 2) step_center = self._playback_context['rffreq'] updated_plot = False if last_center is not None and last_center >= step_center: # starting a new sweep, plot the data we have self.capture_receive.emit(self._state, sweep_start, sweep_stop, None, self._playback_sweep_data, None, None) updated_plot = True self._playback_sweep_start() self._playback_sweep_last_center = step_center usable_bins = compute_usable_bins(self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, self._state.fshift) usable_bins, fstart, fstop = adjust_usable_fstart_fstop( self._dut.properties, self._state.rfe_mode(), len(pkt.data), self._state.decimation, step_center, pkt.spec_inv, usable_bins) pow_data = compute_fft(self._dut, pkt, self._playback_context, **self._dsp_options) pow_data, usable_bins, fstart, fstop = (trim_to_usable_fstart_fstop( pow_data, usable_bins, fstart, fstop)) clip_left = max(sweep_start, fstart) clip_right = min(sweep_stop, fstop) sweep_points = len(self._playback_sweep_data) point_left = int((clip_left - sweep_start) * sweep_points / (sweep_stop - sweep_start)) point_right = int((clip_right - sweep_start) * sweep_points / (sweep_stop - sweep_start)) xvalues = np.linspace(clip_left, clip_right, point_right - point_left) if point_left >= point_right: logger.info('received sweep step outside sweep: %r, %r' % ((fstart, fstop), (sweep_start, sweep_stop))) else: self._playback_sweep_data[point_left:point_right] = np.interp( xvalues, np.linspace(fstart, fstop, len(pow_data)), pow_data) return updated_plot def _playback_vrt(self, auto_rewind=True): """ Return the next VRT packet in the playback file """ reader = vrt_packet_reader(self._playback_file.read) data = None try: while True: data = reader.send(data) except StopIteration: pass except ValueError: return None if data == '' and auto_rewind: self._playback_file.seek(0) data = self._playback_vrt(auto_rewind=False) return None if data == '' else data def process_capture(self, fstart, fstop, data): # store usable bins before next call to capture_time_domain usable_bins = list(self._capture_device.usable_bins) # only read data if WSA digitizer is used if 'DIGITIZER' in self._state.device_settings['iq_output_path']: if self._state.sweeping(): self.read_sweep() return self.read_block() if 'reflevel' in data['context_pkt']: self._ref_level = data['context_pkt']['reflevel'] pow_data = compute_fft(self._dut, data['data_pkt'], data['context_pkt'], ref=self._ref_level, **self._dsp_options) if not self._options.get('show_attenuated_edges'): pow_data, usable_bins, fstart, fstop = ( trim_to_usable_fstart_fstop(pow_data, usable_bins, fstart, fstop)) #FIXME: Find out why there is a case where pow_data may be empty if pow_data.any(): if self._plot_options.get('reference_offset_value'): pow_data += self._plot_options['reference_offset_value'] if self._export_csv: self._export_csv_file(self._state.rfe_mode(), fstart, fstop, pow_data) self.capture_receive.emit(self._state, fstart, fstop, data['data_pkt'], pow_data, usable_bins, None) def process_sweep(self, fstart, fstop, data): sweep_segments = list(self._sweep_device.sweep_segments) if not self._state.sweeping(): self.read_block() return self.read_sweep() self.pow_data = data self.iq_data = None if not self._options.get('show_sweep_steps'): sweep_segments = None if self._plot_options.get('reference_offset_value'): self.pow_data += self._plot_options['reference_offset_value'] if self._export_csv: self._export_csv_file(self._state.rfe_mode(), fstart, fstop, self.pow_data) self.capture_receive.emit(self._state, fstart, fstop, None, self.pow_data, None, sweep_segments) def _state_changed(self, state, changed): """ Emit signal and handle special cases where extra work is needed in response to a state change. """ # make sure resolution of center are the same as the device's tunning resolution center = float( np.round( state.center, -1 * int(np.log10(self._dut.properties.TUNING_RESOLUTION)))) state = SpecAState(state, center=center) if not state.sweeping(): # force span to correct value for the mode given if state.decimation > 1: span = (float(self._dut.properties.FULL_BW[state.rfe_mode()]) / state.decimation * self._dut.properties.DECIMATED_USABLE) else: span = self._dut.properties.USABLE_BW[state.rfe_mode()] state = SpecAState(state, span=span) changed = [x for x in changed if x != 'span'] if not self._state or span != self._state.span: changed.append('span') elif 'mode' in changed and 'span' not in changed: span = self._dut.properties.DEFAULT_SPECA_SPAN state = SpecAState(state, span=span) changed.append('span') self._state = state # start capture loop again when user switches output path # back to the internal digitizer XXX: very WSA5000-specific if 'device_settings.iq_output_path' in changed: if state.device_settings.get('iq_output_path') == 'DIGITIZER': self.start_capture() elif state.device_settings.get('iq_output_path') == 'CONNECTOR': if state.sweeping(): state.mode = self._dut.properties.RFE_MODES[0] if self._recording_file: self._dut.inject_recording_state(state.to_json_object()) self.state_change.emit(state, changed) def apply_device_settings(self, **kwargs): """ Apply device-specific settings and trigger a state change event. :param kwargs: keyword arguments of SpecAState.device_settings """ device_settings = dict(self._state.device_settings, **kwargs) state = SpecAState(self._state, device_settings=device_settings) if device_settings.get( 'iq_output_path') == 'CONNECTOR' or 'trigger' in kwargs: self._capture_device.configure_device(device_settings) changed = ['device_settings.%s' % s for s in kwargs] self._state_changed(state, changed) def apply_settings(self, **kwargs): """ Apply state settings and trigger a state change event. :param kwargs: keyword arguments of SpecAState attributes """ if self._state is None: logger.warn('apply_settings with _state == None: %r' % kwargs) return state = SpecAState(self._state, **kwargs) self._state_changed(state, kwargs.keys()) def _apply_complete_settings(self, state_json, playback): """ Apply state setting changes from a complete JSON object. Used for initial settings and applying settings from a recording. """ if self._state: old = self._state.to_json_object() old['playback'] = self._state.playback else: old = {} changed = [ key for key, value in state_json.iteritems() if old.get(key) != value ] if old.get('playback') != playback: changed.append('playback') if 'device_settings' in changed: changed.remove('device_settings') oset = old.get('device_settings', {}) dset = state_json['device_settings'] changed.extend([ 'device_settings.%s' % key for key, value in dset.iteritems() if oset.get(key) != value ]) state = SpecAState.from_json_object(state_json, playback) self._state_changed(state, changed) def apply_options(self, **kwargs): """ Apply menu options and signal the change :param kwargs: keyword arguments of the dsp options """ self._options.update(kwargs) self.options_change.emit(dict(self._options), kwargs.keys()) for key, value in kwargs.iteritems(): if key.startswith('dsp.'): self._dsp_options[key[4:]] = value def apply_plot_options(self, **kwargs): """ Apply plot option changes and signal the change :param kwargs: keyword arguments of the plot options """ self._plot_options.update(kwargs) self.plot_change.emit(dict(self._plot_options), kwargs.keys()) def get_options(self): return dict(self._options) def enable_user_xrange_control(self, enable): self._user_xrange_control_enabled = enable if not enable: self._pending_user_xrange = None def user_xrange_changed(self, start, stop): if self._user_xrange_control_enabled: self._pending_user_xrange = start, stop def applying_user_xrange(self): return self._applying_user_xrange