def __init__(self, *args): """ Constructor """ QtWidgets.QApplication.__init__(self, *args) # create logger self.logger = logging.getLogger('ScopeOut.gui.ThreadedClient') self.logger.info("Threaded Client initialized") # save a reference to the app database. # all access to the database must occur in this thread. self.database = None self.db_session = None # start in single-channel acquisition mode by default. self.multi_channel_acquisition = False # Create widgets. self.acquisition_control = sw.AcquisitionControlWidget(None) self.plot = sw.WavePlotWidget() self.histogram = sw.HistogramPlotWidget() self.wave_options = sw.WaveOptionsTabWidget() self.wave_column = sw.WaveColumnWidget() self.histogram_options = sw.HistogramOptionsWidget() self.logger.info("All Widgets initialized") widgets = { 'column': self.wave_column, 'plot': self.plot, 'acqControl': self.acquisition_control, 'wave_options': self.wave_options, 'hist_options': self.histogram_options, 'hist': self.histogram } commands = {'end': self.close_event} # Create main window that holds widgets. self.main_window = sw.ScopeOutMainWindow(widgets, commands) # Connect the various signals that shuttle data between widgets/threads. self.connect_signals() # Show the GUI self.main_window.show() # Oscilloscope and scope finder self.scopes = [] self.active_scope = None self.scope_finder = ScopeFinder() # Thread timers self.check_scope_timer = threading.Timer(5.0, self.check_scope) self.find_scope_timer = threading.Timer(0.1, self.find_scope) self.find_scope_timer.start()
class ThreadedClient(QtWidgets.QApplication): """ Launches the GUI and handles I/O. GUI components reside within the body of the class itself. This client acquires and manipulates data from attached scopes and feeds it to the GUI. Various threads are created to carry out USB communication asynchronously. NOTES: Initially, the client is not connected to any scopes, and searches for them continuously. This occurs in the scopeFind thread. when a scope is found, this thread returns, and periodic scope checking begins in the scopeCheck thread. A loss of connection should disable the interface and initiate scopeFind again. Creation of the widgets that make up the actual interface is done in the constructor of this class. All Qt Signals that facilitate the interaction of the client with these widgets are connected in the connect_signals method. """ lock = threading.Lock() # Lock for scope resource stop_flag = threading.Event() # Event representing termination of program acquisition_stop_flag = threading.Event() # Event representing termination of continuous acquisition channel_set_flag = threading.Event() # Set when data channel has been successfully changed. wave_acquired_flag = threading.Event() # Set during continuous acquisition when a waveform has been acquired. continuous_flag = threading.Event() # Set while program is finding scopes continuously continuous_flag.set() status_change_signal = QtCore.pyqtSignal(str) # Signal sent to GUI waveform counter. scope_change_signal = QtCore.pyqtSignal(object) # Signal sent to change the active oscilloscope. new_wave_signal = QtCore.pyqtSignal(Waveform) wave_added_to_db_signal = QtCore.pyqtSignal(Waveform) def __init__(self, *args): """ Constructor """ QtWidgets.QApplication.__init__(self, *args) # create logger self.logger = logging.getLogger('ScopeOut.gui.ThreadedClient') self.logger.info("Threaded Client initialized") # save a reference to the app database. # all access to the database must occur in this thread. self.database = None self.db_session = None # start in single-channel acquisition mode by default. self.multi_channel_acquisition = False # Create widgets. self.acquisition_control = sw.AcquisitionControlWidget(None) self.plot = sw.WavePlotWidget() self.histogram = sw.HistogramPlotWidget() self.wave_options = sw.WaveOptionsTabWidget() self.wave_column = sw.WaveColumnWidget() self.histogram_options = sw.HistogramOptionsWidget() self.logger.info("All Widgets initialized") widgets = { 'column': self.wave_column, 'plot': self.plot, 'acqControl': self.acquisition_control, 'wave_options': self.wave_options, 'hist_options': self.histogram_options, 'hist': self.histogram } commands = {'end': self.close_event} # Create main window that holds widgets. self.main_window = sw.ScopeOutMainWindow(widgets, commands) # Connect the various signals that shuttle data between widgets/threads. self.connect_signals() # Show the GUI self.main_window.show() # Oscilloscope and scope finder self.scopes = [] self.active_scope = None self.scope_finder = ScopeFinder() # Thread timers self.check_scope_timer = threading.Timer(5.0, self.check_scope) self.find_scope_timer = threading.Timer(0.1, self.find_scope) self.find_scope_timer.start() # noinspection PyUnresolvedReferences def connect_signals(self): """ Connects signals from subwidgets to appropriate slots. """ # Client Signals self.status_change_signal.connect(self.main_window.status) self.scope_change_signal.connect(self.acquisition_control.set_active_oscilloscope) self.new_wave_signal.connect(self.plot_wave) self.new_wave_signal.connect(self.save_wave_to_db) self.new_wave_signal.connect(self.update_histogram) self.new_wave_signal.connect(self.histogram_options.update_properties) self.wave_added_to_db_signal.connect(self.wave_column.add_wave) # Acq Control Signals self.acquisition_control.acquire_button.clicked.connect(partial(self.acq_event, 'now')) self.acquisition_control.acquire_on_trigger_button.clicked.connect(partial(self.acq_event, 'trig')) self.acquisition_control.continuous_acquire_button.clicked.connect(partial(self.acq_event, 'cont')) self.acquisition_control.channel_combobox.currentIndexChanged.connect(self.set_channel) self.acquisition_control.autoset_button.clicked.connect(self.autoset_event) self.acquisition_control.stop_acquisition_button.clicked.connect(self.acquisition_stop_flag.set) self.acquisition_control.hold_plot_checkbox.toggled.connect(self.wave_column.set_plot_hold) # Main window Signals self.main_window.reset_action.triggered.connect(self.reset) self.main_window.reset_action.triggered.connect(self.wave_column.reset) self.main_window.save_action.triggered.connect(self.save_wave_to_disk) self.main_window.save_properties_action.triggered.connect(self.save_properties_to_disk) self.main_window.save_plot_action.triggered.connect(self.save_plot_to_disk) self.main_window.save_histogram_action.triggered.connect(self.save_histogram_to_disk) self.main_window.load_session_action.triggered.connect(self.load_database) self.main_window.save_settings_action.triggered.connect(self.save_configuration) self.main_window.show_plot_action.toggled.connect(self.plot.setEnabled) self.main_window.show_histogram_action.toggled.connect(self.histogram.setEnabled) # Wave Column Signals self.wave_column.wave_signal.connect(self.plot_wave) self.wave_column.save_signal.connect(self.save_wave_to_disk) self.wave_column.save_properties_signal.connect(self.save_properties_to_disk) self.wave_column.delete_signal.connect(self.delete_wave) self.wave_column.delete_signal.connect(self.update_histogram) # Plot signals self.plot.save_plot_action.triggered.connect(self.save_plot_to_disk) # Histogram signals self.histogram.save_histogram_action.triggered.connect(self.save_histogram_to_disk) # Histogram Options signals self.histogram_options.property_selector.currentIndexChanged.connect(self.update_histogram) self.histogram_options.bin_number_selector.valueChanged.connect(self.update_histogram) self.logger.info("Signals connected") def save_wave_to_db(self, wave): """ Save a wave and its data in the database. :param wave: a Waveform, with its data contained in the x_list and y_list attributes. :return: """ self.db_session.add(wave) try: self.db_session.commit() self.logger.info("Saved waveform #" + str(wave.id) + " to the database") self.wave_added_to_db_signal.emit(wave) data = zip(wave.x_list, wave.y_list) self.database.bulk_insert_data_points(data, wave.id) except Exception as e: self.logger.error(e) self.db_session.rollback() def plot_wave(self, wave): """ Send a wave to the plotting widget. :param self: :param wave: a Waveform, with its data contained in the x_list and y_list attributes. """ if self.plot.isEnabled(): self.plot.show_plot(wave, self.acquisition_control.plot_held, self.acquisition_control.show_peak_window) def update_histogram(self): """ Update the histogram widget if the app is in histogram mode. """ if self.histogram.isEnabled(): wave_property = self.histogram_options.property_selector.currentText().lower().replace(' ', '_') if wave_property: histogram_list = [val for (val,) in self.db_session.query(getattr(Waveform, wave_property)).all()] self.histogram.show_histogram(histogram_list, self.histogram_options.bin_number_selector.value()) self.histogram.histogram.set_title(wave_property) def acq_event(self, mode): """ Executed to collect waveform data from scope. Parameters: :mode: A string defining the mode of acquisition: {'now' | 'trig' | 'cont'} """ def process_wave(wave): """ Extract wave and data from tuple generated by oscilloscope. Run desired calculations on acquired wave and display plots. Parameters: :wave_tuple: a tuple containing a Waveform, a list of x values, and a list of y values. """ try: assert type(wave) is Waveform if wave.error is not None: self.logger.error("Wave error: %s", wave.error) self.update_status(wave.error) return wave.detect_peak_and_integrate( self.wave_options.peak_detection_mode, self.wave_options.peak_detection_parameters) self.logger.info("Successfully acquired waveform from %s", wave.data_channel) self.update_status('Waveform acquired on ' + wave.data_channel) except Exception as e: self.update_status('Error occurred during wave processing. Check log for details.') self.logger.error(e) finally: self.new_wave_signal.emit(wave) def immediate_acquisition_thread(): """ Contains instructions for acquiring and storing waveforms ASAP. self.multiAcq serves as the flag to initiate multi-channel acquisition. """ self.channel_set_flag.clear() if self.active_scope is not None: self.update_status('Acquiring data...') if not self.multi_channel_acquisition: self.logger.info("Single channel acquisition") try: self.lock.acquire() self.active_scope.make_waveform() wave = self.active_scope.next_waveform except Exception as e: self.logger.error(e) wave = None finally: if self.lock.locked(): self.lock.release() if wave is not None and (not self.stop_flag.isSet()): process_wave(wave) else: self.update_status('Error on Waveform Acquisition') else: self.logger.info("Multichannel acquisition") self.plot.plot.reset_plot() for i in range(0, self.active_scope.numChannels): try: self.logger.info("Acquiring data from channel %d", i + 1) self.set_channel(i) self.channel_set_flag.wait() self.lock.acquire() self.active_scope.make_waveform() self.lock.release() wave = self.active_scope.next_waveform except Exception as e: self.logger.error(e) wave = None finally: if self.lock.locked(): self.lock.release() if wave is not None and (not self.stop_flag.isSet()): process_wave(wave) else: self.update_status('Error on Waveform Acquisition') self.update_status('Acquired all active channels.') self.multi_channel_acquisition = True self.main_window.update() enable_buttons(True) def acquire_on_trig_thread(): """ Waits for the scope to trigger, then acquires and stores waveforms in the same way as immAcq. """ self.lock.acquire() trigger_state = self.active_scope.getTriggerStatus() while trigger_state != 'TRIGGER' and not self.stop_flag.isSet() and not self.acquisition_stop_flag.isSet(): trigger_state = self.active_scope.getTriggerStatus() if not self.stop_flag.isSet() and not self.acquisition_stop_flag.isSet(): try: self.active_scope.make_waveform() wave = self.active_scope.next_waveform except AttributeError: wave = None finally: self.wave_acquired_flag.set() if self.lock.locked(): self.lock.release() if not self.stop_flag.isSet() and not self.acquisition_stop_flag.isSet(): if wave is not None: process_wave(wave) elif self.acquisition_stop_flag.isSet(): self.update_status('Acquisition terminated') self.logger.info('Acquisition on trigger terminated.') if mode == 'trig': self.acquisition_stop_flag.clear() self.wave_acquired_flag.set() # have to set this for continuous acq to halt properly if self.lock.locked(): self.lock.release() else: self.update_status('Error on Waveform Acquisition') self.logger.info('Error on Waveform Acquisition.') if mode == 'trig': enable_buttons(True) def continuous_acquisition_thread(): """ Continually runs trigAcqThread until the stop signal is received. """ while not self.stop_flag.isSet() and not self.acquisition_stop_flag.isSet(): self.wave_acquired_flag.wait() if not self.acquisition_stop_flag.isSet(): acqThread = threading.Thread(target=acquire_on_trig_thread) acqThread.start() self.wave_acquired_flag.clear() self.acquisition_stop_flag.clear() self.update_status("Continuous Acquisiton Halted.") enable_buttons(True) self.check_scope_timer = threading.Timer(5.0, self.check_scope) self.check_scope_timer.start() def enable_buttons(bool): """ Disables/enables buttons that should not be active during acquisition. Parameters: :bool: True to enable buttons, false to disable. """ self.acquisition_control.enable_buttons(bool) self.acquisition_stop_flag.clear() if not self.database: self.database = Database() self.db_session = self.database.session() if mode == 'now': # Single, Immediate acquisition enable_buttons(False) self.logger.info("Immediate acquisition Event") acquisition_thread = threading.Thread(target=immediate_acquisition_thread) acquisition_thread.start() elif mode == 'trig': # Acquire on trigger enable_buttons(False) self.update_status("Waiting for trigger...") self.logger.info("Acquisition on trigger event") acquisition_thread = threading.Thread(target=acquire_on_trig_thread) acquisition_thread.start() elif mode == 'cont': # Continuous Acquisition enable_buttons(False) self.check_scope_timer.cancel() self.logger.info('Continuous Acquisition Event') self.update_status("Acquiring Continuously...") self.wave_acquired_flag.set() acquisition_thread = threading.Thread(target=continuous_acquisition_thread) acquisition_thread.start() def find_scope(self): """ Continually checks for connected scopes, until one is found, then begins periodic checking. """ if not self.stop_flag.is_set(): self.scopes = self.scope_finder.refresh().get_scopes() while not self.scopes: # Check for scopes and connect if possible if self.stop_flag.isSet(): self.scopes = [] break self.lock.acquire() self.scopes = self.scope_finder.refresh().get_scopes() self.lock.release() if not self.stop_flag.isSet(): # Scope Found! self.active_scope = self.scopes[0] self.logger.info("Set active scope to %s", str(self.active_scope)) self.scope_change_signal.emit(self.active_scope) self.update_status('Found ' + str(self.active_scope)) self.main_window.setEnabled(True) self.check_scope_timer = threading.Timer(5.0, self.check_scope) self.check_scope_timer.start() def check_scope(self): """ Periodically confirms that scopes are still connected. """ if not self.stop_flag.isSet(): self.lock.acquire() connected = self.scope_finder.check_scope(0) if self.lock.locked(): self.lock.release() if not connected: self.scopes = [] self.logger.info("Lost Connection to Oscilloscope(s)") self.update_status("Lost Connection to Oscilloscope(s)") self.main_window.setEnabled(False) self.check_scope_timer.cancel() self.find_scope_timer = threading.Timer(0.1, self.find_scope) self.find_scope_timer.start() elif not self.stop_flag.isSet(): self.check_scope_timer = threading.Timer(5.0, self.check_scope) self.check_scope_timer.start() def close_event(self): """ Executed on app close. """ self.logger.info('Closing ScopeOut. \n') self.stop_flag.set() self.continuous_flag.clear() self.check_scope_timer.cancel() self.quit() def reset(self): """ Called to reset waveform and plot. """ if self.plot.isEnabled(): self.plot.plot.reset_plot() if self.histogram.isEnabled(): self.histogram.reset() self.wave_column.reset() self.histogram_options.reset() self.update_status('Data Reset.') self.db_session = None self.database = None def set_channel(self, channel): """ Set data channel of active scope. Parameters: :channel: desired data channel """ channels = self.acquisition_control.data_channels def channel_thread(): try: self.lock.acquire() if self.acquisition_control.scope.setDataChannel(channels[channel]): self.logger.info('Successfully set data channel %s', channels[channel]) self.update_status('Data channel set to ' + channels[channel]) else: self.logger.debug('Failed to set data channel set to ' + channels[channel]) self.update_status('Failed to set data channel ' + channels[channel]) except Exception as e: self.logger.error(e) finally: try: self.channel_set_flag.set() if self.lock.locked(): self.lock.release() except Exception as e: self.logger.error(e) self.channel_set_flag.clear() self.logger.info('Attempting to set data channel %s', channels[channel]) self.acquisition_control.continuous_acquire_button.setEnabled(True) self.acquisition_control.acquire_on_trigger_button.setEnabled(True) if channel in range(0, self.acquisition_control.scope.numChannels): self.multi_channel_acquisition = False set_channel_thread = threading.Thread(target=channel_thread) set_channel_thread.start() elif channels[channel] == 'All': self.logger.info("Selected all data channels") self.update_status("Selected all data channels") self.multi_channel_acquisition = True elif channels[channel] == 'Math': self.logger.info("selected Math data channel") self.update_status("selected Math data channel") self.multi_channel_acquisition = False set_channel_thread = threading.Thread(target=channel_thread) set_channel_thread.start() # No triggering in math mode self.acquisition_control.continuous_acquire_button.setEnabled(False) self.acquisition_control.acquire_on_trigger_button.setEnabled(False) self.acquisition_control.stop_acquisition_button.setEnabled(False) def save_wave_to_disk(self, waveform=None): """ Called in order to save in-memory waveforms to disk. Parameters: :wave: a particular wave to save, if none is passed then all waves in memory are saved. """ if waveform: try: wave_directory = Config.get('Export', 'waveform_dir') if not os.path.exists(wave_directory): os.makedirs(wave_directory) day_directory = os.path.join(wave_directory, date.today().isoformat()) if not os.path.exists(day_directory): os.makedirs(day_directory) default_file = 'Capture' + datetime.now().strftime('%m-%d-%H-%M-%S') + '.csv' default_file = os.path.join(day_directory, default_file).replace('\\', '/') file_name = QtWidgets.QFileDialog.getSaveFileName(self.main_window, 'Save As', default_file)[0] with WaveformCsvFile(waveform, file_name) as file: file.write() self.logger.info('Waveform saved to ' + file_name) self.update_status('Waveform saved to ' + file_name) except Exception as e: self.logger.error(e) else: wave_count = self.db_session.query(Waveform).count() if wave_count: try: wave_directory = Config.get('Export', 'waveform_dir') if not os.path.exists(wave_directory): os.makedirs(wave_directory) day_directory = os.path.join(wave_directory, date.today().isoformat()) if not os.path.exists(day_directory): os.makedirs(day_directory) default_file = 'Capture' + datetime.now().strftime('%m-%d-%H-%M-%S') + '.csv' default_file = os.path.join(day_directory, default_file).replace('\\', '/') file_name = QtWidgets.QFileDialog.getSaveFileName(self.main_window, 'Save As', default_file)[0] with WaveformCsvFile(self.db_session.query(Waveform), file_name) as file: file.write() self.logger.info("%d waveforms saved to %s", wave_count, file_name) self.update_status('Waveforms saved to ' + file_name) except Exception as e: self.logger.error(e) else: self.update_status('No Waveforms to Save') def save_properties_to_disk(self, waveform=None): """ Save the values of any number of a waveform's properties to disk. Parameters: :waveform: a Waveform, the properties of which are to be saved. If none is present, the properties of all waveforms in memory are saved. """ def make_properties_file(): wave_directory = Config.get('Export', 'waveform_dir') if not os.path.exists(wave_directory): os.makedirs(wave_directory) day_directory = os.path.join(wave_directory, date.today().isoformat()) if not os.path.exists(day_directory): os.makedirs(day_directory) default_file = 'Properties' + datetime.now().strftime('%m-%d-%H-%M-%S') + '.csv' default_file = os.path.join(day_directory, default_file).replace('\\', '/') file_name = QtWidgets.QFileDialog.getSaveFileName(self.main_window, 'Save As', default_file)[0] return file_name def write_properties(file_name, waves, fields): """ Writes the selected properties of a list of Waveforms to a .csv file. Parameters: :file_name: the path to the output file. :waves: the list of Waveforms to be processed. :fields: an array containing the names of the selected properties. """ try: with WaveformCsvFile(waves, file_name) as file: file.write_properties(fields) self.logger.info('Waveform properties saved to ' + file_name) self.update_status('Waveform properties saved to ' + file_name) except Exception as e: self.logger.error(e) if waveform: properties_dialog = sw.SelectPropertiesDialog(waveform) properties_dialog.property_signal.connect(partial(write_properties, make_properties_file(), [waveform])) properties_dialog.exec() else: if self.db_session: wave_list = self.db_session.query(Waveform).all() properties_dialog = sw.SelectPropertiesDialog(wave_list[0]) properties_dialog.property_signal.connect(partial(write_properties, make_properties_file(), wave_list)) properties_dialog.exec() else: self.update_status('No waveforms to save.') def save_plot_to_disk(self): """ Save the currently displayed plot to disk. """ plot_directory = Config.get('Export', 'plot_dir') if not os.path.exists(plot_directory): os.makedirs(plot_directory) day_directory = os.path.join(plot_directory, date.today().isoformat()) if not os.path.exists(day_directory): os.makedirs(day_directory) default_file = 'Plot' + datetime.now().strftime('%m-%d-%H-%M-%S') + '.png' default_file = os.path.join(day_directory, default_file).replace('\\', '/') file_name = QtWidgets.QFileDialog.getSaveFileName(self.main_window, 'Save As', default_file)[0] if self.plot.save_plot(file_name): self.update_status("Plot saved successfully") else: self.update_status("Error occurred while saving plot. Check log for details.") def save_histogram_to_disk(self): """ Save an image of the active histogram. :return: """ plot_directory = Config.get('Export', 'plot_dir') if not os.path.exists(plot_directory): os.makedirs(plot_directory) day_directory = os.path.join(plot_directory, date.today().isoformat()) if not os.path.exists(day_directory): os.makedirs(day_directory) default_file = 'Histogram' + datetime.now().strftime('%m-%d-%H-%M-%S') + '.png' default_file = os.path.join(day_directory, default_file).replace('\\', '/') file_name = QtWidgets.QFileDialog.getSaveFileName(self.main_window, 'Save As', default_file)[0] if self.histogram.save_plot(file_name): self.update_status("Plot saved successfully") else: self.update_status("Error occurred while saving plot. Check log for details.") def update_status(self, message): """ Print a message to the statusbar. Parameters: :message: The string to be printed. """ self.status_change_signal.emit(message) def autoset_event(self): """ Called when a scope autoset is requested. """ def do_autoset(): """ Thread to execute the autoset. """ self.lock.acquire() self.acquisition_control.scope.autoSet() self.lock.release() self.logger.info("Starting autoSet") self.update_status("Executing Auto-set. Ensure process is complete before continuing.") threading.Thread(target=do_autoset, name='AutoSetThread').start() def delete_wave(self, wave): """ Removes the given waveform from the database. :param wave: the waveform to delete. """ try: self.db_session.delete(wave) self.db_session.commit() except Exception as e: self.logger.error(e) self.db_session.rollback() def load_database(self): """ Connect to an old database file, and load its waves into memory if it is valid. """ try: default_file = Config.get('Database', 'database_dir') database_path = QtWidgets.QFileDialog.getOpenFileName(self.main_window, 'Open', default_file)[0] # Occurs if user hits cancel if database_path is '': return self.update_status('Loading waves from ' + database_path) self.logger.info('Disconnecting from database') # clear old session if self.db_session: self.db_session.close() self.db_session = None # reset GUI self.reset() self.update_status('Loading waves from ' + database_path) self.logger.info('Loading waves from ' + database_path) # make new connection self.database = Database(database_path) if self.database.is_setup: self.db_session = self.database.session() # get waves loaded_waves = self.db_session.query(Waveform).all() if not len(loaded_waves): raise RuntimeError('Database contained no waves.') # display waves to user. [self.wave_column.add_wave(wave) for wave in loaded_waves] try: self.plot_wave(loaded_waves[-1]) except ValueError as e: self.logger.info(e) except Exception as e: self.logger.error(e) self.histogram_options.update_properties(loaded_waves[-1]) self.update_histogram() self.update_status('Wave loading complete.') except Exception as e: self.logger.error(e) self.update_status('Failed to load waves from ' + database_path) def save_configuration(self): """ Save the current settings to the configuration file. :return: """ self.logger.info('Saving configuration') settings = [('Peak Detection', 'detection_method', self.wave_options.peak_detection_mode), ('Peak Detection', 'smart_start_threshold', self.wave_options.smart.start_threshold_input.value()), ('Peak Detection', 'smart_end_threshold', self.wave_options.smart.end_threshold_input.value()), ('Peak Detection', 'fixed_start_time', self.wave_options.fixed.start_time_input.value()), ('Peak Detection', 'fixed_start_unit', self.wave_options.fixed.start_time_unit_combobox.currentText()), ('Peak Detection', 'fixed_width_time', self.wave_options.fixed.peak_width_input.value()), ('Peak Detection', 'fixed_width_unit', self.wave_options.fixed.peak_width_unit_combobox.currentText()), ('Peak Detection', 'hybrid_start_threshold', self.wave_options.hybrid.start_threshold_input.value()), ('Peak Detection', 'hybrid_width_time', self.wave_options.hybrid.peak_width_input.value()), ('Peak Detection', 'hybrid_width_unit', self.wave_options.hybrid.peak_width_units.currentText()), ('Peak Detection', 'voltage_threshold_start_edge', self.wave_options.voltage_threshold.start_above_below_combobox.currentText()), ('Peak Detection', 'voltage_threshold_start_value', self.wave_options.voltage_threshold.start_voltage_spinbox.value()), ('Peak Detection', 'voltage_threshold_start_unit', self.wave_options.voltage_threshold.start_voltage_unit_combobox.currentText()), ('Peak Detection', 'voltage_threshold_end_edge', self.wave_options.voltage_threshold.end_above_below_combobox.currentText()), ('Peak Detection', 'voltage_threshold_end_value', self.wave_options.voltage_threshold.end_voltage_spinbox.value()), ('Peak Detection', 'voltage_threshold_end_unit', self.wave_options.voltage_threshold.end_voltage_unit_combobox.currentText()), ('Histogram', 'default_property', self.histogram_options.property_selector.currentText().lower().replace(' ', '_')), ('Histogram', 'number_of_bins', self.histogram_options.bin_number_selector.value()), ('Acquisition Control', 'hold_plot', self.acquisition_control.plot_held), ('Acquisition Control', 'show_peak', self.acquisition_control.show_peak_window), ('Acquisition Control', 'data_channel', self.acquisition_control.channel_combobox.currentText()), ('View', 'show_plot', self.main_window.show_plot_action.isChecked()), ('View', 'show_histogram', self.main_window.save_histogram_action.isChecked())] Config.set_multiple(settings) self.update_status('Configuration saved.')