diff --git a/docs/changelog.md b/docs/changelog.md index d39e89f47..85ad2ebc2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -27,7 +27,8 @@ None - Fixed syntax error in `qudi.util.fit_models.lorentzian.LorentzianLinear` fit model ### New Features -None +- Added "Dump all status variables" button to manually dump status variables without restarting qudi +- Added settings to configure automatic dumping of status variables during qudi runtime ### Other - Improved documentation [`getting_started.md`](getting_started.md) diff --git a/docs/design_concepts/measurement_modules.md b/docs/design_concepts/measurement_modules.md index 6c4b0245b..9a1b9cb58 100644 --- a/docs/design_concepts/measurement_modules.md +++ b/docs/design_concepts/measurement_modules.md @@ -8,44 +8,44 @@ title: qudi-core --- # Qudi Measurement Modules -Qudi user applications revolve around "qudi measurement modules", namely hardware, logic and +Qudi user applications revolve around "qudi measurement modules", namely hardware, logic and graphical user interface (GUI) modules. > **⚠ WARNING:** -> -> The term "module" is a bit misleading in this context since it usually refers to a `.py` file -> containing one or multiple definitions and/or statements. +> +> The term "module" is a bit misleading in this context since it usually refers to a `.py` file +> containing one or multiple definitions and/or statements. > In our case a "qudi/measurement module" refers to a non-abstract subclass of `qudi.core.Base` -> (`LogicBase` and `GuiBase` are subclasses of `Base` as well) that is declared in a Python module -> usually located at `qudi/[hardware|logic|gui]/` +> (`LogicBase` and `GuiBase` are subclasses of `Base` as well) that is declared in a Python module +> usually located at `qudi/[hardware|logic|gui]/` > (exception: [qudi extension](../404.md) directories). -> +> > Historical reasons... you know... -At the heart of each qudi measurement application stands a logic module. Logic modules -are the "brains" of each application and are responsible for control, configuration, monitoring, +At the heart of each qudi measurement application stands a logic module. Logic modules +are the "brains" of each application and are responsible for control, configuration, monitoring, data analysis and instrument orchestration to name only a few common tasks. -If an application requires hardware instrumentation you also need qudi hardware modules to provide -abstracted hardware interfaces to logic modules. If you want to know more about hardware +If an application requires hardware instrumentation you also need qudi hardware modules to provide +abstracted hardware interfaces to logic modules. If you want to know more about hardware abstraction in qudi, please read the [hardware interface documentation](hardware_interface.md). -Having a logic module and possibly a hardware module is the bare minimum of a qudi measurement -application. The logic module provides a set of methods and properties that can be considered an -API for the specific measurement application, i.e. it provides full control over a certain type of -measurement. You can now control the logic via an interactive IPython session, e.g. a jupyter +Having a logic module and possibly a hardware module is the bare minimum of a qudi measurement +application. The logic module provides a set of methods and properties that can be considered an +API for the specific measurement application, i.e. it provides full control over a certain type of +measurement. You can now control the logic via an interactive IPython session, e.g. a jupyter notebook using the qudi kernel or the qudi manager GUI console. -While a logic module is in principle enough to control a measurement application, you may want to -provide a more user-friendly interface. This is where qudi GUI modules come into play. -Each qudi GUI module must assemble and show a Qt `QMainWindow` instance that can use everything -[Qt for Python (PySide2)](https://doc.qt.io/qtforpython/) has to offer. -They must connect to at least one logic module in order to provide a graphical interface for it. +While a logic module is in principle enough to control a measurement application, you may want to +provide a more user-friendly interface. This is where qudi GUI modules come into play. +Each qudi GUI module must assemble and show a Qt `QMainWindow` instance that can use everything +[Qt for Python (PySide2)](https://doc.qt.io/qtforpython/) has to offer. +They must connect to at least one logic module in order to provide a graphical interface for it. > **⚠ WARNING:** -> -> GUI modules MUST NOT contain any "intelligent" code. -> In other words they MUST NOT facilitate any +> +> GUI modules MUST NOT contain any "intelligent" code. +> In other words they MUST NOT facilitate any > functionality that could not be achieved by using the bare logic module(s) it is connecting to. @@ -54,100 +54,100 @@ All qudi measurement module classes are descendants of the abstract `qudi.core.m which provides the following features: #### 1. Logging -Easy access to the qudi logging facility via their own logger object property `log`. +Easy access to the qudi logging facility via their own logger object property `log`. It can be used with all major log levels: ```python .log.debug('debug message') # ignored by logger unless running in debug mode .log.info('info message') -.log.warn('warning message') +.log.warn('warning message') .log.error('error message') # user prompt unless running in headless mode -.log.critical('critical message') # user prompt unless running in headless mode and +.log.critical('critical message') # user prompt unless running in headless mode and # qudi shutdown attempt ``` #### 2. Finite State Machine -A very simple finite state machine (FSM) that can be accessed via property `module_state`: +A very simple finite state machine (FSM) that can be accessed via property `module_state`: -![FSM state diagram](../images/module_fsm_diagram.svg) +![FSM state diagram](../images/module_fsm_diagram.svg) The qudi [module manager](../404.md) uses this FSM to control and monitor qudi measurement modules. -It can also be accessed by other entities in order to check if the measurement module is in `idle` -or `locked` state (i.e. if the module is busy). +It can also be accessed by other entities in order to check if the measurement module is in `idle` +or `locked` state (i.e. if the module is busy). The name of the current state can be polled by calling the FSM object: `.module_state()`. #### 3. Thread Management -Logic modules (subclass of `qudi.core.module.LogicBase`) will run by default in their own thread. -Hardware and GUI modules will not live in their own threads by default. This is why it is so -important to facilitate [inter-module communication](../404.md) mainly with Qt Signals in order +Logic modules (subclass of `qudi.core.module.LogicBase`) will run by default in their own thread. +Hardware and GUI modules will not live in their own threads by default. This is why it is so +important to facilitate [inter-module communication](../404.md) mainly with Qt Signals in order to automatically respect thread affinity. -You can access the Qt `QThread` object that represents the native thread of the module via the +You can access the Qt `QThread` object that represents the native thread of the module via the property `module_thread`. -To simply check if a module is running its own thread use the `bool` -property `is_module_threaded`. +To simply check if a module is running its own thread use the `bool` +property `is_module_threaded`. -To manually alter thread affinity, you can explicitly declare `_threaded` in the class body of your -measurement module implementation to be `True` or `False` to let it run in its own thread or in +To manually alter thread affinity, you can explicitly declare `_threaded` in the class body of your +measurement module implementation to be `True` or `False` to let it run in its own thread or in the main thread, respectively, i.e.: ```python from qudi.core.module import Base class MyExampleModule(Base): """ Description goes here """ - + _threaded = True # or alternatively False ... ``` Spawning and joining threads is handled automatically by the qudi [thread manager](../404.md). #### 4. Balloon and Pop-Up Messaging -An easy way to notify the user with a message independent of the logging facility is provided via +An easy way to notify the user with a message independent of the logging facility is provided via the two utility methdos `_send_balloon_message` and `_send_pop_up_message`. -By providing a title and message string to these methods, the user will either see a balloon +By providing a title and message string to these methods, the user will either see a balloon message (if supported by the OS) or a pop-up message with an OK button to dismiss, respectively. For balloon messages you can additionally provide a timeout and a `QIcon` instance to customize -the display duration and appearance. -Of course pop-up messages will not work if qudi is running in headless mode. In that case the -message will be printed out. This is also the behaviour if balloon messages are not supported +the display duration and appearance. +Of course pop-up messages will not work if qudi is running in headless mode. In that case the +message will be printed out. This is also the behaviour if balloon messages are not supported by the OS. #### 5. Status Variables -Status variables (`qudi.core.module.StatusVar` members) are automatically dumped and loaded upon -deactivation and activation of the measurement module, respectively. +Status variables (`qudi.core.module.StatusVar` members) are automatically dumped and loaded upon +deactivation and activation of the measurement module, respectively. -In case you want to manually issue a dump of status variables, a module can call -`_dump_status_variables`. +In case you want to manually issue a dump of status variables, a module can call +`dump_status_variables`. > **⚠ WARNING:** -> -> Please be aware that dumping status variables can potentially be slow depending on +> +> Please be aware that dumping status variables can potentially be slow depending on the type and size of the variables. So think carefully before using manual dumping. See also the [qudi status variable documentation](../404.md). #### 6. Static Configuration -Using qudi config options (`qudi.core.module.ConfigOption` members) one can facilitate static -configuration of your measurement modules. -Upon instantiation of a module, `ConfigOption` meta +Using qudi config options (`qudi.core.module.ConfigOption` members) one can facilitate static +configuration of your measurement modules. +Upon instantiation of a module, `ConfigOption` meta variables are automatically initialized from the corresponding part of the current qudi config. > **⚠ WARNING:** -> -> `ConfigOption` variables are only initialized once at the instantiation of the module and +> +> `ConfigOption` variables are only initialized once at the instantiation of the module and NOT each time the module is activated. See also the [qudi configuration option documentation](../404.md). #### 7. Measurement Module Interconnection -You can define other measurement modules that can be accessed via `Connector` meta object members. -The qudi module manager will automatically load and activate dependency modules according to the +You can define other measurement modules that can be accessed via `Connector` meta object members. +The qudi module manager will automatically load and activate dependency modules according to the configuration and connect them to the module upon activation. See also the [section further below](#inter-module-communication) for more info. - + #### 8. Meta Information Various read-only properties providing meta-information about the module: @@ -159,16 +159,16 @@ Various read-only properties providing meta-information about the module: | `module_default_data_dir` | The full path to the default module data directory. Can be overridden by module implementation. | #### 9. Access to qudi main instance -Each measurement module holds a (weak) reference to the [`qudi.core.application.Qudi`](../404.md) -singleton instance. -This object holds references to all running core facilities like the currently loaded -`Configuration`, the `ModuleManager`, `ThreadManager` and the `rpyc` servers for remote module +Each measurement module holds a (weak) reference to the [`qudi.core.application.Qudi`](../404.md) +singleton instance. +This object holds references to all running core facilities like the currently loaded +`Configuration`, the `ModuleManager`, `ThreadManager` and the `rpyc` servers for remote module and IPython kernel functionality. > **⚠ WARNING:** -> -> Designing a measurement module that needs to access the qudi application singleton is -generally considered bad practice. Unless you have a very specific and good reason to do so, +> +> Designing a measurement module that needs to access the qudi application singleton is +generally considered bad practice. Unless you have a very specific and good reason to do so, you should never use this object in your experiment toolchains. @@ -176,24 +176,24 @@ you should never use this object in your experiment toolchains. So, as you might have noticed the relationship of GUI, logic and hardware modules is hierarchical: - GUI modules control one or more logic modules but no other GUI or hardware modules - Logic modules control other logic modules and/or hardware modules but no GUI modules -- Hardware modules control no other qudi modules and are just providing an interface to a specific +- Hardware modules control no other qudi modules and are just providing an interface to a specific instrument The connection to another module is done by the `qudi.core.connector.Connector` meta object. These -connectors declare the dependency of a module on another module further down the hierarchy, i.e. it +connectors declare the dependency of a module on another module further down the hierarchy, i.e. it opens up a control flow path to another module. See the [qudi connectors documentation](connectors.md) for more details on how connectors work. -Generally the control flow between modules should be signal-driven according to the -[Qt signal-slot principle](https://doc.qt.io/qt-5/signalsandslots.html). +Generally the control flow between modules should be signal-driven according to the +[Qt signal-slot principle](https://doc.qt.io/qt-5/signalsandslots.html). -In the case of qudi this means a module should connect its own Qt signals to slots -(callback methods) in another module (unidirectional control flow) and connect signals from the -other module with its own slots (bidirectional control flow). -So, a modules can trigger the execution of a slot in another module. If both modules connected that -way are not running in the same thread all this will automatically happen asynchronously. -This is especially useful for GUI modules calling long-running logic methods/slots because they +In the case of qudi this means a module should connect its own Qt signals to slots +(callback methods) in another module (unidirectional control flow) and connect signals from the +other module with its own slots (bidirectional control flow). +So, a modules can trigger the execution of a slot in another module. If both modules connected that +way are not running in the same thread all this will automatically happen asynchronously. +This is especially useful for GUI modules calling long-running logic methods/slots because they would otherwise lock up and be unresponsive until the logic method has returned. A common example would be a GUI module triggering the start of a long-running logic method: @@ -205,19 +205,19 @@ from qudi.core.connector import Connector # GUI module declaration in e.g. qudi/gui/my_gui_module.py class MyGuiModule(Base): """ Description goes here """ - + # Qt signal triggering the start of the measurement - sigStartMeasurement = Signal() - + sigStartMeasurement = Signal() + # Connector to get a reference to the measurement logic module _logic_connector = Connector(interface='MyLogicModule', name='my_logic') ... - + def on_activate(self): self.sigStartMeasurement.connect(self._logic_connector().start_measurement) self._logic_connector().sigMeasurementFinished.connect(self._measurement_finished) - + def trigger_measurement_start(self): """ Will just emit the sigStartMeasurement signal """ self.sigStartMeasurement.emit() @@ -231,12 +231,12 @@ class MyGuiModule(Base): # Logic module declaration in e.g. qudi/logic/my_logic_module.py class MyLogicModule(LogicBase): """ Description goes here """ - + # Qt signal notifying all connected "listeners" about a finished measurement - sigMeasurementFinished = Signal() + sigMeasurementFinished = Signal() ... - + def start_measurement(self): """ API method to start a measurement """ # Actually perform your measurement here and emit notification signal upon finishing @@ -244,21 +244,21 @@ class MyLogicModule(LogicBase): ... ``` -In the above example, a GUI call to `trigger_measurement_start` will return immediately and cause -the logic module to asynchronously start the measurement by running `start_measurement`. -While the measurement is running in the logic thread, the GUI module stays responsive and can -perform other tasks. -As soon as the logic module has finished its measurement it will emit a signal causing all -connected slots to be called asynchronously in their respective threads. In our case this will +In the above example, a GUI call to `trigger_measurement_start` will return immediately and cause +the logic module to asynchronously start the measurement by running `start_measurement`. +While the measurement is running in the logic thread, the GUI module stays responsive and can +perform other tasks. +As soon as the logic module has finished its measurement it will emit a signal causing all +connected slots to be called asynchronously in their respective threads. In our case this will execute the `_measurement_finished` callback and print the message. -The same kind of control flow can be established between multiple logic modules, each running in +The same kind of control flow can be established between multiple logic modules, each running in its own thread. -There is an exception to this kind of control flow... hardware modules. -Hardware modules usually just provide a set of wrapper methods to control an instrument and are -typically controlled by logic modules that run in their own thread. So in most cases there is no -need to access hardware functionality asynchronously and the logic can thus simply access the +There is an exception to this kind of control flow... hardware modules. +Hardware modules usually just provide a set of wrapper methods to control an instrument and are +typically controlled by logic modules that run in their own thread. So in most cases there is no +need to access hardware functionality asynchronously and the logic can thus simply access the hardware directly via its connector (without signal/slot mechanics): ```python from qudi.core.module import LogicBase @@ -267,12 +267,12 @@ from qudi.core.connector import Connector class MyLogicModule(LogicBase): """ Description goes here """ - + # Connector to get a reference to the hardware module _hardware_connector = Connector(interface='MyHardwareInterface', name='my_hardware') ... - + def do_stuff_in_hardware(self): """ Will perform some task using the connected hardware """ self._hardware_connector().do_stuff() # direct method call, no signal/slot shenanigans diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index a8110280b..b74eca1ef 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -""" This module contains the +"""This module contains the Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this distribution and on @@ -49,6 +49,9 @@ class QudiMainGui(GuiBase): _console_font_size = StatusVar(name='console_font_size', default=10) _show_error_popups = StatusVar(name='show_error_popups', default=True) + signal_update_automatic_status_var_checkstate = QtCore.Signal(bool) + signal_update_automatic_status_var_interval = QtCore.Signal(int) + def __init__(self, *args, **kwargs): """Create an instance of the module. @@ -105,6 +108,7 @@ def on_activate(self): self._init_remote_modules_widget() self.reset_default_layout() + self.show() def on_deactivate(self): @@ -126,6 +130,10 @@ def _connect_signals(self): self.mw.action_open_configuration_editor.triggered.connect(self.new_configuration) self.mw.action_load_all_modules.triggered.connect( qudi_main.module_manager.start_all_modules) + self.mw.action_dump_status_variables.triggered.connect( + qudi_main.module_manager.dump_status_variables, QtCore.Qt.QueuedConnection + ) + self.mw.action_view_default.triggered.connect(self.reset_default_layout) # Connect signals from manager qudi_main.configuration.sigConfigChanged.connect(self.update_config_widget) @@ -143,6 +151,8 @@ def _connect_signals(self): qudi_main.module_manager.deactivate_module) self.mw.module_widget.sigCleanupModule.connect( qudi_main.module_manager.clear_module_app_data) + self.mw.module_widget.sigDumpStatusVarModule.connect( + qudi_main.module_manager.dump_module_status_var) def _disconnect_signals(self): qudi_main = self._qudi_main @@ -152,6 +162,7 @@ def _disconnect_signals(self): self.mw.action_reload_qudi.triggered.disconnect() self.mw.action_open_configuration_editor.triggered.disconnect() self.mw.action_load_all_modules.triggered.disconnect() + self.mw.action_dump_status_variables.triggered.disconnect() self.mw.action_view_default.triggered.disconnect() # Disconnect signals from manager qudi_main.configuration.sigConfigChanged.disconnect(self.update_config_widget) @@ -167,6 +178,7 @@ def _disconnect_signals(self): self.mw.module_widget.sigReloadModule.disconnect() self.mw.module_widget.sigDeactivateModule.disconnect() self.mw.module_widget.sigCleanupModule.disconnect() + self.mw.module_widget.sigDumpStatusVarModule.disconnect() get_signal_handler().sigRecordLogged.disconnect(self.handle_log_record) @@ -218,6 +230,7 @@ def reset_default_layout(self): self.mw.action_view_console.setChecked(self._has_console) self.mw.action_view_console.setVisible(self._has_console) + return def handle_log_record(self, entry): @@ -340,15 +353,18 @@ def update_configured_modules(self, modules=None): if modules is None: modules = self._qudi_main.module_manager self.mw.module_widget.update_modules(modules) + self.mw.settings_dialog.module_widget.update_modules(modules) @QtCore.Slot(str, str, str) def update_module_state(self, base, name, state): self.mw.module_widget.update_module_state(base, name, state) + self.mw.settings_dialog.module_widget.update_module_state(base, name, state) return @QtCore.Slot(str, str, bool) def update_module_app_data(self, base, name, exists): self.mw.module_widget.update_module_app_data(base, name, exists) + self.mw.settings_dialog.module_widget.update_module_app_data(base, name, exists) def get_qudi_version(self): """ Try to determine the software version in case the program is in a git repository. diff --git a/src/qudi/core/gui/main_gui/mainwindow.py b/src/qudi/core/gui/main_gui/mainwindow.py index 3053a4244..852af9942 100644 --- a/src/qudi/core/gui/main_gui/mainwindow.py +++ b/src/qudi/core/gui/main_gui/mainwindow.py @@ -75,6 +75,16 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): QtGui.QIcon(os.path.join(icon_path, 'dialog-warning'))) self.action_load_all_modules.setText('Load all modules') self.action_load_all_modules.setToolTip('Load all available modules found in configuration') + + # Dump status variables action + self.action_dump_status_variables = QtWidgets.QAction() + self.action_dump_status_variables.setIcon( + QtGui.QIcon(os.path.join(icon_path, "document-save")) + ) + self.action_dump_status_variables.setText("Save all Status Variables") + self.action_dump_status_variables.setToolTip( + "Save Status Variables of all active modules" + ) # quit action self.action_quit = QtWidgets.QAction() self.action_quit.setIcon(QtGui.QIcon(os.path.join(icon_path, 'application-exit'))) @@ -135,6 +145,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.toolbar.addAction(self.action_reload_qudi) self.toolbar.addSeparator() self.toolbar.addAction(self.action_load_all_modules) + self.toolbar.addAction(self.action_dump_status_variables) self.addToolBar(self.toolbar) # Create menu bar @@ -146,6 +157,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): menu.addAction(self.action_reload_qudi) menu.addSeparator() menu.addAction(self.action_load_all_modules) + menu.addAction(self.action_dump_status_variables) menu.addSeparator() menu.addAction(self.action_settings) menu.addSeparator() diff --git a/src/qudi/core/gui/main_gui/modulewidget.py b/src/qudi/core/gui/main_gui/modulewidget.py index d61121d7e..f6f84eedf 100644 --- a/src/qudi/core/gui/main_gui/modulewidget.py +++ b/src/qudi/core/gui/main_gui/modulewidget.py @@ -23,6 +23,7 @@ from PySide2 import QtCore, QtGui, QtWidgets from qudi.util.paths import get_artwork_dir from qudi.util.mutex import Mutex +from qudi.util.widgets.scientific_spinbox import ScienSpinBox class ModuleFrameWidget(QtWidgets.QWidget): @@ -33,6 +34,8 @@ class ModuleFrameWidget(QtWidgets.QWidget): sigDeactivateClicked = QtCore.Signal(str) sigReloadClicked = QtCore.Signal(str) sigCleanupClicked = QtCore.Signal(str) + sigDumpStatusVarClicked = QtCore.Signal(str) + def __init__(self, *args, module_name=None, **kwargs): super().__init__(*args, **kwargs) @@ -44,12 +47,15 @@ def __init__(self, *args, module_name=None, **kwargs): self.deactivate_button.setObjectName('deactivateButton') self.reload_button = QtWidgets.QToolButton() self.reload_button.setObjectName('reloadButton') + self.dump_status_var_button = QtWidgets.QToolButton() + self.dump_status_var_button.setObjectName('dumpStatusVarButton') # Set icons for QToolButtons icon_path = os.path.join(get_artwork_dir(), 'icons') self.cleanup_button.setIcon(QtGui.QIcon(os.path.join(icon_path, 'edit-clear'))) self.deactivate_button.setIcon(QtGui.QIcon(os.path.join(icon_path, 'edit-delete'))) self.reload_button.setIcon(QtGui.QIcon(os.path.join(icon_path, 'view-refresh'))) + self.dump_status_var_button.setIcon(QtGui.QIcon(os.path.join(icon_path, 'document-save'))) # Create activation pushbutton self.activate_button = QtWidgets.QPushButton('load/activate ') @@ -69,6 +75,7 @@ def __init__(self, *args, module_name=None, **kwargs): self.reload_button.setToolTip('Reload module') self.activate_button.setToolTip('Load this module and all its dependencies') self.status_label.setToolTip('Displays module status information') + self.dump_status_var_button.setToolTip('Save status variables') # Combine all widgets in a layout and set as main layout layout = QtWidgets.QGridLayout() @@ -76,6 +83,7 @@ def __init__(self, *args, module_name=None, **kwargs): layout.addWidget(self.reload_button, 0, 1) layout.addWidget(self.deactivate_button, 0, 2) layout.addWidget(self.cleanup_button, 0, 3) + layout.addWidget(self.dump_status_var_button, 0, 4) layout.addWidget(self.status_label, 1, 0, 1, 4) self.setLayout(layout) @@ -87,6 +95,7 @@ def __init__(self, *args, module_name=None, **kwargs): self.deactivate_button.clicked.connect(self.deactivate_clicked) self.reload_button.clicked.connect(self.reload_clicked) self.cleanup_button.clicked.connect(self.cleanup_clicked) + self.dump_status_var_button.clicked.connect(self.dump_status_var_clicked) return def set_module_name(self, name): @@ -100,6 +109,7 @@ def set_module_state(self, state): self.cleanup_button.setEnabled(True) self.deactivate_button.setEnabled(False) self.reload_button.setEnabled(False) + self.dump_status_var_button.setEnabled(False) if self.activate_button.isChecked(): self.activate_button.setChecked(False) elif state == 'deactivated': @@ -107,6 +117,7 @@ def set_module_state(self, state): self.cleanup_button.setEnabled(True) self.deactivate_button.setEnabled(False) self.reload_button.setEnabled(True) + self.dump_status_var_button.setEnabled(False) if self.activate_button.isChecked(): self.activate_button.setChecked(False) else: @@ -114,6 +125,7 @@ def set_module_state(self, state): self.cleanup_button.setEnabled(False) self.deactivate_button.setEnabled(True) self.reload_button.setEnabled(True) + self.dump_status_var_button.setEnabled(True) if not self.activate_button.isChecked(): self.activate_button.setChecked(True) self.status_label.setText('Module is {0}'.format(state)) @@ -133,11 +145,96 @@ def deactivate_clicked(self): def cleanup_clicked(self): self.sigCleanupClicked.emit(self._module_name) + @QtCore.Slot() + def dump_status_var_clicked(self): + self.sigDumpStatusVarClicked.emit(self._module_name) + @QtCore.Slot() def reload_clicked(self): self.sigReloadClicked.emit(self._module_name) +class ModuleStatusVariableFrameWidget(QtWidgets.QWidget): + """ + Custom module status variable QWidget for the Qudi main GUI + """ + sigAutoDumpStatusVarToggle = QtCore.Signal(bool) + sigAutoDumpStatusVarEditFinished = QtCore.Signal(int) + + + def __init__(self, *args, module_name=None, **kwargs): + super().__init__(*args, **kwargs) + + # Create QtWidgets + self.label = QtWidgets.QLabel() + self.label.setObjectName('label') + self.label.setMinimumWidth(200) + self.label.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Fixed) + self.toggle_checkbox = QtWidgets.QCheckBox() + self.toggle_checkbox.setObjectName('toggleCheckbox') + self.interval_spinbox = ScienSpinBox() + self.interval_spinbox.setObjectName('intervalSpinbox') + self.interval_spinbox.setSuffix("min") + self.interval_spinbox.setMinimum(1) + self.interval_spinbox.setMaximum(1440) + self.interval_spinbox.setMinimumSize(QtCore.QSize(80, 0)) + self.interval_spinbox.setValue(1) + + # Set tooltips + self.label.setToolTip("Name of the module") + self.toggle_checkbox.setToolTip("Enable / Disable automatic status variable saving") + self.interval_spinbox.setToolTip("Automatic status variable saving interval time") + + # Combine all widgets in a layout and set as main layout + layout = QtWidgets.QGridLayout() + layout.addWidget(self.label, 0, 0) + layout.addWidget(self.toggle_checkbox, 0, 1) + layout.addWidget(self.interval_spinbox, 0, 2) + self.setLayout(layout) + + self._module_name = '' + if module_name: + self.set_module_name(module_name) + + self.toggle_checkbox.stateChanged.connect(self.checkbox_state_change) + self.interval_spinbox.editingFinished.connect(self.editing_finished) + return + + def set_module_name(self, name): + if name: + self.label.setText('Load {0}'.format(name)) + self._module_name = name + + def set_module_state(self, state): + if state == 'not loaded': + self.label.setEnabled(False) + self.toggle_checkbox.setEnabled(False) + self.interval_spinbox.setEnabled(False) + elif state == 'deactivated': + self.label.setEnabled(False) + self.toggle_checkbox.setEnabled(False) + self.interval_spinbox.setEnabled(False) + else: + self.label.setEnabled(True) + self.toggle_checkbox.setEnabled(True) + self.interval_spinbox.setEnabled(False) + if self.toggle_checkbox.isChecked(): + self.interval_spinbox.setEnabled(True) + + def set_module_app_data(self, exists): + pass + + @QtCore.Slot(bool) + def checkbox_state_change(self, toggle: bool): + self.sigAutoDumpStatusVarToggle.emit(toggle) + self.interval_spinbox.setEnabled(toggle) + + @QtCore.Slot(int) + def editing_finished(self, interval: int): + self.sigAutoDumpStatusVarEditFinished.emit(interval) + + class ModuleListModel(QtCore.QAbstractListModel): """ """ @@ -229,6 +326,7 @@ class ModuleListItemDelegate(QtWidgets.QStyledItemDelegate): sigDeactivateClicked = QtCore.Signal(str) sigReloadClicked = QtCore.Signal(str) sigCleanupClicked = QtCore.Signal(str) + sigDumpStatusVarClicked = QtCore.Signal(str) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -243,6 +341,7 @@ def createEditor(self, parent, option, index): widget.sigDeactivateClicked.connect(self.sigDeactivateClicked) widget.sigReloadClicked.connect(self.sigReloadClicked) widget.sigCleanupClicked.connect(self.sigCleanupClicked) + widget.sigDumpStatusVarClicked.connect(self.sigDumpStatusVarClicked) return widget def setEditorData(self, editor, index): @@ -272,6 +371,30 @@ def paint(self, painter, option, index): painter.restore() +class ModuleStatusVariablesListItemDelegate(ModuleListItemDelegate): + sigAutoDumpStatusVarToggle = QtCore.Signal(bool) + sigAutoDumpStatusVarEditFinished = QtCore.Signal(int) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.render_widget = ModuleStatusVariableFrameWidget() + + def createEditor(self, parent, option, index): + widget = ModuleStatusVariableFrameWidget(parent=parent) + # Found no other way to pefectly match editor and rendered item view (using paint()) + widget.setContentsMargins(2, 2, 2, 2) + widget.sigAutoDumpStatusVarToggle.connect(self.state_change) + widget.sigAutoDumpStatusVarEditFinished.connect(self.editing_finished) + return widget + + @QtCore.Slot(bool) + def state_change(self, toggle: bool): + self.sigAutoDumpStatusVarToggle.emit(toggle) + + @QtCore.Slot(int) + def editing_finished(self, interval: int): + self.sigAutoDumpStatusVarEditFinished.emit(interval) + + class ModuleListView(QtWidgets.QListView): """ """ @@ -307,6 +430,7 @@ class ModuleWidget(QtWidgets.QTabWidget): sigActivateModule = QtCore.Signal(str) sigDeactivateModule = QtCore.Signal(str) sigCleanupModule = QtCore.Signal(str) + sigDumpStatusVarModule = QtCore.Signal(str) sigReloadModule = QtCore.Signal(str) def __init__(self, *args, **kwargs): @@ -328,6 +452,7 @@ def __init__(self, *args, **kwargs): delegate.sigDeactivateClicked.connect(self.sigDeactivateModule) delegate.sigReloadClicked.connect(self.sigReloadModule) delegate.sigCleanupClicked.connect(self.sigCleanupModule) + delegate.sigDumpStatusVarClicked.connect(self.sigDumpStatusVarModule) @QtCore.Slot(dict) def update_modules(self, modules_dict): @@ -346,3 +471,68 @@ def update_module_state(self, base, name, state): @QtCore.Slot(str, str, bool) def update_module_app_data(self, base, name, exists): self.list_models[base].change_app_data(name, exists) + +class ModuleStatusVariablesListView(ModuleListView): + """ + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setMouseTracking(True) + delegate = ModuleStatusVariablesListItemDelegate() + self.setItemDelegate(delegate) + self.setMinimumWidth(delegate.sizeHint().width()) + self.setUniformItemSizes(True) + self.setContentsMargins(0, 0, 0, 0) + self.setSpacing(1) + self.previous_index = QtCore.QModelIndex() + + +class ModuleStatusVariablesWidget(QtWidgets.QTabWidget): + """ + """ + sigAutoDumpStatusVarToggle = QtCore.Signal(bool) + sigAutoDumpStatusVarEditFinished = QtCore.Signal(int) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + self.list_models = {'gui' : ModuleListModel(), + 'logic' : ModuleListModel(), + 'hardware': ModuleListModel()} + self.list_views = {'gui' : ModuleStatusVariablesListView(), + 'logic' : ModuleStatusVariablesListView(), + 'hardware': ModuleStatusVariablesListView()} + self.addTab(self.list_views['gui'], 'GUI') + self.addTab(self.list_views['logic'], 'Logic') + self.addTab(self.list_views['hardware'], 'Hardware') + for base, view in self.list_views.items(): + view.setModel(self.list_models[base]) + delegate = view.itemDelegate() + delegate.sigAutoDumpStatusVarEditFinished.connect(self.state_change) + delegate.sigAutoDumpStatusVarToggle.connect(self.editing_finished) + + @QtCore.Slot(dict) + def update_modules(self, modules_dict): + for base, model in self.list_models.items(): + model.reset_modules( + {name: mod.state for name, mod in modules_dict.items() if mod.module_base == base}, + {name: mod.has_app_data for name, mod in modules_dict.items() if + mod.module_base == base} + ) + return + + @QtCore.Slot(str, str, str) + def update_module_state(self, base, name, state): + self.list_models[base].change_module_state(name, state) + + @QtCore.Slot(str, str, bool) + def update_module_app_data(self, base, name, exists): + self.list_models[base].change_app_data(name, exists) + + @QtCore.Slot(bool) + def state_change(self, toggle: bool): + self.sigAutoDumpStatusVarToggle.emit(toggle) + + @QtCore.Slot(int) + def editing_finished(self, interval: int): + self.sigAutoDumpStatusVarEditFinished.emit(interval) diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index cdc1d845e..0f121d9f9 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -20,6 +20,8 @@ """ from PySide2 import QtCore, QtWidgets +from qudi.core.gui.main_gui.modulewidget import ModuleStatusVariablesWidget +from qudi.util.widgets.scientific_spinbox import ScienSpinBox class SettingsDialog(QtWidgets.QDialog): @@ -56,11 +58,22 @@ def __init__(self, parent=None, **kwargs): layout.addWidget(label, 1, 0) layout.addWidget(self.show_error_popups_checkbox, 1, 1) + # Automatic status variable saving settings + groupbox = QtWidgets.QGroupBox("Automatic Status Variable Saving") + groupbox_layout = QtWidgets.QGridLayout() + groupbox_layout.setRowStretch(1, 1) + groupbox.setLayout(groupbox_layout) + self.module_widget = ModuleStatusVariablesWidget() + self.module_widget.setObjectName('moduleTabWidgetStatus') + groupbox_layout.addWidget(self.module_widget, 0, 0) + layout.addWidget(groupbox, 2, 0, 1, 2) + buttonbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Apply) buttonbox.setOrientation(QtCore.Qt.Horizontal) - layout.addWidget(buttonbox, 2, 0, 1, 2) + layout.addWidget(buttonbox, 5, 0, 1, 2) + # Add internal signals buttonbox.accepted.connect(self.accept) diff --git a/src/qudi/core/module.py b/src/qudi/core/module.py index f4d96d054..fbe8b025b 100644 --- a/src/qudi/core/module.py +++ b/src/qudi/core/module.py @@ -112,6 +112,9 @@ class Base(QtCore.QObject, metaclass=ModuleMeta): """ _threaded = False + _dump_status_variables_interval: float = StatusVar(name='dump_status_variables_interval', default=60.0) + _dump_status_variables_automation_toggle: bool = StatusVar(name='dump_status_variables_automation_toggle', default=False) + # FIXME: This __new__ implementation has the sole purpose to circumvent a known PySide2(6) bug. # See https://bugreports.qt.io/browse/PYSIDE-1434 for more details. def __new__(cls, *args, **kwargs): @@ -293,11 +296,90 @@ def is_module_threaded(self) -> bool: """ return self._threaded + @property + def dump_status_variables_interval(self) -> float: + """ + Property for the timer interval of the automatic status variable saving in s. + + @return float: timer interval in s + """ + return self._dump_status_variables_interval + + @dump_status_variables_interval.setter + def dump_status_variables_interval(self, interval: int): + """ + Setter method for the timer used to automatically dump status variables. + + @param float interval: interval of the timer in s > 0 + """ + if interval <= 0: + raise ValueError(f"Requested automatic timer {interval=} <= 0. Please choose an interval > 0.") + self._dump_status_variables_interval = interval + self.log.info( + f"Setting automated status variable saving timer interval to {interval} s." + ) + if self._dump_status_variables_automation_toggle: + self.toggle_automatically_dump_status_variables() + + @property + def dump_status_variables_automation_toggle(self) -> bool: + """ + Property for whether periodic status variables dumping is enabled. + + @return bool + """ + return self._dump_status_variables_automation_toggle + + @dump_status_variables_automation_toggle.setter + def dump_status_variables_automation_toggle(self, toggle: bool) -> None: + """ + Setter method for property for whether periodic status variables dumping is enabled. + + @param bool toggle + """ + self._dump_status_variables_automation_toggle = toggle + self.toggle_automatically_dump_status_variables() + + @QtCore.Slot() + def toggle_automatically_dump_status_variables(self) -> None: + if QtCore.QThread.currentThread() != self.thread(): + QtCore.QMetaObject.invokeMethod(self, + 'toggle_automatically_dump_status_variables', + QtCore.Qt.BlockingQueuedConnection) + return + if not self._dump_status_variables_automation_toggle: + self._automated_status_variable_dumping_timer.stop() + self.log.info("Stopped automatic status variable dumping timer") + return + self._automated_status_variable_dumping_timer.setInterval(int(self._dump_status_variables_interval * 1e3)) + self._automated_status_variable_dumping_timer.start() + self.log.info(f"Started automatic status variable dumping timer with interval of {self._dump_status_variables_interval} s") + + def _automaticically_dump_status_variables_loop(self): + self.log.debug(f"Automatic status variable dumping loop") + if not self._dump_status_variables_automation_toggle: + return + self.dump_status_variables() + self._automated_status_variable_dumping_timer.start() + + def _init_automatic_status_variable_dumping(self): + self._automated_status_variable_dumping_timer = QtCore.QTimer() + self._automated_status_variable_dumping_timer.setSingleShot(True) + self._automated_status_variable_dumping_timer.timeout.connect(self._automaticically_dump_status_variables_loop) + + def _deactivate_automatically_dump_status_variables(self): + try: + self._automated_status_variable_dumping_timer.timeout.disconnect() + except RuntimeError: + pass + def __activation_callback(self, event=None) -> bool: """ Restore status variables before activation and invoke on_activate method. """ try: self._load_status_variables() + self._init_automatic_status_variable_dumping() + self.dump_status_variables_automation_toggle = copy.deepcopy(self._dump_status_variables_automation_toggle) self.on_activate() except: self.log.exception('Exception during activation:') @@ -314,7 +396,8 @@ def __deactivation_callback(self, event=None) -> bool: self.log.exception('Exception during deactivation:') finally: # save status variables even if deactivation failed - self._dump_status_variables() + self.dump_status_variables() + self._deactivate_automatically_dump_status_variables() return True def _load_status_variables(self) -> None: @@ -340,12 +423,13 @@ def _load_status_variables(self) -> None: except: self.log.exception('Error while settings status variables:') - def _dump_status_variables(self) -> None: + def dump_status_variables(self) -> None: """ Dump status variables to app data directory on disc. This method can also be used to manually dump status variables independent of the automatic dump during module deactivation. """ + self.log.debug(f"Dumping status variables of module {self.module_name}") file_path = get_module_app_data_path(self.__class__.__name__, self.module_base, self.module_name) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 6085d034d..a07c35049 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -25,7 +25,7 @@ import weakref import fysom -from typing import FrozenSet, Iterable +from typing import FrozenSet from functools import partial from PySide2 import QtCore @@ -46,6 +46,7 @@ class ModuleManager(QtCore.QObject): sigModuleStateChanged = QtCore.Signal(str, str, str) sigModuleAppDataChanged = QtCore.Signal(str, str, bool) sigManagedModulesChanged = QtCore.Signal(dict) + automated_status_variable_dumping_timer = QtCore.QTimer() def __new__(cls, *args, **kwargs): with cls._lock: @@ -62,6 +63,7 @@ def __init__(self, *args, qudi_main, **kwargs): super().__init__(*args, **kwargs) self._qudi_main_ref = weakref.ref(qudi_main, self._qudi_main_ref_dead_callback) self._modules = dict() + self._automated_status_variable_dumping_timer_interval = 60 @classmethod def instance(cls): @@ -235,6 +237,13 @@ def clear_module_app_data(self, module_name): f'Can not clear module app status.') return self._modules[module_name].clear_module_app_data() + def dump_module_status_var(self, module_name): + with self._lock: + if module_name not in self._modules: + raise KeyError(f'No module named "{module_name}" found in managed qudi modules. ' + f'Can not save module status variables.') + return self._modules[module_name]._instance.dump_status_variables() + def has_app_data(self, module_name): with self._lock: if module_name not in self._modules: @@ -260,6 +269,16 @@ def _qudi_main_ref_dead_callback(self): 'ModuleManager.') self.clear() + def dump_status_variables(self): + """ + Method that dumps the status variables of all active modules. + """ + logger.debug(f"Dumping status variables of all modules") + with self._lock: + for _, module in self.modules.items(): + if module.is_active: + module.instance.dump_status_variables() + @QtCore.Slot() def _activate_module_slot(self): """ @@ -301,7 +320,7 @@ def __init__(self, qudi_main_ref, name, base, configuration): self._qudi_main_ref = qudi_main_ref # Weak reference to qudi main instance self._name = name # Each qudi module needs a unique string identifier - self._base = base # Remember qudi module base + self._base = base # Remember qudi module base name ('gui', 'logic', 'hardware') self._instance = None # Store the module instance later on cfg = copy.deepcopy(configuration)