From 0524d11d1dd687aa0061228f78b99497d841c73d Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Wed, 8 May 2024 16:24:58 +0200 Subject: [PATCH 01/26] adding debug messages --- src/qudi/core/module.py | 254 +++++++++++++++++++++++----------------- 1 file changed, 144 insertions(+), 110 deletions(-) diff --git a/src/qudi/core/module.py b/src/qudi/core/module.py index f4d96d054..2b26b9cfc 100644 --- a/src/qudi/core/module.py +++ b/src/qudi/core/module.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU Lesser General Public License along with qudi. If not, see . """ + import logging import os import copy @@ -30,7 +31,11 @@ from qudi.core.configoption import MissingOption from qudi.core.statusvariable import StatusVar -from qudi.util.paths import get_module_app_data_path, get_daily_directory, get_default_data_dir +from qudi.util.paths import ( + get_module_app_data_path, + get_daily_directory, + get_default_data_dir, +) from qudi.util.yaml import yaml_load, yaml_dump from qudi.core.meta import ModuleMeta from qudi.core.logger import get_logger @@ -40,6 +45,7 @@ class ModuleStateMachine(Fysom, QtCore.QObject): """ FIXME """ + # do not copy declaration of trigger(self, event, *args, **kwargs), just apply Slot decorator trigger = QtCore.Slot(str, result=bool)(Fysom.trigger) @@ -55,13 +61,17 @@ def __init__(self, callbacks=None, parent=None, **kwargs): # name: event name, # src: source state, # dst: destination state - fsm_cfg = {'initial': 'deactivated', - 'events': [{'name': 'activate', 'src': 'deactivated', 'dst': 'idle'}, - {'name': 'deactivate', 'src': 'idle', 'dst': 'deactivated'}, - {'name': 'deactivate', 'src': 'locked', 'dst': 'deactivated'}, - {'name': 'lock', 'src': 'idle', 'dst': 'locked'}, - {'name': 'unlock', 'src': 'locked', 'dst': 'idle'}], - 'callbacks': callbacks} + fsm_cfg = { + "initial": "deactivated", + "events": [ + {"name": "activate", "src": "deactivated", "dst": "idle"}, + {"name": "deactivate", "src": "idle", "dst": "deactivated"}, + {"name": "deactivate", "src": "locked", "dst": "deactivated"}, + {"name": "lock", "src": "idle", "dst": "locked"}, + {"name": "unlock", "src": "locked", "dst": "idle"}, + ], + "callbacks": callbacks, + } # Initialise state machine: super().__init__(parent=parent, cfg=fsm_cfg, **kwargs) @@ -98,7 +108,7 @@ def unlock(self) -> None: class Base(QtCore.QObject, metaclass=ModuleMeta): - """ Base class for all loadable modules + """Base class for all loadable modules * Ensure that the program will not die during the load of modules * Initialize modules @@ -110,21 +120,29 @@ class Base(QtCore.QObject, metaclass=ModuleMeta): * Get status variables * Reload module data (from saved variables) """ + _threaded = 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): - abstract = getattr(cls, '__abstractmethods__', frozenset()) + abstract = getattr(cls, "__abstractmethods__", frozenset()) if abstract: - raise TypeError(f'Can\'t instantiate abstract class "{cls.__name__}" ' - f'with abstract methods {set(abstract)}') + raise TypeError( + f'Can\'t instantiate abstract class "{cls.__name__}" ' + f"with abstract methods {set(abstract)}" + ) return super().__new__(cls, *args, **kwargs) - def __init__(self, qudi_main_weakref: Any, name: str, - config: Optional[Mapping[str, Any]] = None, - callbacks: Optional[Mapping[str, Callable]] = None, **kwargs): - """ Initialise Base instance. Set up its state machine and initialize ConfigOption meta + def __init__( + self, + qudi_main_weakref: Any, + name: str, + config: Optional[Mapping[str, Any]] = None, + callbacks: Optional[Mapping[str, Callable]] = None, + **kwargs, + ): + """Initialise Base instance. Set up its state machine and initialize ConfigOption meta attributes from given config. @param object self: the object being initialised @@ -143,14 +161,14 @@ def __init__(self, qudi_main_weakref: Any, name: str, self.__qudi_main_weakref = qudi_main_weakref # Create logger instance for module - self.__logger = get_logger(f'{self.__module__}.{self.__class__.__name__}') + self.__logger = get_logger(f"{self.__module__}.{self.__class__.__name__}") # Create a copy of the _meta class dict and attach it to the created instance self._meta = copy.deepcopy(self._meta) # Add additional meta info to _meta dict - self._meta['name'] = name - self._meta['uuid'] = uuid4() - self._meta['configuration'] = copy.deepcopy(config) + self._meta["name"] = name + self._meta["uuid"] = uuid4() + self._meta["configuration"] = copy.deepcopy(config) # set instance attributes according to config_option meta objects self.__initialize_config_options(config) @@ -159,24 +177,28 @@ def __init__(self, qudi_main_weakref: Any, name: str, self.__initialize_connectors() # Initialize module FSM - default_callbacks = {'on_before_activate' : self.__activation_callback, - 'on_before_deactivate': self.__deactivation_callback} + default_callbacks = { + "on_before_activate": self.__activation_callback, + "on_before_deactivate": self.__deactivation_callback, + } default_callbacks.update(callbacks) self.module_state = ModuleStateMachine(parent=self, callbacks=default_callbacks) return def __initialize_config_options(self, config: Optional[Mapping[str, Any]]) -> None: - for attr_name, cfg_opt in self._meta['config_options'].items(): + for attr_name, cfg_opt in self._meta["config_options"].items(): if cfg_opt.name in config: cfg_val = copy.deepcopy(config[cfg_opt.name]) else: if cfg_opt.missing == MissingOption.error: raise ValueError( f'Required ConfigOption "{cfg_opt.name}" not given in configuration.\n' - f'Configuration is: {config}' + f"Configuration is: {config}" ) - msg = f'No ConfigOption "{cfg_opt.name}" configured, using default value ' \ - f'"{cfg_opt.default}" instead.' + msg = ( + f'No ConfigOption "{cfg_opt.name}" configured, using default value ' + f'"{cfg_opt.default}" instead.' + ) cfg_val = copy.deepcopy(cfg_opt.default) if cfg_opt.missing == MissingOption.warn: self.log.warning(msg) @@ -189,7 +211,7 @@ def __initialize_config_options(self, config: Optional[Mapping[str, Any]]) -> No setattr(self, attr_name, cfg_val) def __initialize_connectors(self) -> None: - for attr_name, conn in self._meta['connectors'].items(): + for attr_name, conn in self._meta["connectors"].items(): setattr(self, attr_name, conn) def __eq__(self, other): @@ -202,18 +224,17 @@ def __hash__(self): @QtCore.Slot() def move_to_main_thread(self) -> None: - """ Method that will move this module into the main/manager thread. - """ + """Method that will move this module into the main/manager thread.""" if QtCore.QThread.currentThread() != self.thread(): - QtCore.QMetaObject.invokeMethod(self, - 'move_to_main_thread', - QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self, "move_to_main_thread", QtCore.Qt.BlockingQueuedConnection + ) else: self.moveToThread(QtCore.QCoreApplication.instance().thread()) @property def module_thread(self) -> Union[QtCore.QThread, None]: - """ Read-only property returning the current module QThread instance if the module is + """Read-only property returning the current module QThread instance if the module is threaded. Returns None otherwise. """ if self._threaded: @@ -222,36 +243,37 @@ def module_thread(self) -> Union[QtCore.QThread, None]: @property def module_name(self) -> str: - """ Read-only property returning the module name of this module instance as specified in the + """Read-only property returning the module name of this module instance as specified in the config. """ - return self._meta['name'] + return self._meta["name"] @property def module_base(self) -> str: - """ Read-only property returning the module base of this module instance + """Read-only property returning the module base of this module instance ('hardware' 'logic' or 'gui') """ - return self._meta['base'] + return self._meta["base"] @property def module_uuid(self) -> uuid.UUID: - """ Read-only property returning a unique uuid for this module instance. - """ - return self._meta['uuid'] + """Read-only property returning a unique uuid for this module instance.""" + return self._meta["uuid"] @property def module_default_data_dir(self) -> str: - """ Read-only property returning the generic default directory in which to save data. + """Read-only property returning the generic default directory in which to save data. Module implementations can overwrite this property with a custom path but should only do so with a very good reason. """ config = self._qudi_main.configuration - data_root = config['default_data_dir'] + data_root = config["default_data_dir"] if data_root is None: data_root = get_default_data_dir() - if config['daily_data_dirs']: - data_dir = os.path.join(get_daily_directory(root=data_root), self.module_name) + if config["daily_data_dirs"]: + data_dir = os.path.join( + get_daily_directory(root=data_root), self.module_name + ) else: data_dir = os.path.join(data_root, self.module_name) return data_dir @@ -260,7 +282,7 @@ def module_default_data_dir(self) -> str: def module_status_variables(self) -> Dict[str, Any]: variables = dict() try: - for attr_name, var in self._meta['status_variables'].items(): + for attr_name, var in self._meta["status_variables"].items(): if hasattr(self, attr_name): value = getattr(self, attr_name) if not isinstance(value, StatusVar): @@ -268,7 +290,7 @@ def module_status_variables(self) -> Dict[str, Any]: value = var.representer_function(self, value) variables[var.name] = value except: - self.log.exception('Error while collecting status variables:') + self.log.exception("Error while collecting status variables:") return variables @property @@ -276,79 +298,76 @@ def _qudi_main(self) -> Any: qudi_main = self.__qudi_main_weakref() if qudi_main is None: raise RuntimeError( - 'Unexpected missing qudi main instance. It has either been deleted or garbage ' - 'collected.' + "Unexpected missing qudi main instance. It has either been deleted or garbage " + "collected." ) return qudi_main @property def log(self) -> logging.Logger: - """ Returns the module logger instance - """ + """Returns the module logger instance""" return self.__logger @property def is_module_threaded(self) -> bool: - """ Returns whether the module shall be started in its own thread. - """ + """Returns whether the module shall be started in its own thread.""" return self._threaded def __activation_callback(self, event=None) -> bool: - """ Restore status variables before activation and invoke on_activate method. - """ + """Restore status variables before activation and invoke on_activate method.""" try: self._load_status_variables() self.on_activate() except: - self.log.exception('Exception during activation:') + self.log.exception("Exception during activation:") return False return True def __deactivation_callback(self, event=None) -> bool: - """ Invoke on_deactivate method and save status variables afterwards even if deactivation + """Invoke on_deactivate method and save status variables afterwards even if deactivation fails. """ try: self.on_deactivate() except: - self.log.exception('Exception during deactivation:') + self.log.exception("Exception during deactivation:") finally: # save status variables even if deactivation failed self._dump_status_variables() return True def _load_status_variables(self) -> None: - """ Load status variables from app data directory on disc. - """ + """Load status variables from app data directory on disc.""" # Load status variables from app data directory - file_path = get_module_app_data_path(self.__class__.__name__, - self.module_base, - self.module_name) + file_path = get_module_app_data_path( + self.__class__.__name__, self.module_base, self.module_name + ) try: variables = yaml_load(file_path, ignore_missing=True) except: variables = dict() - self.log.exception('Failed to load status variables:') + self.log.exception("Failed to load status variables:") # Set instance attributes according to StatusVar meta objects try: - for attr_name, var in self._meta['status_variables'].items(): + for attr_name, var in self._meta["status_variables"].items(): value = variables.get(var.name, copy.deepcopy(var.default)) if var.constructor_function is not None: value = var.constructor_function(self, value) setattr(self, attr_name, value) except: - self.log.exception('Error while settings status variables:') + self.log.exception("Error while settings status variables:") def _dump_status_variables(self) -> None: - """ Dump status variables to app data directory on disc. + """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. """ - file_path = get_module_app_data_path(self.__class__.__name__, - self.module_base, - self.module_name) + 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 + ) # collect StatusVar values into dictionary variables = self.module_status_variables # Save to file if any StatusVars have been found @@ -356,16 +375,21 @@ def _dump_status_variables(self) -> None: try: yaml_dump(file_path, variables) except: - self.log.exception('Failed to save status variables:') - - def _send_balloon_message(self, title: str, message: str, time: Optional[float] = None, - icon: Optional[QtGui.QIcon] = None) -> None: + self.log.exception("Failed to save status variables:") + + def _send_balloon_message( + self, + title: str, + message: str, + time: Optional[float] = None, + icon: Optional[QtGui.QIcon] = None, + ) -> None: qudi_main = self.__qudi_main_weakref() if qudi_main is None: return if qudi_main.gui is None: - log = get_logger('balloon-message') - log.warning(f'{title}:\n{message}') + log = get_logger("balloon-message") + log.warning(f"{title}:\n{message}") return qudi_main.gui.balloon_message(title, message, time, icon) @@ -374,100 +398,110 @@ def _send_pop_up_message(self, title: str, message: str): if qudi_main is None: return if qudi_main.gui is None: - log = get_logger('pop-up-message') - log.warning(f'{title}:\n{message}') + log = get_logger("pop-up-message") + log.warning(f"{title}:\n{message}") return qudi_main.gui.pop_up_message(title, message) def connect_modules(self, connections: Mapping[str, Any]) -> None: - """ Connects given modules (values) to their respective Connector (keys). + """Connects given modules (values) to their respective Connector (keys). DO NOT CALL THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING! """ # Sanity checks - conn_names = set(conn.name for conn in self._meta['connectors'].values()) + conn_names = set(conn.name for conn in self._meta["connectors"].values()) mandatory_conn = set( - conn.name for conn in self._meta['connectors'].values() if not conn.optional + conn.name for conn in self._meta["connectors"].values() if not conn.optional ) configured_conn = set(connections) if not configured_conn.issubset(conn_names): - raise KeyError(f'Mismatch of connectors in configuration {configured_conn} and module ' - f'Connector meta objects {conn_names}.') + raise KeyError( + f"Mismatch of connectors in configuration {configured_conn} and module " + f"Connector meta objects {conn_names}." + ) if not mandatory_conn.issubset(configured_conn): - raise ValueError(f'Not all mandatory connectors are specified in config.\n' - f'Mandatory connectors are: {mandatory_conn}') + raise ValueError( + f"Not all mandatory connectors are specified in config.\n" + f"Mandatory connectors are: {mandatory_conn}" + ) # Iterate through module connectors and connect them if possible - for conn in self._meta['connectors'].values(): + for conn in self._meta["connectors"].values(): target = connections.get(conn.name, None) if target is None: continue if conn.is_connected: - raise RuntimeError(f'Connector "{conn.name}" already connected.\n' - f'Call "disconnect_modules()" before trying to reconnect.') + raise RuntimeError( + f'Connector "{conn.name}" already connected.\n' + f'Call "disconnect_modules()" before trying to reconnect.' + ) conn.connect(target) def disconnect_modules(self) -> None: - """ Disconnects all Connector instances for this module. + """Disconnects all Connector instances for this module. DO NOT CALL THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING! """ - for conn in self._meta['connectors'].values(): + for conn in self._meta["connectors"].values(): conn.disconnect() @abstractmethod def on_activate(self) -> None: - """ Method called when module is activated. Must be implemented by actual qudi module. - """ - raise NotImplementedError('Please implement and specify the activation method.') + """Method called when module is activated. Must be implemented by actual qudi module.""" + raise NotImplementedError("Please implement and specify the activation method.") @abstractmethod def on_deactivate(self) -> None: - """ Method called when module is deactivated. Must be implemented by actual qudi module. - """ - raise NotImplementedError('Please implement and specify the deactivation method.') + """Method called when module is deactivated. Must be implemented by actual qudi module.""" + raise NotImplementedError( + "Please implement and specify the deactivation method." + ) class LogicBase(Base): - """ - """ + """ """ + _threaded = True class GuiBase(Base): - """This is the GUI base class. It provides functions that every GUI module should have. - """ + """This is the GUI base class. It provides functions that every GUI module should have.""" + _threaded = False - __window_geometry = StatusVar(name='_GuiBase__window_geometry', default=None) - __window_state = StatusVar(name='_GuiBase__window_state', default=None) + __window_geometry = StatusVar(name="_GuiBase__window_geometry", default=None) + __window_state = StatusVar(name="_GuiBase__window_state", default=None) @abstractmethod def show(self) -> None: - raise NotImplementedError('Every GUI module needs to implement the show() method!') + raise NotImplementedError( + "Every GUI module needs to implement the show() method!" + ) def _save_window_geometry(self, window: QtWidgets.QMainWindow) -> None: try: - self.__window_geometry = window.saveGeometry().toHex().data().decode('utf-8') + self.__window_geometry = ( + window.saveGeometry().toHex().data().decode("utf-8") + ) except: - self.log.exception('Unable to save window geometry:') + self.log.exception("Unable to save window geometry:") self.__window_geometry = None try: - self.__window_state = window.saveState().toHex().data().decode('utf-8') + self.__window_state = window.saveState().toHex().data().decode("utf-8") except: - self.log.exception('Unable to save window geometry:') + self.log.exception("Unable to save window geometry:") self.__window_state = None def _restore_window_geometry(self, window: QtWidgets.QMainWindow) -> bool: if isinstance(self.__window_geometry, str): try: - encoded = QtCore.QByteArray(self.__window_geometry.encode('utf-8')) + encoded = QtCore.QByteArray(self.__window_geometry.encode("utf-8")) window.restoreGeometry(QtCore.QByteArray.fromHex(encoded)) except: - self.log.exception('Unable to restore window geometry:') + self.log.exception("Unable to restore window geometry:") if isinstance(self.__window_state, str): try: - encoded = QtCore.QByteArray(self.__window_state.encode('utf-8')) + encoded = QtCore.QByteArray(self.__window_state.encode("utf-8")) return window.restoreState(QtCore.QByteArray.fromHex(encoded)) except: - self.log.exception('Unable to restore window state:') + self.log.exception("Unable to restore window state:") return False From 2d1c3897b366fc9002c21c1b2607c5b095985b48 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Wed, 8 May 2024 16:54:57 +0200 Subject: [PATCH 02/26] added method to dump all status vars of active modules --- src/qudi/core/modulemanager.py | 339 ++++++++++++++++++++++----------- 1 file changed, 225 insertions(+), 114 deletions(-) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 7cde765de..65f56c292 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -29,7 +29,9 @@ from functools import partial from PySide2 import QtCore -from qudi.util.mutex import RecursiveMutex # provides access serialization between threads +from qudi.util.mutex import ( + RecursiveMutex, +) # provides access serialization between threads from qudi.core.logger import get_logger from qudi.core.servers import get_remote_module_instance from qudi.core.module import Base, get_module_app_data_path @@ -38,8 +40,8 @@ class ModuleManager(QtCore.QObject): - """ - """ + """ """ + _instance = None # Only class instance created will be stored here as weakref _lock = RecursiveMutex() @@ -54,8 +56,8 @@ def __new__(cls, *args, **kwargs): cls._instance = weakref.ref(obj) return obj raise RuntimeError( - 'ModuleManager is a singleton. An instance has already been created in this ' - 'process. Please use ModuleManager.instance() instead.' + "ModuleManager is a singleton. An instance has already been created in this " + "process. Please use ModuleManager.instance() instead." ) def __init__(self, *args, qudi_main, **kwargs): @@ -81,7 +83,7 @@ def __getitem__(self, key): def __setitem__(self, key, value): with self._lock: if value.name != key: - raise NameError('ManagedModule.name attribute does not match key') + raise NameError("ManagedModule.name attribute does not match key") self.add_module(value, allow_overwrite=True) def __delitem__(self, key): @@ -123,8 +125,11 @@ def module_states(self): @property def module_instances(self): with self._lock: - return {name: mod.instance for name, mod in self._modules.items() if - mod.instance is not None} + return { + name: mod.instance + for name, mod in self._modules.items() + if mod.instance is not None + } @property def modules(self): @@ -146,13 +151,17 @@ def remove_module(self, module_name, ignore_missing=False, emit_change=True): if emit_change: self.sigManagedModulesChanged.emit(self.modules) - def add_module(self, name, base, configuration, allow_overwrite=False, emit_change=True): + def add_module( + self, name, base, configuration, allow_overwrite=False, emit_change=True + ): with self._lock: if not isinstance(name, str) or not name: - raise TypeError('module name must be non-empty str type') - if base not in ('gui', 'logic', 'hardware'): - raise ValueError(f'No valid module base "{base}". ' - f'Unable to create qudi module "{name}".') + raise TypeError("module name must be non-empty str type") + if base not in ("gui", "logic", "hardware"): + raise ValueError( + f'No valid module base "{base}". ' + f'Unable to create qudi module "{name}".' + ) if allow_overwrite: self.remove_module(name, ignore_missing=True) elif name in self._modules: @@ -168,7 +177,7 @@ def add_module(self, name, base, configuration, allow_overwrite=False, emit_chan if remote_modules_server is None: raise RuntimeError( f'Unable to share qudi module "{module.name}" as remote module. No remote ' - f'module server running in this qudi process.' + f"module server running in this qudi process." ) else: logger.info( @@ -181,51 +190,67 @@ def add_module(self, name, base, configuration, allow_overwrite=False, emit_chan def refresh_module_links(self): with self._lock: weak_refs = { - name: weakref.ref(mod, partial(self._module_ref_dead_callback, module_name=name)) + name: weakref.ref( + mod, partial(self._module_ref_dead_callback, module_name=name) + ) for name, mod in self._modules.items() } for module_name, module in self._modules.items(): # Add required module references required = set(module.connection_cfg.values()) module.required_modules = set( - mod_ref for name, mod_ref in weak_refs.items() if name in required) + mod_ref for name, mod_ref in weak_refs.items() if name in required + ) # Add dependent module references - module.dependent_modules = set(mod_ref for mod_ref in weak_refs.values() if - module_name in mod_ref().connection_cfg.values()) + module.dependent_modules = set( + mod_ref + for mod_ref in weak_refs.values() + if module_name in mod_ref().connection_cfg.values() + ) def activate_module(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'Module activation aborted.') + raise KeyError( + f'No module named "{module_name}" found in managed qudi modules. ' + f"Module activation aborted." + ) self._modules[module_name].activate() def deactivate_module(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'Module deactivation aborted.') + raise KeyError( + f'No module named "{module_name}" found in managed qudi modules. ' + f"Module deactivation aborted." + ) self._modules[module_name].deactivate() def reload_module(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'Module reload aborted.') + raise KeyError( + f'No module named "{module_name}" found in managed qudi modules. ' + f"Module reload aborted." + ) return self._modules[module_name].reload() def clear_module_app_data(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 clear module app status.') + raise KeyError( + f'No module named "{module_name}" found in managed qudi modules. ' + f"Can not clear module app status." + ) return self._modules[module_name].clear_module_app_data() def has_app_data(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 check for app status file.') + raise KeyError( + f'No module named "{module_name}" found in managed qudi modules. ' + f"Can not check for app status file." + ) return self._modules[module_name].has_app_data() def start_all_modules(self): @@ -242,34 +267,51 @@ def _module_ref_dead_callback(self, dead_ref, module_name): self.remove_module(module_name, ignore_missing=True) def _qudi_main_ref_dead_callback(self): - logger.error('Qudi main reference no longer valid. This should never happen. Tearing down ' - 'ModuleManager.') + logger.error( + "Qudi main reference no longer valid. This should never happen. Tearing down " + "ModuleManager." + ) self.clear() + def dump_status_variables(self): + """ + Method that dumps the status variables of all active modules. + """ + logger.debug(f"Dumping status variables") + with self._lock: + for _, module in self.modules.items(): + if module.is_active: + module.instance._dump_status_variables() + class ManagedModule(QtCore.QObject): - """ Object representing a qudi module (gui, logic or hardware) to be managed by the qudi Manager - object. Contains status properties and handles initialization, state transitions and - connection of the module. + """Object representing a qudi module (gui, logic or hardware) to be managed by the qudi Manager + object. Contains status properties and handles initialization, state transitions and + connection of the module. """ + sigStateChanged = QtCore.Signal(str, str, str) sigAppDataChanged = QtCore.Signal(str, str, bool) _lock = RecursiveMutex() # Single mutex shared across all ManagedModule instances - __state_poll_interval = 1 # Max interval in seconds to poll module_state of remote modules + __state_poll_interval = ( + 1 # Max interval in seconds to poll module_state of remote modules + ) def __init__(self, qudi_main_ref, name, base, configuration): if not isinstance(qudi_main_ref, weakref.ref): - raise TypeError('qudi_main_ref must be weakref to qudi main instance.') + raise TypeError("qudi_main_ref must be weakref to qudi main instance.") if not name or not isinstance(name, str): - raise ValueError('Module name must be a non-empty string.') - if base not in ('gui', 'logic', 'hardware'): + raise ValueError("Module name must be a non-empty string.") + if base not in ("gui", "logic", "hardware"): raise ValueError('Module base must be one of ("gui", "logic", "hardware").') super().__init__() if self.thread() is not QtCore.QCoreApplication.instance().thread(): - raise RuntimeError('ManagedModules can only be owned by the application main thread.') + raise RuntimeError( + "ManagedModules can only be owned by the application main thread." + ) self._qudi_main_ref = qudi_main_ref # Weak reference to qudi main instance self._name = name # Each qudi module needs a unique string identifier @@ -279,29 +321,35 @@ def __init__(self, qudi_main_ref, name, base, configuration): cfg = copy.deepcopy(configuration) # Extract module and class name - self._module, self._class = cfg.get( - 'module.Class', - 'REMOTE.REMOTE' - ).rsplit('.', 1) + self._module, self._class = cfg.get("module.Class", "REMOTE.REMOTE").rsplit( + ".", 1 + ) # Remember connections by name - self._connect_cfg = cfg.get('connect', dict()) + self._connect_cfg = cfg.get("connect", dict()) # See if remotemodules access to this module is allowed - self._allow_remote_access = cfg.get('allow_remote', False) + self._allow_remote_access = cfg.get("allow_remote", False) # Extract remote modules URL and certificate if this module is run on a remote machine - self._remote_module_name = cfg.get('native_module_name', None) - self._remote_address = cfg.get('address', None) - self._remote_port = cfg.get('port', None) - self._remote_certfile = cfg.get('certfile', None) - self._remote_keyfile = cfg.get('keyfile', None) - if any(attr is None for attr in [self._remote_module_name, self._remote_address, self._remote_port]): + self._remote_module_name = cfg.get("native_module_name", None) + self._remote_address = cfg.get("address", None) + self._remote_port = cfg.get("port", None) + self._remote_certfile = cfg.get("certfile", None) + self._remote_keyfile = cfg.get("keyfile", None) + if any( + attr is None + for attr in [ + self._remote_module_name, + self._remote_address, + self._remote_port, + ] + ): self._remote_url = None else: - self._remote_url = f'rpyc://{self._remote_address}:{self._remote_port:d}/{self._remote_module_name}/' + self._remote_url = f"rpyc://{self._remote_address}:{self._remote_port:d}/{self._remote_module_name}/" # Do not propagate remotemodules access self._allow_remote_access = False # The rest are config options - self._options = cfg.get('options', dict()) + self._options = cfg.get("options", dict()) self._required_modules = frozenset() self._dependent_modules = frozenset() @@ -349,12 +397,15 @@ def is_loaded(self): @property def is_active(self): with self._lock: - return self._instance is not None and self._instance.module_state() != 'deactivated' + return ( + self._instance is not None + and self._instance.module_state() != "deactivated" + ) @property def is_busy(self): with self._lock: - return self.is_active and self._instance.module_state() != 'idle' + return self.is_active and self._instance.module_state() != "idle" @property def is_remote(self): @@ -381,9 +432,9 @@ def state(self): try: return self._instance.module_state() except AttributeError: - return 'not loaded' + return "not loaded" except: - return 'BROKEN' + return "BROKEN" @property def connection_cfg(self): @@ -397,15 +448,19 @@ def required_modules(self) -> FrozenSet[weakref.ref]: def required_modules(self, module_iter): for module in module_iter: if not isinstance(module, weakref.ref): - raise TypeError('items in required_modules must be weakref.ref instances.') + raise TypeError( + "items in required_modules must be weakref.ref instances." + ) if not isinstance(module(), ManagedModule): if module() is None: logger.error( f'Dead weakref passed as required module to ManagedModule "{self._name}"' ) return - raise TypeError('required_modules must be iterable of ManagedModule instances ' - '(or weakref to same instances)') + raise TypeError( + "required_modules must be iterable of ManagedModule instances " + "(or weakref to same instances)" + ) self._required_modules = frozenset(module_iter) @property @@ -417,15 +472,19 @@ def dependent_modules(self, module_iter): dep_modules = set() for module in module_iter: if not isinstance(module, weakref.ref): - raise TypeError('items in dependent_modules must be weakref.ref instances.') + raise TypeError( + "items in dependent_modules must be weakref.ref instances." + ) if not isinstance(module(), ManagedModule): if module() is None: logger.error( f'Dead weakref passed as dependent module to ManagedModule "{self._name}"' ) return - raise TypeError('dependent_modules must be iterable of ManagedModule instances ' - '(or weakref to same instances)') + raise TypeError( + "dependent_modules must be iterable of ManagedModule instances " + "(or weakref to same instances)" + ) dep_modules.add(module) self._dependent_modules = frozenset(dep_modules) @@ -436,8 +495,10 @@ def ranking_active_dependent_modules(self): for module_ref in self.dependent_modules: module = module_ref() if module is None: - logger.warning(f'Dead dependent module weakref encountered in ManagedModule ' - f'"{self._name}".') + logger.warning( + f"Dead dependent module weakref encountered in ManagedModule " + f'"{self._name}".' + ) continue if module.is_active: active_modules = module.ranking_active_dependent_modules @@ -449,7 +510,7 @@ def ranking_active_dependent_modules(self): @property def module_thread_name(self): - return f'mod-{self._base}-{self._name}' + return f"mod-{self._base}-{self._name}" @property def has_app_data(self): @@ -470,9 +531,13 @@ def clear_module_app_data(self): def activate(self) -> None: # Switch to the main thread if this method was called from another thread if QtCore.QThread.currentThread() is not self.thread(): - QtCore.QMetaObject.invokeMethod(self, 'activate', QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self, "activate", QtCore.Qt.BlockingQueuedConnection + ) if not self.is_active: - raise RuntimeError(f'Failed to activate {self.module_base} module "{self.name}"!') + raise RuntimeError( + f'Failed to activate {self.module_base} module "{self.name}"!' + ) return with self._lock: @@ -482,12 +547,14 @@ def activate(self) -> None: # Return early if already active if self.is_active: # If it is a GUI module, show it again. - if self.module_base == 'gui': + if self.module_base == "gui": self._instance.show() return if self.is_remote: - logger.info(f'Activating remote {self.module_base} module "{self.remote_url}"') + logger.info( + f'Activating remote {self.module_base} module "{self.remote_url}"' + ) else: logger.info( f'Activating {self.module_base} module "{self.module_name}.{self.class_name}"' @@ -497,8 +564,10 @@ def activate(self) -> None: for module_ref in self.required_modules: module = module_ref() if module is None: - raise ReferenceError(f'Dead required module weakref encountered in ' - f'ManagedModule "{self._name}".') + raise ReferenceError( + f"Dead required module weakref encountered in " + f'ManagedModule "{self._name}".' + ) module.activate() # Establish module interconnections via Connector meta object in qudi module instance @@ -512,15 +581,19 @@ def activate(self) -> None: self._instance.moveToThread(thread) thread.start() try: - QtCore.QMetaObject.invokeMethod(self._instance.module_state, - 'activate', - QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self._instance.module_state, + "activate", + QtCore.Qt.BlockingQueuedConnection, + ) finally: # Cleanup if activation was not successful if not self.is_active: - QtCore.QMetaObject.invokeMethod(self._instance, - 'move_to_main_thread', - QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self._instance, + "move_to_main_thread", + QtCore.Qt.BlockingQueuedConnection, + ) thread_manager.quit_thread(thread_name) thread_manager.join_thread(thread_name) else: @@ -540,16 +613,22 @@ def activate(self) -> None: self._disconnect() except: pass - raise RuntimeError(f'Failed to activate {self.module_base} module "{self.name}"!') + raise RuntimeError( + f'Failed to activate {self.module_base} module "{self.name}"!' + ) if self.is_remote: self.__poll_timer = QtCore.QTimer(self) - self.__poll_timer.setInterval(int(round(self.__state_poll_interval * 1000))) + self.__poll_timer.setInterval( + int(round(self.__state_poll_interval * 1000)) + ) self.__poll_timer.setSingleShot(True) self.__poll_timer.timeout.connect(self._poll_module_state) self.__poll_timer.start() else: - self._instance.module_state.sigStateChanged.connect(self._state_change_callback) + self._instance.module_state.sigStateChanged.connect( + self._state_change_callback + ) @QtCore.Slot() def _poll_module_state(self): @@ -571,9 +650,13 @@ def _state_change_callback(self, event=None): def deactivate(self): # Switch to the main thread if this method was called from another thread if QtCore.QThread.currentThread() is not self.thread(): - QtCore.QMetaObject.invokeMethod(self, 'deactivate', QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self, "deactivate", QtCore.Qt.BlockingQueuedConnection + ) if self.is_active: - raise RuntimeError(f'Failed to deactivate {self.module_base} module "{self.name}"!') + raise RuntimeError( + f'Failed to deactivate {self.module_base} module "{self.name}"!' + ) return with self._lock: @@ -581,7 +664,9 @@ def deactivate(self): return if self.is_remote: - logger.info(f'Deactivating remote {self.module_base} module "{self.remote_url}"') + logger.info( + f'Deactivating remote {self.module_base} module "{self.remote_url}"' + ) else: logger.info( f'Deactivating {self.module_base} module "{self.module_name}.{self.class_name}"' @@ -601,7 +686,9 @@ def deactivate(self): self.__poll_timer.stop() self.__poll_timer.timeout.disconnect() except AttributeError: - self._instance.module_state.sigStateChanged.disconnect(self._state_change_callback) + self._instance.module_state.sigStateChanged.disconnect( + self._state_change_callback + ) finally: self.__poll_timer = None @@ -610,13 +697,17 @@ def deactivate(self): thread_name = self.module_thread_name thread_manager = self._qudi_main_ref().thread_manager try: - QtCore.QMetaObject.invokeMethod(self._instance.module_state, - 'deactivate', - QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self._instance.module_state, + "deactivate", + QtCore.Qt.BlockingQueuedConnection, + ) finally: - QtCore.QMetaObject.invokeMethod(self._instance, - 'move_to_main_thread', - QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self._instance, + "move_to_main_thread", + QtCore.Qt.BlockingQueuedConnection, + ) thread_manager.quit_thread(thread_name) thread_manager.join_thread(thread_name) else: @@ -636,13 +727,17 @@ def deactivate(self): # Raise exception if by some reason no exception propagated to here and the deactivation # is still unsuccessful. if self.is_active: - raise RuntimeError(f'Failed to deactivate {self.module_base} module "{self.name}"!') + raise RuntimeError( + f'Failed to deactivate {self.module_base} module "{self.name}"!' + ) @QtCore.Slot() def reload(self): # Switch to the main thread if this method was called from another thread if QtCore.QThread.currentThread() is not self.thread(): - QtCore.QMetaObject.invokeMethod(self, 'reload', QtCore.Qt.BlockingQueuedConnection) + QtCore.QMetaObject.invokeMethod( + self, "reload", QtCore.Qt.BlockingQueuedConnection + ) return with self._lock: @@ -668,8 +763,7 @@ def reload(self): self.activate() def _load(self, reload=False): - """ - """ + """ """ with self._lock: try: # Do nothing if already loaded and no reload is requested @@ -678,38 +772,50 @@ def _load(self, reload=False): if self.is_remote: try: - self._instance = get_remote_module_instance(self.remote_url, - certfile=self._remote_certfile, - keyfile=self._remote_keyfile) + self._instance = get_remote_module_instance( + self.remote_url, + certfile=self._remote_certfile, + keyfile=self._remote_keyfile, + ) except BaseException as e: self._instance = None - raise RuntimeError(f'Error during initialization of remote ' - f'{self.module_base} module {self.remote_url}') from e + raise RuntimeError( + f"Error during initialization of remote " + f"{self.module_base} module {self.remote_url}" + ) from e else: # qudi module import and reload - mod = importlib.import_module(f'qudi.{self._base}.{self._module}') + mod = importlib.import_module(f"qudi.{self._base}.{self._module}") importlib.reload(mod) # Try getting qudi module class from imported module mod_class = getattr(mod, self._class, None) if mod_class is None: - raise AttributeError(f'No module class "{self._class}" found in module ' - f'"qudi.{self._base}.{self._module}"') + raise AttributeError( + f'No module class "{self._class}" found in module ' + f'"qudi.{self._base}.{self._module}"' + ) # Check if imported class is a valid qudi module class if not issubclass(mod_class, Base): - raise TypeError(f'Qudi module class "{mod_class}" is no subclass of ' - f'"qudi.core.module.Base"') + raise TypeError( + f'Qudi module class "{mod_class}" is no subclass of ' + f'"qudi.core.module.Base"' + ) # Try to instantiate the imported qudi module class try: - self._instance = mod_class(qudi_main_weakref=self._qudi_main_ref, - name=self._name, - config=self._options) + self._instance = mod_class( + qudi_main_weakref=self._qudi_main_ref, + name=self._name, + config=self._options, + ) except BaseException as e: self._instance = None - raise RuntimeError(f'Error during initialization of qudi module ' - f'"qudi.{self._base}.{self._module}.{self._class}"') + raise RuntimeError( + f"Error during initialization of qudi module " + f'"qudi.{self._base}.{self._module}.{self._class}"' + ) finally: self.__last_state = self.state self.sigStateChanged.emit(self._base, self._name, self.__last_state) @@ -718,15 +824,20 @@ def _connect(self): with self._lock: # Check if module has already been loaded/instantiated if not self.is_loaded: - raise RuntimeError(f'Connection failed. No module instance found for module ' - f'"{self._base}.{self._name}".') + raise RuntimeError( + f"Connection failed. No module instance found for module " + f'"{self._base}.{self._name}".' + ) # Collect all module instances required by connector config module_instances = { - module_ref().name: module_ref().instance for module_ref in self.required_modules + module_ref().name: module_ref().instance + for module_ref in self.required_modules + } + module_connections = { + conn_name: module_instances[mod_name] + for conn_name, mod_name in self._connect_cfg.items() } - module_connections = {conn_name: module_instances[mod_name] for conn_name, mod_name in - self._connect_cfg.items()} # Apply module connections self._instance.connect_modules(module_connections) From c491d2271b21f196f3f45d2b543510b83a7d9fe4 Mon Sep 17 00:00:00 2001 From: NicolasStaudenmaier Date: Wed, 8 May 2024 17:22:17 +0200 Subject: [PATCH 03/26] add gui implementation for saving status variables --- src/qudi/core/gui/main_gui/main_gui.py | 3 +++ src/qudi/core/gui/main_gui/mainwindow.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index a8110280b..3242eb0f4 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -126,6 +126,8 @@ 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) self.mw.action_view_default.triggered.connect(self.reset_default_layout) # Connect signals from manager qudi_main.configuration.sigConfigChanged.connect(self.update_config_widget) @@ -152,6 +154,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) diff --git a/src/qudi/core/gui/main_gui/mainwindow.py b/src/qudi/core/gui/main_gui/mainwindow.py index 3053a4244..e27959b3f 100644 --- a/src/qudi/core/gui/main_gui/mainwindow.py +++ b/src/qudi/core/gui/main_gui/mainwindow.py @@ -75,6 +75,11 @@ 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') + 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 +140,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 +152,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() From 4c78492831a085170375aa3b80e051b853c78dc8 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Fri, 10 May 2024 10:16:14 +0200 Subject: [PATCH 04/26] improved comment to show that only strings are stored --- src/qudi/core/modulemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 65f56c292..88a536cfa 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -315,7 +315,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) From 1166e6d91323e067e7fef68b77afca3503040765 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Fri, 10 May 2024 16:01:05 +0200 Subject: [PATCH 05/26] added automatic saving of status variables --- src/qudi/core/gui/main_gui/main_gui.py | 302 +++++++++++++++-------- src/qudi/core/gui/main_gui/mainwindow.py | 167 ++++++++----- src/qudi/core/modulemanager.py | 58 +++++ 3 files changed, 363 insertions(+), 164 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 3242eb0f4..5e67ff3a8 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 @@ -22,11 +22,13 @@ import sys import logging import subprocess +from types import MethodWrapperType import jupyter_client.kernelspec from PySide2 import QtCore, QtWidgets from qtconsole.manager import QtKernelManager from collections.abc import Mapping, Sequence, Set +from qudi.core import modulemanager from qudi.core.statusvariable import StatusVar from qudi.core.threadmanager import ThreadManager from qudi.util.paths import get_main_dir, get_default_config_dir @@ -45,16 +47,23 @@ class QudiMainGui(GuiBase): """ This class provides a GUI to the qudi main application object. """ + # status vars - _console_font_size = StatusVar(name='console_font_size', default=10) - _show_error_popups = StatusVar(name='show_error_popups', default=True) + _console_font_size = StatusVar(name="console_font_size", default=10) + _show_error_popups = StatusVar(name="show_error_popups", default=True) + _automatic_status_var_dump = StatusVar(name="automatic_status_var_dump", default=0) + _automatic_status_var_dump_interval = StatusVar( + name="automatic_status_var_dump_interval", default=5 + ) + signal_update_automatic_status_var_checkstate = QtCore.Signal(int) + signal_update_automatic_status_var_interval = QtCore.Signal(int) def __init__(self, *args, **kwargs): """Create an instance of the module. - @param object manager: - @param str name: - @param dict config: + @param object manager: + @param str name: + @param dict config: """ super().__init__(*args, **kwargs) self.error_dialog = None @@ -62,7 +71,7 @@ def __init__(self, *args, **kwargs): self._has_console = False # Flag indicating if an IPython console is available def on_activate(self): - """ Activation method called on change to active state. + """Activation method called on change to active state. This method creates the Manager main window. """ @@ -76,18 +85,25 @@ def on_activate(self): # Get qudi version number and configure statusbar and "about qudi" dialog version = self.get_qudi_version() if isinstance(version, str): - self.mw.about_qudi_dialog.version_label.setText('version {0}'.format(version)) + self.mw.about_qudi_dialog.version_label.setText( + "version {0}".format(version) + ) self.mw.version_label.setText( - ' version {0} configured from {1}' - ''.format(version, self._qudi_main.configuration.file_path)) + ' version {0} configured from {1}' + "".format(version, self._qudi_main.configuration.file_path) + ) else: self.mw.about_qudi_dialog.version_label.setText( - ' {0}' - ' , on branch {1}.'.format(version[0], version[1])) + ' {0}' + " , on branch {1}.".format(version[0], version[1]) + ) self.mw.version_label.setText( - ' {0}' - ' , on branch {1}, configured from {2}' - ''.format(version[0], version[1], self._qudi_main.configuration.file_path)) + ' {0}' + " , on branch {1}, configured from {2}" + "".format( + version[0], version[1], self._qudi_main.configuration.file_path + ) + ) self._connect_signals() @@ -105,46 +121,91 @@ def on_activate(self): self._init_remote_modules_widget() self.reset_default_layout() + # set correct automatic status variable dumping behaviour + self.mw.dump_status_variables_interval_spinbox.setValue( + self._automatic_status_var_dump_interval + ) + self.mw.checkbox_automatic_status_variable_dumping.setChecked( + self._set_automatic_status_var_check_state() + ) self.show() def on_deactivate(self): - """Close window and remove connections. - """ + """Close window and remove connections.""" self._disconnect_signals() self.stop_jupyter_widget() self._save_window_geometry(self.mw) self.mw.close() def _connect_signals(self): - get_signal_handler().sigRecordLogged.connect(self.handle_log_record, QtCore.Qt.QueuedConnection) + get_signal_handler().sigRecordLogged.connect( + self.handle_log_record, QtCore.Qt.QueuedConnection + ) qudi_main = self._qudi_main # Connect up the main windows actions - self.mw.action_quit.triggered.connect(qudi_main.prompt_quit, QtCore.Qt.QueuedConnection) + self.mw.action_quit.triggered.connect( + qudi_main.prompt_quit, QtCore.Qt.QueuedConnection + ) self.mw.action_load_configuration.triggered.connect(self.load_configuration) self.mw.action_reload_qudi.triggered.connect( - qudi_main.prompt_restart, QtCore.Qt.QueuedConnection) - self.mw.action_open_configuration_editor.triggered.connect(self.new_configuration) + qudi_main.prompt_restart, QtCore.Qt.QueuedConnection + ) + 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) + qudi_main.module_manager.start_all_modules + ) self.mw.action_dump_status_variables.triggered.connect( - qudi_main.module_manager.dump_status_variables) + qudi_main.module_manager.dump_status_variables, QtCore.Qt.QueuedConnection + ) + self.signal_update_automatic_status_var_checkstate.connect( + qudi_main.module_manager.toggle_automated_status_variable_dumping, + QtCore.Qt.QueuedConnection, + ) + self.signal_update_automatic_status_var_checkstate.connect( + self.mw.toggle_dump_status_variables_interval_spinbox, + QtCore.Qt.QueuedConnection, + ) + self.signal_update_automatic_status_var_interval.connect( + qudi_main.module_manager.automated_status_variable_dumping_timer_interval_slot, + QtCore.Qt.QueuedConnection, + ) + self.mw.checkbox_automatic_status_variable_dumping.stateChanged.connect( + self.toggle_automatic_status_var_check_state, QtCore.Qt.QueuedConnection + ) + self.mw.dump_status_variables_interval_spinbox.valueChanged.connect( + self.update_automatic_status_var_interval, 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) - qudi_main.module_manager.sigManagedModulesChanged.connect(self.update_configured_modules) + qudi_main.module_manager.sigManagedModulesChanged.connect( + self.update_configured_modules + ) qudi_main.module_manager.sigModuleStateChanged.connect(self.update_module_state) - qudi_main.module_manager.sigModuleAppDataChanged.connect(self.update_module_app_data) + qudi_main.module_manager.sigModuleAppDataChanged.connect( + self.update_module_app_data + ) # Settings dialog self.mw.settings_dialog.accepted.connect(self.apply_settings) self.mw.settings_dialog.rejected.connect(self.keep_settings) - self.error_dialog.disable_checkbox.clicked.connect(self._error_dialog_enabled_changed) + self.error_dialog.disable_checkbox.clicked.connect( + self._error_dialog_enabled_changed + ) # Modules list - self.mw.module_widget.sigActivateModule.connect(qudi_main.module_manager.activate_module) - self.mw.module_widget.sigReloadModule.connect(qudi_main.module_manager.reload_module) + self.mw.module_widget.sigActivateModule.connect( + qudi_main.module_manager.activate_module + ) + self.mw.module_widget.sigReloadModule.connect( + qudi_main.module_manager.reload_module + ) self.mw.module_widget.sigDeactivateModule.connect( - qudi_main.module_manager.deactivate_module) + qudi_main.module_manager.deactivate_module + ) self.mw.module_widget.sigCleanupModule.connect( - qudi_main.module_manager.clear_module_app_data) + qudi_main.module_manager.clear_module_app_data + ) def _disconnect_signals(self): qudi_main = self._qudi_main @@ -155,12 +216,23 @@ def _disconnect_signals(self): 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.signal_update_automatic_status_var_checkstate.disconnect() + self.signal_update_automatic_status_var_checkstate.disconnect() + self.signal_update_automatic_status_var_interval.disconnect() + self.mw.checkbox_automatic_status_variable_dumping.stateChanged.disconnect() + self.mw.dump_status_variables_interval_spinbox.valueChanged.disconnect() self.mw.action_view_default.triggered.disconnect() # Disconnect signals from manager qudi_main.configuration.sigConfigChanged.disconnect(self.update_config_widget) - qudi_main.module_manager.sigManagedModulesChanged.disconnect(self.update_configured_modules) - qudi_main.module_manager.sigModuleStateChanged.disconnect(self.update_module_state) - qudi_main.module_manager.sigModuleAppDataChanged.disconnect(self.update_module_app_data) + qudi_main.module_manager.sigManagedModulesChanged.disconnect( + self.update_configured_modules + ) + qudi_main.module_manager.sigModuleStateChanged.disconnect( + self.update_module_state + ) + qudi_main.module_manager.sigModuleAppDataChanged.disconnect( + self.update_module_app_data + ) # Settings dialog self.mw.settings_dialog.accepted.disconnect() self.mw.settings_dialog.rejected.disconnect() @@ -181,18 +253,19 @@ def _init_remote_modules_widget(self): self.mw.remote_dockwidget.setVisible(False) self.mw.action_view_remote.setVisible(False) else: - server_config = self._qudi_main.configuration['remote_modules_server'] - host = server_config['address'] - port = server_config['port'] + server_config = self._qudi_main.configuration["remote_modules_server"] + host = server_config["address"] + port = server_config["port"] self.mw.remote_widget.setVisible(True) - self.mw.remote_widget.server_label.setText(f'Server URL: rpyc://{host}:{port}/') + self.mw.remote_widget.server_label.setText( + f"Server URL: rpyc://{host}:{port}/" + ) self.mw.remote_widget.shared_module_listview.setModel( remote_server.service.shared_modules ) def show(self): - """Show the window and bring it to the top. - """ + """Show the window and bring it to the top.""" self.mw.show() self.mw.activateWindow() self.mw.raise_() @@ -216,7 +289,9 @@ def reset_default_layout(self): self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.config_dockwidget) self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.log_dockwidget) self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.remote_dockwidget) - self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.threads_dockwidget) + self.mw.addDockWidget( + QtCore.Qt.BottomDockWidgetArea, self.mw.threads_dockwidget + ) self.mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.mw.console_dockwidget) self.mw.action_view_console.setChecked(self._has_console) @@ -229,28 +304,29 @@ def handle_log_record(self, entry): @param logging.LogRecord entry: log record as returned from logging module """ - if entry.levelname in ('error', 'critical'): + if entry.levelname in ("error", "critical"): self.error_dialog.new_error(entry) return def start_jupyter_widget(self): - """ Starts a qudi IPython kernel in a separate process and connects it to the console widget - """ + """Starts a qudi IPython kernel in a separate process and connects it to the console widget""" self._has_console = False try: # Create and start kernel process - kernel_manager = QtKernelManager(kernel_name='qudi', autorestart=False) + kernel_manager = QtKernelManager(kernel_name="qudi", autorestart=False) # kernel_manager.kernel.gui = 'qt4' kernel_manager.start_kernel() # create kernel client and connect to console widget - banner = 'This is an interactive IPython console. A reference to the running qudi ' \ - 'instance can be accessed via "qudi". View the current namespace with dir().\n' \ - 'Go, play.\n' + banner = ( + "This is an interactive IPython console. A reference to the running qudi " + 'instance can be accessed via "qudi". View the current namespace with dir().\n' + "Go, play.\n" + ) self.mw.console_widget.banner = banner self.mw.console_widget.font_size = self._console_font_size self.mw.console_widget.reset_font() - self.mw.console_widget.set_default_style(colors='linux') + self.mw.console_widget.set_default_style(colors="linux") kernel_client = kernel_manager.client() kernel_client.hb_channel.time_to_dead = 10.0 kernel_client.hb_channel.kernel_died.connect(self.kernel_died_callback) @@ -258,23 +334,22 @@ def start_jupyter_widget(self): self.mw.console_widget.kernel_manager = kernel_manager self.mw.console_widget.kernel_client = kernel_client self._has_console = True - self.log.info('IPython kernel for qudi main GUI successfully started.') + self.log.info("IPython kernel for qudi main GUI successfully started.") except jupyter_client.kernelspec.NoSuchKernel: self.log.warn( - 'Qudi IPython kernelspec not installed.\n' - 'IPython console and jupyter notebook integration not available.\n' + "Qudi IPython kernelspec not installed.\n" + "IPython console and jupyter notebook integration not available.\n" 'Run "qudi-install-kernel" from within the qudi Python environment to fix this. ' ) except: self.log.exception( - 'Exception while trying to start IPython kernel for qudi main GUI. Qudi IPython ' - 'console not available.' + "Exception while trying to start IPython kernel for qudi main GUI. Qudi IPython " + "console not available." ) @QtCore.Slot() def kernel_died_callback(self): - """ - """ + """ """ try: self.mw.console_widget.kernel_client.stop_channels() except: @@ -282,34 +357,37 @@ def kernel_died_callback(self): if self._has_console: self._has_console = False self.log.error( - 'Qudi IPython kernel has unexpectedly died. This can be caused by a corrupt qudi ' + "Qudi IPython kernel has unexpectedly died. This can be caused by a corrupt qudi " 'kernelspec installation. Try to run "qudi-install-kernel" from within the qudi ' - 'Python environment and restart qudi.' + "Python environment and restart qudi." ) def stop_jupyter_widget(self): - """ Stops the qudi IPython kernel process and detaches it from the console widget - """ + """Stops the qudi IPython kernel process and detaches it from the console widget""" try: self.mw.console_widget.kernel_client.stop_channels() except: - self.log.exception('Exception while trying to shutdown qudi IPython client:') + self.log.exception( + "Exception while trying to shutdown qudi IPython client:" + ) try: self.mw.console_widget.kernel_manager.shutdown_kernel() except: - self.log.exception('Exception while trying to shutdown qudi IPython kernel:') + self.log.exception( + "Exception while trying to shutdown qudi IPython kernel:" + ) self._has_console = False - self.log.info('IPython kernel process for qudi main GUI has shut down.') + self.log.info("IPython kernel process for qudi main GUI has shut down.") def keep_settings(self): - """ Write old values into settings dialog. - """ + """Write old values into settings dialog.""" self.mw.settings_dialog.font_size_spinbox.setValue(self._console_font_size) - self.mw.settings_dialog.show_error_popups_checkbox.setChecked(self._show_error_popups) + self.mw.settings_dialog.show_error_popups_checkbox.setChecked( + self._show_error_popups + ) def apply_settings(self): - """ Apply values from settings dialog. - """ + """Apply values from settings dialog.""" # Console font size font_size = self.mw.settings_dialog.font_size_spinbox.value() self.mw.console_widget.font_size = font_size @@ -323,23 +401,22 @@ def apply_settings(self): @QtCore.Slot() def _error_dialog_enabled_changed(self): - """ Callback for the error dialog disable checkbox - """ + """Callback for the error dialog disable checkbox""" self._show_error_popups = self.error_dialog.enabled - self.mw.settings_dialog.show_error_popups_checkbox.setChecked(self._show_error_popups) + self.mw.settings_dialog.show_error_popups_checkbox.setChecked( + self._show_error_popups + ) @QtCore.Slot(object) def update_config_widget(self, config=None): - """ Clear and refill the tree widget showing the configuration. - """ + """Clear and refill the tree widget showing the configuration.""" if config is None: config = self._qudi_main.configuration self.mw.config_widget.set_config(config.config_map) @QtCore.Slot(dict) def update_configured_modules(self, modules=None): - """ Clear and refill the module list widget - """ + """Clear and refill the module list widget""" if modules is None: modules = self._qudi_main.module_manager self.mw.module_widget.update_modules(modules) @@ -354,8 +431,7 @@ def update_module_app_data(self, base, name, exists): self.mw.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. - """ + """Try to determine the software version in case the program is in a git repository.""" # Try to get repository information if qudi has been checked out as git repo if Repo is not None: try: @@ -366,31 +442,35 @@ def get_qudi_version(self): except InvalidGitRepositoryError: pass except: - self.log.exception('Unexpected error while trying to get git repo:') + self.log.exception("Unexpected error while trying to get git repo:") # Try to get qudi.core version number try: from qudi.core import __version__ + return __version__ except: - self.log.exception('Unexpected error while trying to get qudi version:') - return 'unknown' + self.log.exception("Unexpected error while trying to get qudi version:") + return "unknown" def load_configuration(self): - """ Ask the user for a file where the configuration should be loaded from - """ - filename = QtWidgets.QFileDialog.getOpenFileName(self.mw, - 'Load Configuration', - get_default_config_dir(True), - 'Configuration files (*.cfg)')[0] + """Ask the user for a file where the configuration should be loaded from""" + filename = QtWidgets.QFileDialog.getOpenFileName( + self.mw, + "Load Configuration", + get_default_config_dir(True), + "Configuration files (*.cfg)", + )[0] if filename: reply = QtWidgets.QMessageBox.question( self.mw, - 'Restart', - 'Do you want to restart to use the configuration?\n' + "Restart", + "Do you want to restart to use the configuration?\n" 'Choosing "No" will use the selected config file for the next start of Qudi.', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.No + QtWidgets.QMessageBox.Yes + | QtWidgets.QMessageBox.No + | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.No, ) if reply == QtWidgets.QMessageBox.Cancel: return @@ -399,21 +479,39 @@ def load_configuration(self): self._qudi_main.restart() def new_configuration(self): - """ Prompt the user to open the graphical config editor in a subprocess in order to + """Prompt the user to open the graphical config editor in a subprocess in order to edit/create config files for qudi. """ reply = QtWidgets.QMessageBox.question( - self.mw, - 'Open Configuration Editor', - 'Do you want open the graphical qudi configuration editor to create or edit qudi ' - 'config files?\n', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.Yes + self.mw, + "Open Configuration Editor", + "Do you want open the graphical qudi configuration editor to create or edit qudi " + "config files?\n", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.Yes, ) if reply == QtWidgets.QMessageBox.Yes: - process = subprocess.Popen(args=[sys.executable, '-m', 'tools.config_editor'], - close_fds=False, - env=os.environ.copy(), - stdin=sys.stdin, - stdout=sys.stdout, - stderr=sys.stderr) + process = subprocess.Popen( + args=[sys.executable, "-m", "tools.config_editor"], + close_fds=False, + env=os.environ.copy(), + stdin=sys.stdin, + stdout=sys.stdout, + stderr=sys.stderr, + ) + + def _set_automatic_status_var_check_state(self): + """ + Method to set the correct state of automatic status variable dumping and start the timer, if turned on. + """ + if self._automatic_status_var_dump == QtCore.Qt.Checked: + return True + return False + + def toggle_automatic_status_var_check_state(self, toggle): + self.signal_update_automatic_status_var_checkstate.emit(toggle) + self._automatic_status_var_dump = toggle + + def update_automatic_status_var_interval(self, interval): + self.signal_update_automatic_status_var_interval.emit(interval) + self._automatic_status_var_dump_interval = interval diff --git a/src/qudi/core/gui/main_gui/mainwindow.py b/src/qudi/core/gui/main_gui/mainwindow.py index e27959b3f..5bbe153b6 100644 --- a/src/qudi/core/gui/main_gui/mainwindow.py +++ b/src/qudi/core/gui/main_gui/mainwindow.py @@ -30,110 +30,133 @@ from qudi.util.paths import get_artwork_dir from qudi.util.widgets.advanced_dockwidget import AdvancedDockWidget from qtconsole.rich_jupyter_widget import RichJupyterWidget +from qudi.util.widgets.scientific_spinbox import ScienSpinBox class QudiMainWindow(QtWidgets.QMainWindow): """ Main Window definition for the manager GUI. """ + def __init__(self, parent=None, debug_mode=False, **kwargs): super().__init__(parent, **kwargs) - self.setWindowTitle('qudi: Manager') + self.setWindowTitle("qudi: Manager") screen_size = QtWidgets.QApplication.instance().primaryScreen().availableSize() width = (screen_size.width() * 3) // 4 height = (screen_size.height() * 3) // 4 self.resize(width, height) self.module_widget = ModuleWidget() - self.module_widget.setObjectName('moduleTabWidget') + self.module_widget.setObjectName("moduleTabWidget") self.setCentralWidget(self.module_widget) # Create actions # Toolbar actions - icon_path = os.path.join(get_artwork_dir(), 'icons') + icon_path = os.path.join(get_artwork_dir(), "icons") self.action_load_configuration = QtWidgets.QAction() self.action_load_configuration.setIcon( - QtGui.QIcon(os.path.join(icon_path, 'document-open'))) - self.action_load_configuration.setText('Load configuration') - self.action_load_configuration.setToolTip('Load configuration') + QtGui.QIcon(os.path.join(icon_path, "document-open")) + ) + self.action_load_configuration.setText("Load configuration") + self.action_load_configuration.setToolTip("Load configuration") self.action_open_configuration_editor = QtWidgets.QAction() self.action_open_configuration_editor.setIcon( - QtGui.QIcon(os.path.join(icon_path, 'document-new')) + QtGui.QIcon(os.path.join(icon_path, "document-new")) ) - self.action_open_configuration_editor.setText('New configuration') + self.action_open_configuration_editor.setText("New configuration") self.action_open_configuration_editor.setToolTip( - 'Open the graphical configuration editor for editing or creating a new qudi ' - 'configuration' + "Open the graphical configuration editor for editing or creating a new qudi " + "configuration" ) self.action_reload_qudi = QtWidgets.QAction() self.action_reload_qudi.setIcon( - QtGui.QIcon(os.path.join(icon_path, 'view-refresh'))) - self.action_reload_qudi.setText('Reload current configuration') - self.action_reload_qudi.setToolTip('Reload current configuration') + QtGui.QIcon(os.path.join(icon_path, "view-refresh")) + ) + self.action_reload_qudi.setText("Reload current configuration") + self.action_reload_qudi.setToolTip("Reload current configuration") self.action_load_all_modules = QtWidgets.QAction() self.action_load_all_modules.setIcon( - 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') + 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') + 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" + ) + self.label_automatic_status_variable_dumping = QtWidgets.QLabel() + self.label_automatic_status_variable_dumping.setText("Automatic StatusVar saving: ") + self.checkbox_automatic_status_variable_dumping = QtWidgets.QCheckBox() + self.checkbox_automatic_status_variable_dumping.setChecked(False) + self.checkbox_automatic_status_variable_dumping.setToolTip("Activate / Deactivate automatic dumping of status variables") + self.dump_status_variables_interval_spinbox = ScienSpinBox() + self.dump_status_variables_interval_spinbox.setToolTip("Time interval for automatic dumping of status variables") + self.dump_status_variables_interval_spinbox.setSuffix("min") + self.dump_status_variables_interval_spinbox.setMinimum(1) + self.dump_status_variables_interval_spinbox.setMaximum(1440) + self.dump_status_variables_interval_spinbox.setMinimumSize(QtCore.QSize(80, 0)) + self.dump_status_variables_interval_spinbox.setValue(1) # quit action self.action_quit = QtWidgets.QAction() - self.action_quit.setIcon(QtGui.QIcon(os.path.join(icon_path, 'application-exit'))) - self.action_quit.setText('Quit qudi') - self.action_quit.setToolTip('Quit qudi') - self.action_quit.setShortcut(QtGui.QKeySequence('Ctrl+Q')) + self.action_quit.setIcon( + QtGui.QIcon(os.path.join(icon_path, "application-exit")) + ) + self.action_quit.setText("Quit qudi") + self.action_quit.setToolTip("Quit qudi") + self.action_quit.setShortcut(QtGui.QKeySequence("Ctrl+Q")) # view actions self.action_view_console = QtWidgets.QAction() self.action_view_console.setCheckable(True) self.action_view_console.setChecked(True) - self.action_view_console.setText('Show console') - self.action_view_console.setToolTip('Show IPython console') + self.action_view_console.setText("Show console") + self.action_view_console.setToolTip("Show IPython console") self.action_view_log = QtWidgets.QAction() self.action_view_log.setCheckable(True) self.action_view_log.setChecked(True) - self.action_view_log.setText('Show log') - self.action_view_log.setToolTip('Show log dockwidget') + self.action_view_log.setText("Show log") + self.action_view_log.setToolTip("Show log dockwidget") self.action_view_config = QtWidgets.QAction() self.action_view_config.setCheckable(True) self.action_view_config.setChecked(False) - self.action_view_config.setText('Show configuration') - self.action_view_config.setToolTip('Show configuration dockwidget') + self.action_view_config.setText("Show configuration") + self.action_view_config.setToolTip("Show configuration dockwidget") self.action_view_remote = QtWidgets.QAction() self.action_view_remote.setCheckable(True) self.action_view_remote.setChecked(False) - self.action_view_remote.setText('Show remote') - self.action_view_remote.setToolTip('Show remote connections dockwidget') + self.action_view_remote.setText("Show remote") + self.action_view_remote.setToolTip("Show remote connections dockwidget") self.action_view_threads = QtWidgets.QAction() self.action_view_threads.setCheckable(True) self.action_view_threads.setChecked(False) - self.action_view_threads.setText('Show threads') - self.action_view_threads.setToolTip('Show threads dockwidget') + self.action_view_threads.setText("Show threads") + self.action_view_threads.setToolTip("Show threads dockwidget") self.action_view_default = QtWidgets.QAction() - self.action_view_default.setText('Restore default') - self.action_view_default.setToolTip('Restore default view') + self.action_view_default.setText("Restore default") + self.action_view_default.setToolTip("Restore default view") # Dialog actions self.action_settings = QtWidgets.QAction() - self.action_settings.setIcon(QtGui.QIcon(os.path.join(icon_path, 'configure'))) - self.action_settings.setText('Settings') - self.action_settings.setToolTip('Open settings dialog') + self.action_settings.setIcon(QtGui.QIcon(os.path.join(icon_path, "configure"))) + self.action_settings.setText("Settings") + self.action_settings.setToolTip("Open settings dialog") self.action_about_qudi = QtWidgets.QAction() - self.action_about_qudi.setIcon( - QtGui.QIcon(os.path.join(icon_path, 'go-home'))) - self.action_about_qudi.setText('About qudi') - self.action_about_qudi.setToolTip('Read up about qudi') + self.action_about_qudi.setIcon(QtGui.QIcon(os.path.join(icon_path, "go-home"))) + self.action_about_qudi.setText("About qudi") + self.action_about_qudi.setToolTip("Read up about qudi") self.action_about_qt = QtWidgets.QAction() - self.action_about_qt.setIcon( - QtGui.QIcon(os.path.join(icon_path, 'go-home'))) - self.action_about_qt.setText('About Qt') - self.action_about_qt.setToolTip('Read up about Qt') + self.action_about_qt.setIcon(QtGui.QIcon(os.path.join(icon_path, "go-home"))) + self.action_about_qt.setText("About Qt") + self.action_about_qt.setToolTip("Read up about Qt") # Create toolbar self.toolbar = QtWidgets.QToolBar() - self.toolbar.setObjectName('QudiMainWindow Toolbar') + self.toolbar.setObjectName("QudiMainWindow Toolbar") self.toolbar.setOrientation(QtCore.Qt.Horizontal) self.toolbar.addAction(self.action_open_configuration_editor) self.toolbar.addAction(self.action_load_configuration) @@ -141,11 +164,14 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.toolbar.addSeparator() self.toolbar.addAction(self.action_load_all_modules) self.toolbar.addAction(self.action_dump_status_variables) + self.toolbar.addWidget(self.label_automatic_status_variable_dumping) + self.toolbar.addWidget(self.checkbox_automatic_status_variable_dumping) + self.toolbar.addWidget(self.dump_status_variables_interval_spinbox) self.addToolBar(self.toolbar) # Create menu bar self.menubar = QtWidgets.QMenuBar() - menu = QtWidgets.QMenu('File') + menu = QtWidgets.QMenu("File") menu.addAction(self.action_open_configuration_editor) menu.addSeparator() menu.addAction(self.action_load_configuration) @@ -158,7 +184,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): menu.addSeparator() menu.addAction(self.action_quit) self.menubar.addMenu(menu) - menu = QtWidgets.QMenu('View') + menu = QtWidgets.QMenu("View") menu.addAction(self.action_view_console) menu.addAction(self.action_view_log) menu.addAction(self.action_view_config) @@ -167,7 +193,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): menu.addSeparator() menu.addAction(self.action_view_default) self.menubar.addMenu(menu) - menu = QtWidgets.QMenu('About') + menu = QtWidgets.QMenu("About") menu.addAction(self.action_about_qudi) menu.addAction(self.action_about_qt) self.menubar.addMenu(menu) @@ -182,33 +208,34 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): # Create dialogues self.about_qudi_dialog = AboutQudiDialog() - self.about_qudi_dialog.setWindowTitle('About qudi') + self.about_qudi_dialog.setWindowTitle("About qudi") self.settings_dialog = SettingsDialog() # Create dockwidgets self.config_widget = ConfigQTreeWidget() - self.config_dockwidget = AdvancedDockWidget('Configuration') + self.config_dockwidget = AdvancedDockWidget("Configuration") self.config_dockwidget.setWidget(self.config_widget) self.config_dockwidget.setAllowedAreas( QtCore.Qt.BottomDockWidgetArea | QtCore.Qt.LeftDockWidgetArea ) self.log_widget = LogWidget(debug_mode=debug_mode) - self.log_dockwidget = AdvancedDockWidget('Log') + self.log_dockwidget = AdvancedDockWidget("Log") self.log_dockwidget.setWidget(self.log_widget) self.log_dockwidget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) self.remote_widget = RemoteWidget() - self.remote_dockwidget = AdvancedDockWidget('Remote modules') + self.remote_dockwidget = AdvancedDockWidget("Remote modules") self.remote_dockwidget.setWidget(self.remote_widget) self.remote_dockwidget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) self.threads_widget = QtWidgets.QListView() - self.threads_dockwidget = AdvancedDockWidget('Threads') + self.threads_dockwidget = AdvancedDockWidget("Threads") self.threads_dockwidget.setWidget(self.threads_widget) self.threads_dockwidget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) self.console_widget = RichJupyterWidget() - self.console_dockwidget = AdvancedDockWidget('Console') + self.console_dockwidget = AdvancedDockWidget("Console") self.console_dockwidget.setWidget(self.console_widget) self.console_dockwidget.setAllowedAreas( - QtCore.Qt.RightDockWidgetArea | QtCore.Qt.LeftDockWidgetArea) + QtCore.Qt.RightDockWidgetArea | QtCore.Qt.LeftDockWidgetArea + ) # Add dockwidgets to main window self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.config_dockwidget) @@ -218,15 +245,21 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.console_dockwidget) # Synchronize dockwidget visibility change signals - self.config_dockwidget.sigClosed.connect(lambda: self.action_view_config.setChecked(False)) + self.config_dockwidget.sigClosed.connect( + lambda: self.action_view_config.setChecked(False) + ) self.log_dockwidget.sigClosed.connect( - lambda: self.action_view_log.setChecked(False)) + lambda: self.action_view_log.setChecked(False) + ) self.remote_dockwidget.sigClosed.connect( - lambda: self.action_view_remote.setChecked(False)) + lambda: self.action_view_remote.setChecked(False) + ) self.threads_dockwidget.sigClosed.connect( - lambda: self.action_view_threads.setChecked(False)) + lambda: self.action_view_threads.setChecked(False) + ) self.console_dockwidget.sigClosed.connect( - lambda: self.action_view_console.setChecked(False)) + lambda: self.action_view_console.setChecked(False) + ) self.action_view_config.toggled.connect(self.config_dockwidget.setVisible) self.action_view_log.toggled.connect(self.log_dockwidget.setVisible) self.action_view_remote.toggled.connect(self.remote_dockwidget.setVisible) @@ -238,3 +271,13 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.action_about_qt.triggered.connect(QtWidgets.QApplication.aboutQt) self.action_settings.triggered.connect(self.settings_dialog.exec_) # modal return + + def toggle_dump_status_variables_interval_spinbox(self, toggle): + """ + Method that toggles active status of the dump_status_variables_interval_spinbox spinbox. + @param int toggle: Indicates whether to set spinbox as active or not. + """ + if toggle != QtCore.Qt.Checked: + self.dump_status_variables_interval_spinbox.setEnabled(False) + return + self.dump_status_variables_interval_spinbox.setEnabled(True) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 88a536cfa..e235c38e1 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -64,6 +64,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 = 1 @classmethod def instance(cls): @@ -283,6 +284,63 @@ def dump_status_variables(self): if module.is_active: module.instance._dump_status_variables() + def toggle_automated_status_variable_dumping(self, toggle): + """ + Method that creates or destroys the QTimer for handling the automatic dumping of status variables. + + @param int toogle: boolean that determines whether timer is created or destroyed. + """ + if toggle != QtCore.Qt.Checked: + logger.info(f"Automated status variable saving disabled.") + self.automated_status_variable_dumping_timer.timeout.disconnect() + self.automated_status_variable_dumping_timer.stop() + self.automated_status_variable_dumping_timer.deleteLater() + return + + logger.info( + f"Automated status variable saving enabled with interval {self.automated_status_variable_dumping_timer_interval} min." + ) + self.automated_status_variable_dumping_timer = QtCore.QTimer() + self.automated_status_variable_dumping_timer.setInterval( + self.automated_status_variable_dumping_timer_interval * 60e3 + ) + self.automated_status_variable_dumping_timer.timeout.connect( + self.dump_status_variables, QtCore.Qt.QueuedConnection + ) + self.automated_status_variable_dumping_timer.start() + + @property + def automated_status_variable_dumping_timer_interval(self): + return self._automated_status_variable_dumping_timer_interval + + @automated_status_variable_dumping_timer_interval.setter + def automated_status_variable_dumping_timer_interval(self, interval): + """ + Setter method for the timer used to automatically dump status variables. + + @param int interval: interval of the timer in min + """ + self._automated_status_variable_dumping_timer_interval = interval + + def automated_status_variable_dumping_timer_interval_slot(self, interval): + """ + Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter. + + @param int interval: interval of the timer in min + """ + logger.info( + f"Setting automated status variable saving timer interval to {interval} min." + ) + self.automated_status_variable_dumping_timer_interval = interval + if not hasattr(self, "automated_status_variable_dumping_timer"): + return + if ( + self.automated_status_variable_dumping_timer is None + and self.automated_status_variable_dumping_timer.isDeleted() + ): + return + self.automated_status_variable_dumping_timer.setInterval(interval * 60e3) + class ManagedModule(QtCore.QObject): """Object representing a qudi module (gui, logic or hardware) to be managed by the qudi Manager From bfab2ebb78bb6c0cf7348134b69a257026c4b528 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 13 May 2024 18:14:18 +0200 Subject: [PATCH 06/26] moved automatic status variable dumping to settings dialog --- src/qudi/core/gui/main_gui/main_gui.py | 51 +++++++++------- src/qudi/core/gui/main_gui/mainwindow.py | 26 -------- src/qudi/core/gui/main_gui/settingsdialog.py | 62 ++++++++++++++++---- src/qudi/core/modulemanager.py | 17 ++---- 4 files changed, 85 insertions(+), 71 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 5e67ff3a8..512862c2c 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -122,10 +122,10 @@ def on_activate(self): self.reset_default_layout() # set correct automatic status variable dumping behaviour - self.mw.dump_status_variables_interval_spinbox.setValue( + self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( self._automatic_status_var_dump_interval ) - self.mw.checkbox_automatic_status_variable_dumping.setChecked( + self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked( self._set_automatic_status_var_check_state() ) self.show() @@ -164,18 +164,15 @@ def _connect_signals(self): QtCore.Qt.QueuedConnection, ) self.signal_update_automatic_status_var_checkstate.connect( - self.mw.toggle_dump_status_variables_interval_spinbox, + self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox, QtCore.Qt.QueuedConnection, ) self.signal_update_automatic_status_var_interval.connect( qudi_main.module_manager.automated_status_variable_dumping_timer_interval_slot, QtCore.Qt.QueuedConnection, ) - self.mw.checkbox_automatic_status_variable_dumping.stateChanged.connect( - self.toggle_automatic_status_var_check_state, QtCore.Qt.QueuedConnection - ) - self.mw.dump_status_variables_interval_spinbox.valueChanged.connect( - self.update_automatic_status_var_interval, QtCore.Qt.QueuedConnection + self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.connect( + self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox ) self.mw.action_view_default.triggered.connect(self.reset_default_layout) # Connect signals from manager @@ -217,10 +214,9 @@ def _disconnect_signals(self): self.mw.action_load_all_modules.triggered.disconnect() self.mw.action_dump_status_variables.triggered.disconnect() self.signal_update_automatic_status_var_checkstate.disconnect() - self.signal_update_automatic_status_var_checkstate.disconnect() self.signal_update_automatic_status_var_interval.disconnect() - self.mw.checkbox_automatic_status_variable_dumping.stateChanged.disconnect() - self.mw.dump_status_variables_interval_spinbox.valueChanged.disconnect() + self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.disconnect() + self.mw.settings_dialog.dump_status_variables_interval_spinbox.valueChanged.disconnect() self.mw.action_view_default.triggered.disconnect() # Disconnect signals from manager qudi_main.configuration.sigConfigChanged.disconnect(self.update_config_widget) @@ -385,6 +381,12 @@ def keep_settings(self): self.mw.settings_dialog.show_error_popups_checkbox.setChecked( self._show_error_popups ) + self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked( + self._automatic_status_var_dump + ) + self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( + self._automatic_status_var_dump_interval + ) def apply_settings(self): """Apply values from settings dialog.""" @@ -399,6 +401,17 @@ def apply_settings(self): self.error_dialog.set_enabled(error_popups) self._show_error_popups = error_popups + # Automatic status variable dumping + interval = ( + self.mw.settings_dialog.dump_status_variables_interval_spinbox.value() + ) + self.signal_update_automatic_status_var_interval.emit(interval) + self._automatic_status_var_dump_interval = interval + + toggle = self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.checkState() + self.signal_update_automatic_status_var_checkstate.emit(toggle) + self._automatic_status_var_dump = toggle + @QtCore.Slot() def _error_dialog_enabled_changed(self): """Callback for the error dialog disable checkbox""" @@ -502,16 +515,10 @@ def new_configuration(self): def _set_automatic_status_var_check_state(self): """ - Method to set the correct state of automatic status variable dumping and start the timer, if turned on. + Method to set the correct state of automatic status variable dumping. """ + toggle = False if self._automatic_status_var_dump == QtCore.Qt.Checked: - return True - return False - - def toggle_automatic_status_var_check_state(self, toggle): - self.signal_update_automatic_status_var_checkstate.emit(toggle) - self._automatic_status_var_dump = toggle - - def update_automatic_status_var_interval(self, interval): - self.signal_update_automatic_status_var_interval.emit(interval) - self._automatic_status_var_dump_interval = interval + toggle = True + self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox(toggle) + return toggle diff --git a/src/qudi/core/gui/main_gui/mainwindow.py b/src/qudi/core/gui/main_gui/mainwindow.py index 5bbe153b6..dab667e30 100644 --- a/src/qudi/core/gui/main_gui/mainwindow.py +++ b/src/qudi/core/gui/main_gui/mainwindow.py @@ -30,7 +30,6 @@ from qudi.util.paths import get_artwork_dir from qudi.util.widgets.advanced_dockwidget import AdvancedDockWidget from qtconsole.rich_jupyter_widget import RichJupyterWidget -from qudi.util.widgets.scientific_spinbox import ScienSpinBox class QudiMainWindow(QtWidgets.QMainWindow): @@ -91,18 +90,6 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.action_dump_status_variables.setToolTip( "Save Status Variables of all active modules" ) - self.label_automatic_status_variable_dumping = QtWidgets.QLabel() - self.label_automatic_status_variable_dumping.setText("Automatic StatusVar saving: ") - self.checkbox_automatic_status_variable_dumping = QtWidgets.QCheckBox() - self.checkbox_automatic_status_variable_dumping.setChecked(False) - self.checkbox_automatic_status_variable_dumping.setToolTip("Activate / Deactivate automatic dumping of status variables") - self.dump_status_variables_interval_spinbox = ScienSpinBox() - self.dump_status_variables_interval_spinbox.setToolTip("Time interval for automatic dumping of status variables") - self.dump_status_variables_interval_spinbox.setSuffix("min") - self.dump_status_variables_interval_spinbox.setMinimum(1) - self.dump_status_variables_interval_spinbox.setMaximum(1440) - self.dump_status_variables_interval_spinbox.setMinimumSize(QtCore.QSize(80, 0)) - self.dump_status_variables_interval_spinbox.setValue(1) # quit action self.action_quit = QtWidgets.QAction() self.action_quit.setIcon( @@ -164,9 +151,6 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.toolbar.addSeparator() self.toolbar.addAction(self.action_load_all_modules) self.toolbar.addAction(self.action_dump_status_variables) - self.toolbar.addWidget(self.label_automatic_status_variable_dumping) - self.toolbar.addWidget(self.checkbox_automatic_status_variable_dumping) - self.toolbar.addWidget(self.dump_status_variables_interval_spinbox) self.addToolBar(self.toolbar) # Create menu bar @@ -271,13 +255,3 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.action_about_qt.triggered.connect(QtWidgets.QApplication.aboutQt) self.action_settings.triggered.connect(self.settings_dialog.exec_) # modal return - - def toggle_dump_status_variables_interval_spinbox(self, toggle): - """ - Method that toggles active status of the dump_status_variables_interval_spinbox spinbox. - @param int toggle: Indicates whether to set spinbox as active or not. - """ - if toggle != QtCore.Qt.Checked: - self.dump_status_variables_interval_spinbox.setEnabled(False) - return - self.dump_status_variables_interval_spinbox.setEnabled(True) diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index cdc1d845e..e6c2c7f40 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -20,15 +20,17 @@ """ from PySide2 import QtCore, QtWidgets +from qudi.util.widgets.scientific_spinbox import ScienSpinBox class SettingsDialog(QtWidgets.QDialog): """ Custom QDialog widget for configuration of the qudi main GUI """ + def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) - self.setWindowTitle('Qudi: Main GUI settings') + self.setWindowTitle("Qudi: Main GUI settings") # Create main layout # Add widgets to layout and set as main layout @@ -38,31 +40,69 @@ def __init__(self, parent=None, **kwargs): # Create widgets and add them to the layout self.font_size_spinbox = QtWidgets.QSpinBox() - self.font_size_spinbox.setObjectName('fontSizeSpinBox') + self.font_size_spinbox.setObjectName("fontSizeSpinBox") self.font_size_spinbox.setMinimum(5) self.font_size_spinbox.setValue(10) - label = QtWidgets.QLabel('Console font size:') - label.setObjectName('fontSizeLabel') + label = QtWidgets.QLabel("Console font size:") + label.setObjectName("fontSizeLabel") label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(label, 0, 0) layout.addWidget(self.font_size_spinbox, 0, 1) self.show_error_popups_checkbox = QtWidgets.QCheckBox() - self.show_error_popups_checkbox.setObjectName('showErrorPopupsCheckbox') + self.show_error_popups_checkbox.setObjectName("showErrorPopupsCheckbox") self.show_error_popups_checkbox.setChecked(True) - label = QtWidgets.QLabel('Show error popups:') - label.setObjectName('showErrorPopupsLabel') + label = QtWidgets.QLabel("Show error popups:") + label.setObjectName("showErrorPopupsLabel") label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(label, 1, 0) layout.addWidget(self.show_error_popups_checkbox, 1, 1) - buttonbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok - | QtWidgets.QDialogButtonBox.Cancel - | QtWidgets.QDialogButtonBox.Apply) + label = QtWidgets.QLabel("Automatic StatusVar saving") + label.setObjectName("autoStatusVarSavingLabel") + label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.checkbox_automatic_status_variable_dumping = QtWidgets.QCheckBox() + self.checkbox_automatic_status_variable_dumping.setChecked(False) + self.checkbox_automatic_status_variable_dumping.setToolTip( + "Activate / Deactivate automatic dumping of status variables" + ) + layout.addWidget(label, 2, 0) + layout.addWidget(self.checkbox_automatic_status_variable_dumping, 2, 1) + + label = QtWidgets.QLabel("Automatic StatusVar saving interval") + label.setObjectName("autoStatusVarSavingIntervalLabel") + label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + self.dump_status_variables_interval_spinbox = ScienSpinBox() + self.dump_status_variables_interval_spinbox.setToolTip( + "Time interval for automatic dumping of status variables" + ) + self.dump_status_variables_interval_spinbox.setSuffix("min") + self.dump_status_variables_interval_spinbox.setMinimum(1) + self.dump_status_variables_interval_spinbox.setMaximum(1440) + self.dump_status_variables_interval_spinbox.setMinimumSize(QtCore.QSize(80, 0)) + self.dump_status_variables_interval_spinbox.setValue(1) + layout.addWidget(label, 3, 0) + layout.addWidget(self.dump_status_variables_interval_spinbox, 3, 1) + + 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, 4, 0, 1, 2) # Add internal signals buttonbox.accepted.connect(self.accept) buttonbox.rejected.connect(self.reject) buttonbox.button(buttonbox.Apply).clicked.connect(self.accepted) + + def toggle_dump_status_variables_interval_spinbox(self, toggle): + """ + Method that toggles active status of the dump_status_variables_interval_spinbox spinbox. + @param int toggle: Indicates whether to set spinbox as active or not. + """ + if toggle != QtCore.Qt.Checked: + self.dump_status_variables_interval_spinbox.setEnabled(False) + return + self.dump_status_variables_interval_spinbox.setEnabled(True) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index e235c38e1..5741a860f 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -48,6 +48,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: @@ -292,15 +293,14 @@ def toggle_automated_status_variable_dumping(self, toggle): """ if toggle != QtCore.Qt.Checked: logger.info(f"Automated status variable saving disabled.") - self.automated_status_variable_dumping_timer.timeout.disconnect() self.automated_status_variable_dumping_timer.stop() - self.automated_status_variable_dumping_timer.deleteLater() + self.automated_status_variable_dumping_timer.timeout.disconnect() return logger.info( f"Automated status variable saving enabled with interval {self.automated_status_variable_dumping_timer_interval} min." ) - self.automated_status_variable_dumping_timer = QtCore.QTimer() + self.automated_status_variable_dumping_timer.setInterval( self.automated_status_variable_dumping_timer_interval * 60e3 ) @@ -328,18 +328,11 @@ def automated_status_variable_dumping_timer_interval_slot(self, interval): @param int interval: interval of the timer in min """ + self.automated_status_variable_dumping_timer_interval = interval + self.automated_status_variable_dumping_timer.setInterval(interval * 60e3) logger.info( f"Setting automated status variable saving timer interval to {interval} min." ) - self.automated_status_variable_dumping_timer_interval = interval - if not hasattr(self, "automated_status_variable_dumping_timer"): - return - if ( - self.automated_status_variable_dumping_timer is None - and self.automated_status_variable_dumping_timer.isDeleted() - ): - return - self.automated_status_variable_dumping_timer.setInterval(interval * 60e3) class ManagedModule(QtCore.QObject): From 9a3cae41a34ece56d6844c1577fc2656d87a3b9e Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 10 Jun 2024 09:17:53 +0200 Subject: [PATCH 07/26] fixed automatic status variable saving --- src/qudi/core/gui/main_gui/main_gui.py | 21 +++++++++----------- src/qudi/core/gui/main_gui/settingsdialog.py | 7 ++----- src/qudi/core/modulemanager.py | 9 ++++++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 512862c2c..518062d96 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -51,11 +51,13 @@ class QudiMainGui(GuiBase): # status vars _console_font_size = StatusVar(name="console_font_size", default=10) _show_error_popups = StatusVar(name="show_error_popups", default=True) - _automatic_status_var_dump = StatusVar(name="automatic_status_var_dump", default=0) + _automatic_status_var_dump = StatusVar( + name="automatic_status_var_dump", default=False + ) _automatic_status_var_dump_interval = StatusVar( name="automatic_status_var_dump_interval", default=5 ) - signal_update_automatic_status_var_checkstate = QtCore.Signal(int) + signal_update_automatic_status_var_checkstate = QtCore.Signal(bool) signal_update_automatic_status_var_interval = QtCore.Signal(int) def __init__(self, *args, **kwargs): @@ -125,8 +127,8 @@ def on_activate(self): self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( self._automatic_status_var_dump_interval ) - self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked( - self._set_automatic_status_var_check_state() + self.signal_update_automatic_status_var_checkstate.emit( + self._automatic_status_var_dump ) self.show() @@ -216,7 +218,6 @@ def _disconnect_signals(self): self.signal_update_automatic_status_var_checkstate.disconnect() self.signal_update_automatic_status_var_interval.disconnect() self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.disconnect() - self.mw.settings_dialog.dump_status_variables_interval_spinbox.valueChanged.disconnect() self.mw.action_view_default.triggered.disconnect() # Disconnect signals from manager qudi_main.configuration.sigConfigChanged.disconnect(self.update_config_widget) @@ -408,7 +409,7 @@ def apply_settings(self): self.signal_update_automatic_status_var_interval.emit(interval) self._automatic_status_var_dump_interval = interval - toggle = self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.checkState() + toggle = self._get_automatic_status_var_check_state() self.signal_update_automatic_status_var_checkstate.emit(toggle) self._automatic_status_var_dump = toggle @@ -513,12 +514,8 @@ def new_configuration(self): stderr=sys.stderr, ) - def _set_automatic_status_var_check_state(self): + def _get_automatic_status_var_check_state(self): """ Method to set the correct state of automatic status variable dumping. """ - toggle = False - if self._automatic_status_var_dump == QtCore.Qt.Checked: - toggle = True - self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox(toggle) - return toggle + return self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.isChecked() diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index e6c2c7f40..67d7b31b1 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -100,9 +100,6 @@ def __init__(self, parent=None, **kwargs): def toggle_dump_status_variables_interval_spinbox(self, toggle): """ Method that toggles active status of the dump_status_variables_interval_spinbox spinbox. - @param int toggle: Indicates whether to set spinbox as active or not. + @param bool toggle: Indicates whether to set spinbox as active or not. """ - if toggle != QtCore.Qt.Checked: - self.dump_status_variables_interval_spinbox.setEnabled(False) - return - self.dump_status_variables_interval_spinbox.setEnabled(True) + self.dump_status_variables_interval_spinbox.setEnabled(toggle) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 5741a860f..3f72a7881 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -289,12 +289,15 @@ def toggle_automated_status_variable_dumping(self, toggle): """ Method that creates or destroys the QTimer for handling the automatic dumping of status variables. - @param int toogle: boolean that determines whether timer is created or destroyed. + @param bool toggle: boolean that determines whether timer is created or destroyed. """ - if toggle != QtCore.Qt.Checked: + if not toggle: logger.info(f"Automated status variable saving disabled.") self.automated_status_variable_dumping_timer.stop() - self.automated_status_variable_dumping_timer.timeout.disconnect() + try: + self.automated_status_variable_dumping_timer.timeout.disconnect() + except RuntimeError: + pass return logger.info( From 391d8ce798c151cfd4b5d7e7652a2a4efcc45ac6 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Thu, 13 Jun 2024 14:10:14 +0200 Subject: [PATCH 08/26] fixed imports --- src/qudi/core/gui/main_gui/main_gui.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 518062d96..aae6d3646 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -20,15 +20,11 @@ import os import sys -import logging import subprocess -from types import MethodWrapperType import jupyter_client.kernelspec from PySide2 import QtCore, QtWidgets from qtconsole.manager import QtKernelManager -from collections.abc import Mapping, Sequence, Set -from qudi.core import modulemanager from qudi.core.statusvariable import StatusVar from qudi.core.threadmanager import ThreadManager from qudi.util.paths import get_main_dir, get_default_config_dir @@ -174,7 +170,8 @@ def _connect_signals(self): QtCore.Qt.QueuedConnection, ) self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.connect( - self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox + self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox, + QtCore.Qt.QueuedConnection, ) self.mw.action_view_default.triggered.connect(self.reset_default_layout) # Connect signals from manager From 9a41c4fad09bb38a21ad11ef88d23188365a02ec Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Thu, 13 Jun 2024 14:12:47 +0200 Subject: [PATCH 09/26] fixed imports --- src/qudi/core/modulemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 3f72a7881..4ea8ba926 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 From 4c01dbb26efb9d8df92746b28f2f81e94ceec59d Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Fri, 14 Jun 2024 15:05:11 +0200 Subject: [PATCH 10/26] docstring fixes and code cleanup --- src/qudi/core/gui/main_gui/main_gui.py | 12 +++--------- src/qudi/core/gui/main_gui/settingsdialog.py | 7 ------- src/qudi/core/modulemanager.py | 8 +++++++- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index aae6d3646..fea9f41c9 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -162,7 +162,7 @@ def _connect_signals(self): QtCore.Qt.QueuedConnection, ) self.signal_update_automatic_status_var_checkstate.connect( - self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox, + self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled, QtCore.Qt.QueuedConnection, ) self.signal_update_automatic_status_var_interval.connect( @@ -170,7 +170,7 @@ def _connect_signals(self): QtCore.Qt.QueuedConnection, ) self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.connect( - self.mw.settings_dialog.toggle_dump_status_variables_interval_spinbox, + self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled, QtCore.Qt.QueuedConnection, ) self.mw.action_view_default.triggered.connect(self.reset_default_layout) @@ -406,7 +406,7 @@ def apply_settings(self): self.signal_update_automatic_status_var_interval.emit(interval) self._automatic_status_var_dump_interval = interval - toggle = self._get_automatic_status_var_check_state() + toggle = self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.isChecked() self.signal_update_automatic_status_var_checkstate.emit(toggle) self._automatic_status_var_dump = toggle @@ -510,9 +510,3 @@ def new_configuration(self): stdout=sys.stdout, stderr=sys.stderr, ) - - def _get_automatic_status_var_check_state(self): - """ - Method to set the correct state of automatic status variable dumping. - """ - return self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.isChecked() diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index 67d7b31b1..1fc4770e1 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -96,10 +96,3 @@ def __init__(self, parent=None, **kwargs): buttonbox.accepted.connect(self.accept) buttonbox.rejected.connect(self.reject) buttonbox.button(buttonbox.Apply).clicked.connect(self.accepted) - - def toggle_dump_status_variables_interval_spinbox(self, toggle): - """ - Method that toggles active status of the dump_status_variables_interval_spinbox spinbox. - @param bool toggle: Indicates whether to set spinbox as active or not. - """ - self.dump_status_variables_interval_spinbox.setEnabled(toggle) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 4ea8ba926..3ed3cc329 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -314,6 +314,11 @@ def toggle_automated_status_variable_dumping(self, toggle): @property def automated_status_variable_dumping_timer_interval(self): + """ + Property for the timer interval of the automatic status variable saving in min. + + @return int: timer interval in min + """ return self._automated_status_variable_dumping_timer_interval @automated_status_variable_dumping_timer_interval.setter @@ -327,7 +332,8 @@ def automated_status_variable_dumping_timer_interval(self, interval): def automated_status_variable_dumping_timer_interval_slot(self, interval): """ - Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter. + Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter + and simultaneously sets the interval of the timer. @param int interval: interval of the timer in min """ From 9cea7f48a7b03ffb7190933a3d5c6762a57ece93 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Thu, 1 Aug 2024 13:16:52 +0200 Subject: [PATCH 11/26] fixed incorrect startup timer interval --- src/qudi/core/gui/main_gui/main_gui.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index fea9f41c9..76f09c151 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -123,6 +123,9 @@ def on_activate(self): self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( self._automatic_status_var_dump_interval ) + self.signal_update_automatic_status_var_interval.emit( + self._automatic_status_var_dump_interval + ) self.signal_update_automatic_status_var_checkstate.emit( self._automatic_status_var_dump ) From 9a8e38d7b0091b5a764ed540e7c6dd7da859ce94 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 12 Aug 2024 12:19:41 +0200 Subject: [PATCH 12/26] reverted automatic code style changes --- src/qudi/core/gui/main_gui/mainwindow.py | 132 +++++----- src/qudi/core/gui/main_gui/settingsdialog.py | 23 +- src/qudi/core/module.py | 253 ++++++++----------- 3 files changed, 180 insertions(+), 228 deletions(-) diff --git a/src/qudi/core/gui/main_gui/mainwindow.py b/src/qudi/core/gui/main_gui/mainwindow.py index dab667e30..852af9942 100644 --- a/src/qudi/core/gui/main_gui/mainwindow.py +++ b/src/qudi/core/gui/main_gui/mainwindow.py @@ -36,51 +36,46 @@ class QudiMainWindow(QtWidgets.QMainWindow): """ Main Window definition for the manager GUI. """ - def __init__(self, parent=None, debug_mode=False, **kwargs): super().__init__(parent, **kwargs) - self.setWindowTitle("qudi: Manager") + self.setWindowTitle('qudi: Manager') screen_size = QtWidgets.QApplication.instance().primaryScreen().availableSize() width = (screen_size.width() * 3) // 4 height = (screen_size.height() * 3) // 4 self.resize(width, height) self.module_widget = ModuleWidget() - self.module_widget.setObjectName("moduleTabWidget") + self.module_widget.setObjectName('moduleTabWidget') self.setCentralWidget(self.module_widget) # Create actions # Toolbar actions - icon_path = os.path.join(get_artwork_dir(), "icons") + icon_path = os.path.join(get_artwork_dir(), 'icons') self.action_load_configuration = QtWidgets.QAction() self.action_load_configuration.setIcon( - QtGui.QIcon(os.path.join(icon_path, "document-open")) - ) - self.action_load_configuration.setText("Load configuration") - self.action_load_configuration.setToolTip("Load configuration") + QtGui.QIcon(os.path.join(icon_path, 'document-open'))) + self.action_load_configuration.setText('Load configuration') + self.action_load_configuration.setToolTip('Load configuration') self.action_open_configuration_editor = QtWidgets.QAction() self.action_open_configuration_editor.setIcon( - QtGui.QIcon(os.path.join(icon_path, "document-new")) + QtGui.QIcon(os.path.join(icon_path, 'document-new')) ) - self.action_open_configuration_editor.setText("New configuration") + self.action_open_configuration_editor.setText('New configuration') self.action_open_configuration_editor.setToolTip( - "Open the graphical configuration editor for editing or creating a new qudi " - "configuration" + 'Open the graphical configuration editor for editing or creating a new qudi ' + 'configuration' ) self.action_reload_qudi = QtWidgets.QAction() self.action_reload_qudi.setIcon( - QtGui.QIcon(os.path.join(icon_path, "view-refresh")) - ) - self.action_reload_qudi.setText("Reload current configuration") - self.action_reload_qudi.setToolTip("Reload current configuration") + QtGui.QIcon(os.path.join(icon_path, 'view-refresh'))) + self.action_reload_qudi.setText('Reload current configuration') + self.action_reload_qudi.setToolTip('Reload current configuration') self.action_load_all_modules = QtWidgets.QAction() self.action_load_all_modules.setIcon( - 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" - ) + 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( @@ -92,58 +87,58 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): ) # quit action self.action_quit = QtWidgets.QAction() - self.action_quit.setIcon( - QtGui.QIcon(os.path.join(icon_path, "application-exit")) - ) - self.action_quit.setText("Quit qudi") - self.action_quit.setToolTip("Quit qudi") - self.action_quit.setShortcut(QtGui.QKeySequence("Ctrl+Q")) + self.action_quit.setIcon(QtGui.QIcon(os.path.join(icon_path, 'application-exit'))) + self.action_quit.setText('Quit qudi') + self.action_quit.setToolTip('Quit qudi') + self.action_quit.setShortcut(QtGui.QKeySequence('Ctrl+Q')) # view actions self.action_view_console = QtWidgets.QAction() self.action_view_console.setCheckable(True) self.action_view_console.setChecked(True) - self.action_view_console.setText("Show console") - self.action_view_console.setToolTip("Show IPython console") + self.action_view_console.setText('Show console') + self.action_view_console.setToolTip('Show IPython console') self.action_view_log = QtWidgets.QAction() self.action_view_log.setCheckable(True) self.action_view_log.setChecked(True) - self.action_view_log.setText("Show log") - self.action_view_log.setToolTip("Show log dockwidget") + self.action_view_log.setText('Show log') + self.action_view_log.setToolTip('Show log dockwidget') self.action_view_config = QtWidgets.QAction() self.action_view_config.setCheckable(True) self.action_view_config.setChecked(False) - self.action_view_config.setText("Show configuration") - self.action_view_config.setToolTip("Show configuration dockwidget") + self.action_view_config.setText('Show configuration') + self.action_view_config.setToolTip('Show configuration dockwidget') self.action_view_remote = QtWidgets.QAction() self.action_view_remote.setCheckable(True) self.action_view_remote.setChecked(False) - self.action_view_remote.setText("Show remote") - self.action_view_remote.setToolTip("Show remote connections dockwidget") + self.action_view_remote.setText('Show remote') + self.action_view_remote.setToolTip('Show remote connections dockwidget') self.action_view_threads = QtWidgets.QAction() self.action_view_threads.setCheckable(True) self.action_view_threads.setChecked(False) - self.action_view_threads.setText("Show threads") - self.action_view_threads.setToolTip("Show threads dockwidget") + self.action_view_threads.setText('Show threads') + self.action_view_threads.setToolTip('Show threads dockwidget') self.action_view_default = QtWidgets.QAction() - self.action_view_default.setText("Restore default") - self.action_view_default.setToolTip("Restore default view") + self.action_view_default.setText('Restore default') + self.action_view_default.setToolTip('Restore default view') # Dialog actions self.action_settings = QtWidgets.QAction() - self.action_settings.setIcon(QtGui.QIcon(os.path.join(icon_path, "configure"))) - self.action_settings.setText("Settings") - self.action_settings.setToolTip("Open settings dialog") + self.action_settings.setIcon(QtGui.QIcon(os.path.join(icon_path, 'configure'))) + self.action_settings.setText('Settings') + self.action_settings.setToolTip('Open settings dialog') self.action_about_qudi = QtWidgets.QAction() - self.action_about_qudi.setIcon(QtGui.QIcon(os.path.join(icon_path, "go-home"))) - self.action_about_qudi.setText("About qudi") - self.action_about_qudi.setToolTip("Read up about qudi") + self.action_about_qudi.setIcon( + QtGui.QIcon(os.path.join(icon_path, 'go-home'))) + self.action_about_qudi.setText('About qudi') + self.action_about_qudi.setToolTip('Read up about qudi') self.action_about_qt = QtWidgets.QAction() - self.action_about_qt.setIcon(QtGui.QIcon(os.path.join(icon_path, "go-home"))) - self.action_about_qt.setText("About Qt") - self.action_about_qt.setToolTip("Read up about Qt") + self.action_about_qt.setIcon( + QtGui.QIcon(os.path.join(icon_path, 'go-home'))) + self.action_about_qt.setText('About Qt') + self.action_about_qt.setToolTip('Read up about Qt') # Create toolbar self.toolbar = QtWidgets.QToolBar() - self.toolbar.setObjectName("QudiMainWindow Toolbar") + self.toolbar.setObjectName('QudiMainWindow Toolbar') self.toolbar.setOrientation(QtCore.Qt.Horizontal) self.toolbar.addAction(self.action_open_configuration_editor) self.toolbar.addAction(self.action_load_configuration) @@ -155,7 +150,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): # Create menu bar self.menubar = QtWidgets.QMenuBar() - menu = QtWidgets.QMenu("File") + menu = QtWidgets.QMenu('File') menu.addAction(self.action_open_configuration_editor) menu.addSeparator() menu.addAction(self.action_load_configuration) @@ -168,7 +163,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): menu.addSeparator() menu.addAction(self.action_quit) self.menubar.addMenu(menu) - menu = QtWidgets.QMenu("View") + menu = QtWidgets.QMenu('View') menu.addAction(self.action_view_console) menu.addAction(self.action_view_log) menu.addAction(self.action_view_config) @@ -177,7 +172,7 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): menu.addSeparator() menu.addAction(self.action_view_default) self.menubar.addMenu(menu) - menu = QtWidgets.QMenu("About") + menu = QtWidgets.QMenu('About') menu.addAction(self.action_about_qudi) menu.addAction(self.action_about_qt) self.menubar.addMenu(menu) @@ -192,34 +187,33 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): # Create dialogues self.about_qudi_dialog = AboutQudiDialog() - self.about_qudi_dialog.setWindowTitle("About qudi") + self.about_qudi_dialog.setWindowTitle('About qudi') self.settings_dialog = SettingsDialog() # Create dockwidgets self.config_widget = ConfigQTreeWidget() - self.config_dockwidget = AdvancedDockWidget("Configuration") + self.config_dockwidget = AdvancedDockWidget('Configuration') self.config_dockwidget.setWidget(self.config_widget) self.config_dockwidget.setAllowedAreas( QtCore.Qt.BottomDockWidgetArea | QtCore.Qt.LeftDockWidgetArea ) self.log_widget = LogWidget(debug_mode=debug_mode) - self.log_dockwidget = AdvancedDockWidget("Log") + self.log_dockwidget = AdvancedDockWidget('Log') self.log_dockwidget.setWidget(self.log_widget) self.log_dockwidget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) self.remote_widget = RemoteWidget() - self.remote_dockwidget = AdvancedDockWidget("Remote modules") + self.remote_dockwidget = AdvancedDockWidget('Remote modules') self.remote_dockwidget.setWidget(self.remote_widget) self.remote_dockwidget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) self.threads_widget = QtWidgets.QListView() - self.threads_dockwidget = AdvancedDockWidget("Threads") + self.threads_dockwidget = AdvancedDockWidget('Threads') self.threads_dockwidget.setWidget(self.threads_widget) self.threads_dockwidget.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) self.console_widget = RichJupyterWidget() - self.console_dockwidget = AdvancedDockWidget("Console") + self.console_dockwidget = AdvancedDockWidget('Console') self.console_dockwidget.setWidget(self.console_widget) self.console_dockwidget.setAllowedAreas( - QtCore.Qt.RightDockWidgetArea | QtCore.Qt.LeftDockWidgetArea - ) + QtCore.Qt.RightDockWidgetArea | QtCore.Qt.LeftDockWidgetArea) # Add dockwidgets to main window self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.config_dockwidget) @@ -229,21 +223,15 @@ def __init__(self, parent=None, debug_mode=False, **kwargs): self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.console_dockwidget) # Synchronize dockwidget visibility change signals - self.config_dockwidget.sigClosed.connect( - lambda: self.action_view_config.setChecked(False) - ) + self.config_dockwidget.sigClosed.connect(lambda: self.action_view_config.setChecked(False)) self.log_dockwidget.sigClosed.connect( - lambda: self.action_view_log.setChecked(False) - ) + lambda: self.action_view_log.setChecked(False)) self.remote_dockwidget.sigClosed.connect( - lambda: self.action_view_remote.setChecked(False) - ) + lambda: self.action_view_remote.setChecked(False)) self.threads_dockwidget.sigClosed.connect( - lambda: self.action_view_threads.setChecked(False) - ) + lambda: self.action_view_threads.setChecked(False)) self.console_dockwidget.sigClosed.connect( - lambda: self.action_view_console.setChecked(False) - ) + lambda: self.action_view_console.setChecked(False)) self.action_view_config.toggled.connect(self.config_dockwidget.setVisible) self.action_view_log.toggled.connect(self.log_dockwidget.setVisible) self.action_view_remote.toggled.connect(self.remote_dockwidget.setVisible) diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index 1fc4770e1..01ae86ef4 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -27,10 +27,9 @@ class SettingsDialog(QtWidgets.QDialog): """ Custom QDialog widget for configuration of the qudi main GUI """ - def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) - self.setWindowTitle("Qudi: Main GUI settings") + self.setWindowTitle('Qudi: Main GUI settings') # Create main layout # Add widgets to layout and set as main layout @@ -40,20 +39,20 @@ def __init__(self, parent=None, **kwargs): # Create widgets and add them to the layout self.font_size_spinbox = QtWidgets.QSpinBox() - self.font_size_spinbox.setObjectName("fontSizeSpinBox") + self.font_size_spinbox.setObjectName('fontSizeSpinBox') self.font_size_spinbox.setMinimum(5) self.font_size_spinbox.setValue(10) - label = QtWidgets.QLabel("Console font size:") - label.setObjectName("fontSizeLabel") + label = QtWidgets.QLabel('Console font size:') + label.setObjectName('fontSizeLabel') label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(label, 0, 0) layout.addWidget(self.font_size_spinbox, 0, 1) self.show_error_popups_checkbox = QtWidgets.QCheckBox() - self.show_error_popups_checkbox.setObjectName("showErrorPopupsCheckbox") + self.show_error_popups_checkbox.setObjectName('showErrorPopupsCheckbox') self.show_error_popups_checkbox.setChecked(True) - label = QtWidgets.QLabel("Show error popups:") - label.setObjectName("showErrorPopupsLabel") + label = QtWidgets.QLabel('Show error popups:') + label.setObjectName('showErrorPopupsLabel') label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) layout.addWidget(label, 1, 0) layout.addWidget(self.show_error_popups_checkbox, 1, 1) @@ -84,11 +83,9 @@ def __init__(self, parent=None, **kwargs): layout.addWidget(label, 3, 0) layout.addWidget(self.dump_status_variables_interval_spinbox, 3, 1) - buttonbox = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Ok - | QtWidgets.QDialogButtonBox.Cancel - | QtWidgets.QDialogButtonBox.Apply - ) + buttonbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok + | QtWidgets.QDialogButtonBox.Cancel + | QtWidgets.QDialogButtonBox.Apply) buttonbox.setOrientation(QtCore.Qt.Horizontal) layout.addWidget(buttonbox, 4, 0, 1, 2) diff --git a/src/qudi/core/module.py b/src/qudi/core/module.py index 2b26b9cfc..4268758e3 100644 --- a/src/qudi/core/module.py +++ b/src/qudi/core/module.py @@ -18,7 +18,6 @@ You should have received a copy of the GNU Lesser General Public License along with qudi. If not, see . """ - import logging import os import copy @@ -31,11 +30,7 @@ from qudi.core.configoption import MissingOption from qudi.core.statusvariable import StatusVar -from qudi.util.paths import ( - get_module_app_data_path, - get_daily_directory, - get_default_data_dir, -) +from qudi.util.paths import get_module_app_data_path, get_daily_directory, get_default_data_dir from qudi.util.yaml import yaml_load, yaml_dump from qudi.core.meta import ModuleMeta from qudi.core.logger import get_logger @@ -45,7 +40,6 @@ class ModuleStateMachine(Fysom, QtCore.QObject): """ FIXME """ - # do not copy declaration of trigger(self, event, *args, **kwargs), just apply Slot decorator trigger = QtCore.Slot(str, result=bool)(Fysom.trigger) @@ -61,17 +55,13 @@ def __init__(self, callbacks=None, parent=None, **kwargs): # name: event name, # src: source state, # dst: destination state - fsm_cfg = { - "initial": "deactivated", - "events": [ - {"name": "activate", "src": "deactivated", "dst": "idle"}, - {"name": "deactivate", "src": "idle", "dst": "deactivated"}, - {"name": "deactivate", "src": "locked", "dst": "deactivated"}, - {"name": "lock", "src": "idle", "dst": "locked"}, - {"name": "unlock", "src": "locked", "dst": "idle"}, - ], - "callbacks": callbacks, - } + fsm_cfg = {'initial': 'deactivated', + 'events': [{'name': 'activate', 'src': 'deactivated', 'dst': 'idle'}, + {'name': 'deactivate', 'src': 'idle', 'dst': 'deactivated'}, + {'name': 'deactivate', 'src': 'locked', 'dst': 'deactivated'}, + {'name': 'lock', 'src': 'idle', 'dst': 'locked'}, + {'name': 'unlock', 'src': 'locked', 'dst': 'idle'}], + 'callbacks': callbacks} # Initialise state machine: super().__init__(parent=parent, cfg=fsm_cfg, **kwargs) @@ -108,7 +98,7 @@ def unlock(self) -> None: class Base(QtCore.QObject, metaclass=ModuleMeta): - """Base class for all loadable modules + """ Base class for all loadable modules * Ensure that the program will not die during the load of modules * Initialize modules @@ -120,29 +110,21 @@ class Base(QtCore.QObject, metaclass=ModuleMeta): * Get status variables * Reload module data (from saved variables) """ - _threaded = 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): - abstract = getattr(cls, "__abstractmethods__", frozenset()) + abstract = getattr(cls, '__abstractmethods__', frozenset()) if abstract: - raise TypeError( - f'Can\'t instantiate abstract class "{cls.__name__}" ' - f"with abstract methods {set(abstract)}" - ) + raise TypeError(f'Can\'t instantiate abstract class "{cls.__name__}" ' + f'with abstract methods {set(abstract)}') return super().__new__(cls, *args, **kwargs) - def __init__( - self, - qudi_main_weakref: Any, - name: str, - config: Optional[Mapping[str, Any]] = None, - callbacks: Optional[Mapping[str, Callable]] = None, - **kwargs, - ): - """Initialise Base instance. Set up its state machine and initialize ConfigOption meta + def __init__(self, qudi_main_weakref: Any, name: str, + config: Optional[Mapping[str, Any]] = None, + callbacks: Optional[Mapping[str, Callable]] = None, **kwargs): + """ Initialise Base instance. Set up its state machine and initialize ConfigOption meta attributes from given config. @param object self: the object being initialised @@ -161,14 +143,14 @@ def __init__( self.__qudi_main_weakref = qudi_main_weakref # Create logger instance for module - self.__logger = get_logger(f"{self.__module__}.{self.__class__.__name__}") + self.__logger = get_logger(f'{self.__module__}.{self.__class__.__name__}') # Create a copy of the _meta class dict and attach it to the created instance self._meta = copy.deepcopy(self._meta) # Add additional meta info to _meta dict - self._meta["name"] = name - self._meta["uuid"] = uuid4() - self._meta["configuration"] = copy.deepcopy(config) + self._meta['name'] = name + self._meta['uuid'] = uuid4() + self._meta['configuration'] = copy.deepcopy(config) # set instance attributes according to config_option meta objects self.__initialize_config_options(config) @@ -177,28 +159,24 @@ def __init__( self.__initialize_connectors() # Initialize module FSM - default_callbacks = { - "on_before_activate": self.__activation_callback, - "on_before_deactivate": self.__deactivation_callback, - } + default_callbacks = {'on_before_activate' : self.__activation_callback, + 'on_before_deactivate': self.__deactivation_callback} default_callbacks.update(callbacks) self.module_state = ModuleStateMachine(parent=self, callbacks=default_callbacks) return def __initialize_config_options(self, config: Optional[Mapping[str, Any]]) -> None: - for attr_name, cfg_opt in self._meta["config_options"].items(): + for attr_name, cfg_opt in self._meta['config_options'].items(): if cfg_opt.name in config: cfg_val = copy.deepcopy(config[cfg_opt.name]) else: if cfg_opt.missing == MissingOption.error: raise ValueError( f'Required ConfigOption "{cfg_opt.name}" not given in configuration.\n' - f"Configuration is: {config}" + f'Configuration is: {config}' ) - msg = ( - f'No ConfigOption "{cfg_opt.name}" configured, using default value ' - f'"{cfg_opt.default}" instead.' - ) + msg = f'No ConfigOption "{cfg_opt.name}" configured, using default value ' \ + f'"{cfg_opt.default}" instead.' cfg_val = copy.deepcopy(cfg_opt.default) if cfg_opt.missing == MissingOption.warn: self.log.warning(msg) @@ -211,7 +189,7 @@ def __initialize_config_options(self, config: Optional[Mapping[str, Any]]) -> No setattr(self, attr_name, cfg_val) def __initialize_connectors(self) -> None: - for attr_name, conn in self._meta["connectors"].items(): + for attr_name, conn in self._meta['connectors'].items(): setattr(self, attr_name, conn) def __eq__(self, other): @@ -224,17 +202,18 @@ def __hash__(self): @QtCore.Slot() def move_to_main_thread(self) -> None: - """Method that will move this module into the main/manager thread.""" + """ Method that will move this module into the main/manager thread. + """ if QtCore.QThread.currentThread() != self.thread(): - QtCore.QMetaObject.invokeMethod( - self, "move_to_main_thread", QtCore.Qt.BlockingQueuedConnection - ) + QtCore.QMetaObject.invokeMethod(self, + 'move_to_main_thread', + QtCore.Qt.BlockingQueuedConnection) else: self.moveToThread(QtCore.QCoreApplication.instance().thread()) @property def module_thread(self) -> Union[QtCore.QThread, None]: - """Read-only property returning the current module QThread instance if the module is + """ Read-only property returning the current module QThread instance if the module is threaded. Returns None otherwise. """ if self._threaded: @@ -243,37 +222,36 @@ def module_thread(self) -> Union[QtCore.QThread, None]: @property def module_name(self) -> str: - """Read-only property returning the module name of this module instance as specified in the + """ Read-only property returning the module name of this module instance as specified in the config. """ - return self._meta["name"] + return self._meta['name'] @property def module_base(self) -> str: - """Read-only property returning the module base of this module instance + """ Read-only property returning the module base of this module instance ('hardware' 'logic' or 'gui') """ - return self._meta["base"] + return self._meta['base'] @property def module_uuid(self) -> uuid.UUID: - """Read-only property returning a unique uuid for this module instance.""" - return self._meta["uuid"] + """ Read-only property returning a unique uuid for this module instance. + """ + return self._meta['uuid'] @property def module_default_data_dir(self) -> str: - """Read-only property returning the generic default directory in which to save data. + """ Read-only property returning the generic default directory in which to save data. Module implementations can overwrite this property with a custom path but should only do so with a very good reason. """ config = self._qudi_main.configuration - data_root = config["default_data_dir"] + data_root = config['default_data_dir'] if data_root is None: data_root = get_default_data_dir() - if config["daily_data_dirs"]: - data_dir = os.path.join( - get_daily_directory(root=data_root), self.module_name - ) + if config['daily_data_dirs']: + data_dir = os.path.join(get_daily_directory(root=data_root), self.module_name) else: data_dir = os.path.join(data_root, self.module_name) return data_dir @@ -282,7 +260,7 @@ def module_default_data_dir(self) -> str: def module_status_variables(self) -> Dict[str, Any]: variables = dict() try: - for attr_name, var in self._meta["status_variables"].items(): + for attr_name, var in self._meta['status_variables'].items(): if hasattr(self, attr_name): value = getattr(self, attr_name) if not isinstance(value, StatusVar): @@ -290,7 +268,7 @@ def module_status_variables(self) -> Dict[str, Any]: value = var.representer_function(self, value) variables[var.name] = value except: - self.log.exception("Error while collecting status variables:") + self.log.exception('Error while collecting status variables:') return variables @property @@ -298,76 +276,80 @@ def _qudi_main(self) -> Any: qudi_main = self.__qudi_main_weakref() if qudi_main is None: raise RuntimeError( - "Unexpected missing qudi main instance. It has either been deleted or garbage " - "collected." + 'Unexpected missing qudi main instance. It has either been deleted or garbage ' + 'collected.' ) return qudi_main @property def log(self) -> logging.Logger: - """Returns the module logger instance""" + """ Returns the module logger instance + """ return self.__logger @property def is_module_threaded(self) -> bool: - """Returns whether the module shall be started in its own thread.""" + """ Returns whether the module shall be started in its own thread. + """ return self._threaded def __activation_callback(self, event=None) -> bool: - """Restore status variables before activation and invoke on_activate method.""" + """ Restore status variables before activation and invoke on_activate method. + """ try: self._load_status_variables() self.on_activate() except: - self.log.exception("Exception during activation:") + self.log.exception('Exception during activation:') return False return True def __deactivation_callback(self, event=None) -> bool: - """Invoke on_deactivate method and save status variables afterwards even if deactivation + """ Invoke on_deactivate method and save status variables afterwards even if deactivation fails. """ try: self.on_deactivate() except: - self.log.exception("Exception during deactivation:") + self.log.exception('Exception during deactivation:') finally: # save status variables even if deactivation failed self._dump_status_variables() return True def _load_status_variables(self) -> None: - """Load status variables from app data directory on disc.""" + """ Load status variables from app data directory on disc. + """ # Load status variables from app data directory - file_path = get_module_app_data_path( - self.__class__.__name__, self.module_base, self.module_name - ) + file_path = get_module_app_data_path(self.__class__.__name__, + self.module_base, + self.module_name) try: variables = yaml_load(file_path, ignore_missing=True) except: variables = dict() - self.log.exception("Failed to load status variables:") + self.log.exception('Failed to load status variables:') # Set instance attributes according to StatusVar meta objects try: - for attr_name, var in self._meta["status_variables"].items(): + for attr_name, var in self._meta['status_variables'].items(): value = variables.get(var.name, copy.deepcopy(var.default)) if var.constructor_function is not None: value = var.constructor_function(self, value) setattr(self, attr_name, value) except: - self.log.exception("Error while settings status variables:") + self.log.exception('Error while settings status variables:') def _dump_status_variables(self) -> None: - """Dump status variables to app data directory on disc. + """ 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 - ) + file_path = get_module_app_data_path(self.__class__.__name__, + self.module_base, + self.module_name) # collect StatusVar values into dictionary variables = self.module_status_variables # Save to file if any StatusVars have been found @@ -375,21 +357,16 @@ def _dump_status_variables(self) -> None: try: yaml_dump(file_path, variables) except: - self.log.exception("Failed to save status variables:") - - def _send_balloon_message( - self, - title: str, - message: str, - time: Optional[float] = None, - icon: Optional[QtGui.QIcon] = None, - ) -> None: + self.log.exception('Failed to save status variables:') + + def _send_balloon_message(self, title: str, message: str, time: Optional[float] = None, + icon: Optional[QtGui.QIcon] = None) -> None: qudi_main = self.__qudi_main_weakref() if qudi_main is None: return if qudi_main.gui is None: - log = get_logger("balloon-message") - log.warning(f"{title}:\n{message}") + log = get_logger('balloon-message') + log.warning(f'{title}:\n{message}') return qudi_main.gui.balloon_message(title, message, time, icon) @@ -398,110 +375,100 @@ def _send_pop_up_message(self, title: str, message: str): if qudi_main is None: return if qudi_main.gui is None: - log = get_logger("pop-up-message") - log.warning(f"{title}:\n{message}") + log = get_logger('pop-up-message') + log.warning(f'{title}:\n{message}') return qudi_main.gui.pop_up_message(title, message) def connect_modules(self, connections: Mapping[str, Any]) -> None: - """Connects given modules (values) to their respective Connector (keys). + """ Connects given modules (values) to their respective Connector (keys). DO NOT CALL THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING! """ # Sanity checks - conn_names = set(conn.name for conn in self._meta["connectors"].values()) + conn_names = set(conn.name for conn in self._meta['connectors'].values()) mandatory_conn = set( - conn.name for conn in self._meta["connectors"].values() if not conn.optional + conn.name for conn in self._meta['connectors'].values() if not conn.optional ) configured_conn = set(connections) if not configured_conn.issubset(conn_names): - raise KeyError( - f"Mismatch of connectors in configuration {configured_conn} and module " - f"Connector meta objects {conn_names}." - ) + raise KeyError(f'Mismatch of connectors in configuration {configured_conn} and module ' + f'Connector meta objects {conn_names}.') if not mandatory_conn.issubset(configured_conn): - raise ValueError( - f"Not all mandatory connectors are specified in config.\n" - f"Mandatory connectors are: {mandatory_conn}" - ) + raise ValueError(f'Not all mandatory connectors are specified in config.\n' + f'Mandatory connectors are: {mandatory_conn}') # Iterate through module connectors and connect them if possible - for conn in self._meta["connectors"].values(): + for conn in self._meta['connectors'].values(): target = connections.get(conn.name, None) if target is None: continue if conn.is_connected: - raise RuntimeError( - f'Connector "{conn.name}" already connected.\n' - f'Call "disconnect_modules()" before trying to reconnect.' - ) + raise RuntimeError(f'Connector "{conn.name}" already connected.\n' + f'Call "disconnect_modules()" before trying to reconnect.') conn.connect(target) def disconnect_modules(self) -> None: - """Disconnects all Connector instances for this module. + """ Disconnects all Connector instances for this module. DO NOT CALL THIS METHOD UNLESS YOU KNOW WHAT YOU ARE DOING! """ - for conn in self._meta["connectors"].values(): + for conn in self._meta['connectors'].values(): conn.disconnect() @abstractmethod def on_activate(self) -> None: - """Method called when module is activated. Must be implemented by actual qudi module.""" - raise NotImplementedError("Please implement and specify the activation method.") + """ Method called when module is activated. Must be implemented by actual qudi module. + """ + raise NotImplementedError('Please implement and specify the activation method.') @abstractmethod def on_deactivate(self) -> None: - """Method called when module is deactivated. Must be implemented by actual qudi module.""" - raise NotImplementedError( - "Please implement and specify the deactivation method." - ) + """ Method called when module is deactivated. Must be implemented by actual qudi module. + """ + raise NotImplementedError('Please implement and specify the deactivation method.') class LogicBase(Base): - """ """ - + """ + """ _threaded = True class GuiBase(Base): - """This is the GUI base class. It provides functions that every GUI module should have.""" - + """This is the GUI base class. It provides functions that every GUI module should have. + """ _threaded = False - __window_geometry = StatusVar(name="_GuiBase__window_geometry", default=None) - __window_state = StatusVar(name="_GuiBase__window_state", default=None) + __window_geometry = StatusVar(name='_GuiBase__window_geometry', default=None) + __window_state = StatusVar(name='_GuiBase__window_state', default=None) @abstractmethod def show(self) -> None: - raise NotImplementedError( - "Every GUI module needs to implement the show() method!" - ) + raise NotImplementedError('Every GUI module needs to implement the show() method!') def _save_window_geometry(self, window: QtWidgets.QMainWindow) -> None: try: - self.__window_geometry = ( - window.saveGeometry().toHex().data().decode("utf-8") - ) + self.__window_geometry = window.saveGeometry().toHex().data().decode('utf-8') except: - self.log.exception("Unable to save window geometry:") + self.log.exception('Unable to save window geometry:') self.__window_geometry = None try: - self.__window_state = window.saveState().toHex().data().decode("utf-8") + self.__window_state = window.saveState().toHex().data().decode('utf-8') except: - self.log.exception("Unable to save window geometry:") + self.log.exception('Unable to save window geometry:') self.__window_state = None def _restore_window_geometry(self, window: QtWidgets.QMainWindow) -> bool: if isinstance(self.__window_geometry, str): try: - encoded = QtCore.QByteArray(self.__window_geometry.encode("utf-8")) + encoded = QtCore.QByteArray(self.__window_geometry.encode('utf-8')) window.restoreGeometry(QtCore.QByteArray.fromHex(encoded)) except: - self.log.exception("Unable to restore window geometry:") + self.log.exception('Unable to restore window geometry:') if isinstance(self.__window_state, str): try: - encoded = QtCore.QByteArray(self.__window_state.encode("utf-8")) + encoded = QtCore.QByteArray(self.__window_state.encode('utf-8')) return window.restoreState(QtCore.QByteArray.fromHex(encoded)) except: - self.log.exception("Unable to restore window state:") + self.log.exception('Unable to restore window state:') return False From 4de4d8402ddf51720d002b2cd3bf21bd0ffb1d55 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 12 Aug 2024 12:21:44 +0200 Subject: [PATCH 13/26] modified changelog --- docs/changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index f1b7b596a..1e3911ed2 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -11,7 +11,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 None From 5f53e61c95f0cf712a1459c501bc86d15a7bd150 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 12 Aug 2024 12:29:12 +0200 Subject: [PATCH 14/26] reverted automatic style changes --- src/qudi/core/gui/main_gui/main_gui.py | 245 ++++++++++--------------- 1 file changed, 102 insertions(+), 143 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 76f09c151..4bf1d66f6 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -20,10 +20,12 @@ import os import sys +import logging import subprocess import jupyter_client.kernelspec from PySide2 import QtCore, QtWidgets from qtconsole.manager import QtKernelManager +from collections.abc import Mapping, Sequence, Set from qudi.core.statusvariable import StatusVar from qudi.core.threadmanager import ThreadManager @@ -43,10 +45,9 @@ class QudiMainGui(GuiBase): """ This class provides a GUI to the qudi main application object. """ - # status vars - _console_font_size = StatusVar(name="console_font_size", default=10) - _show_error_popups = StatusVar(name="show_error_popups", default=True) + _console_font_size = StatusVar(name='console_font_size', default=10) + _show_error_popups = StatusVar(name='show_error_popups', default=True) _automatic_status_var_dump = StatusVar( name="automatic_status_var_dump", default=False ) @@ -59,9 +60,9 @@ class QudiMainGui(GuiBase): def __init__(self, *args, **kwargs): """Create an instance of the module. - @param object manager: - @param str name: - @param dict config: + @param object manager: + @param str name: + @param dict config: """ super().__init__(*args, **kwargs) self.error_dialog = None @@ -69,7 +70,7 @@ def __init__(self, *args, **kwargs): self._has_console = False # Flag indicating if an IPython console is available def on_activate(self): - """Activation method called on change to active state. + """ Activation method called on change to active state. This method creates the Manager main window. """ @@ -83,25 +84,18 @@ def on_activate(self): # Get qudi version number and configure statusbar and "about qudi" dialog version = self.get_qudi_version() if isinstance(version, str): - self.mw.about_qudi_dialog.version_label.setText( - "version {0}".format(version) - ) + self.mw.about_qudi_dialog.version_label.setText('version {0}'.format(version)) self.mw.version_label.setText( - ' version {0} configured from {1}' - "".format(version, self._qudi_main.configuration.file_path) - ) + ' version {0} configured from {1}' + ''.format(version, self._qudi_main.configuration.file_path)) else: self.mw.about_qudi_dialog.version_label.setText( - ' {0}' - " , on branch {1}.".format(version[0], version[1]) - ) + ' {0}' + ' , on branch {1}.'.format(version[0], version[1])) self.mw.version_label.setText( - ' {0}' - " , on branch {1}, configured from {2}" - "".format( - version[0], version[1], self._qudi_main.configuration.file_path - ) - ) + ' {0}' + ' , on branch {1}, configured from {2}' + ''.format(version[0], version[1], self._qudi_main.configuration.file_path)) self._connect_signals() @@ -132,31 +126,24 @@ def on_activate(self): self.show() def on_deactivate(self): - """Close window and remove connections.""" + """Close window and remove connections. + """ self._disconnect_signals() self.stop_jupyter_widget() self._save_window_geometry(self.mw) self.mw.close() def _connect_signals(self): - get_signal_handler().sigRecordLogged.connect( - self.handle_log_record, QtCore.Qt.QueuedConnection - ) + get_signal_handler().sigRecordLogged.connect(self.handle_log_record, QtCore.Qt.QueuedConnection) qudi_main = self._qudi_main # Connect up the main windows actions - self.mw.action_quit.triggered.connect( - qudi_main.prompt_quit, QtCore.Qt.QueuedConnection - ) + self.mw.action_quit.triggered.connect(qudi_main.prompt_quit, QtCore.Qt.QueuedConnection) self.mw.action_load_configuration.triggered.connect(self.load_configuration) self.mw.action_reload_qudi.triggered.connect( - qudi_main.prompt_restart, QtCore.Qt.QueuedConnection - ) - self.mw.action_open_configuration_editor.triggered.connect( - self.new_configuration - ) + qudi_main.prompt_restart, QtCore.Qt.QueuedConnection) + 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 - ) + 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 ) @@ -179,32 +166,20 @@ def _connect_signals(self): self.mw.action_view_default.triggered.connect(self.reset_default_layout) # Connect signals from manager qudi_main.configuration.sigConfigChanged.connect(self.update_config_widget) - qudi_main.module_manager.sigManagedModulesChanged.connect( - self.update_configured_modules - ) + qudi_main.module_manager.sigManagedModulesChanged.connect(self.update_configured_modules) qudi_main.module_manager.sigModuleStateChanged.connect(self.update_module_state) - qudi_main.module_manager.sigModuleAppDataChanged.connect( - self.update_module_app_data - ) + qudi_main.module_manager.sigModuleAppDataChanged.connect(self.update_module_app_data) # Settings dialog self.mw.settings_dialog.accepted.connect(self.apply_settings) self.mw.settings_dialog.rejected.connect(self.keep_settings) - self.error_dialog.disable_checkbox.clicked.connect( - self._error_dialog_enabled_changed - ) + self.error_dialog.disable_checkbox.clicked.connect(self._error_dialog_enabled_changed) # Modules list - self.mw.module_widget.sigActivateModule.connect( - qudi_main.module_manager.activate_module - ) - self.mw.module_widget.sigReloadModule.connect( - qudi_main.module_manager.reload_module - ) + self.mw.module_widget.sigActivateModule.connect(qudi_main.module_manager.activate_module) + self.mw.module_widget.sigReloadModule.connect(qudi_main.module_manager.reload_module) self.mw.module_widget.sigDeactivateModule.connect( - qudi_main.module_manager.deactivate_module - ) + qudi_main.module_manager.deactivate_module) self.mw.module_widget.sigCleanupModule.connect( - qudi_main.module_manager.clear_module_app_data - ) + qudi_main.module_manager.clear_module_app_data) def _disconnect_signals(self): qudi_main = self._qudi_main @@ -221,15 +196,9 @@ def _disconnect_signals(self): self.mw.action_view_default.triggered.disconnect() # Disconnect signals from manager qudi_main.configuration.sigConfigChanged.disconnect(self.update_config_widget) - qudi_main.module_manager.sigManagedModulesChanged.disconnect( - self.update_configured_modules - ) - qudi_main.module_manager.sigModuleStateChanged.disconnect( - self.update_module_state - ) - qudi_main.module_manager.sigModuleAppDataChanged.disconnect( - self.update_module_app_data - ) + qudi_main.module_manager.sigManagedModulesChanged.disconnect(self.update_configured_modules) + qudi_main.module_manager.sigModuleStateChanged.disconnect(self.update_module_state) + qudi_main.module_manager.sigModuleAppDataChanged.disconnect(self.update_module_app_data) # Settings dialog self.mw.settings_dialog.accepted.disconnect() self.mw.settings_dialog.rejected.disconnect() @@ -250,19 +219,18 @@ def _init_remote_modules_widget(self): self.mw.remote_dockwidget.setVisible(False) self.mw.action_view_remote.setVisible(False) else: - server_config = self._qudi_main.configuration["remote_modules_server"] - host = server_config["address"] - port = server_config["port"] + server_config = self._qudi_main.configuration['remote_modules_server'] + host = server_config['address'] + port = server_config['port'] self.mw.remote_widget.setVisible(True) - self.mw.remote_widget.server_label.setText( - f"Server URL: rpyc://{host}:{port}/" - ) + self.mw.remote_widget.server_label.setText(f'Server URL: rpyc://{host}:{port}/') self.mw.remote_widget.shared_module_listview.setModel( remote_server.service.shared_modules ) def show(self): - """Show the window and bring it to the top.""" + """Show the window and bring it to the top. + """ self.mw.show() self.mw.activateWindow() self.mw.raise_() @@ -286,9 +254,7 @@ def reset_default_layout(self): self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.config_dockwidget) self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.log_dockwidget) self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.remote_dockwidget) - self.mw.addDockWidget( - QtCore.Qt.BottomDockWidgetArea, self.mw.threads_dockwidget - ) + self.mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.mw.threads_dockwidget) self.mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.mw.console_dockwidget) self.mw.action_view_console.setChecked(self._has_console) @@ -301,29 +267,28 @@ def handle_log_record(self, entry): @param logging.LogRecord entry: log record as returned from logging module """ - if entry.levelname in ("error", "critical"): + if entry.levelname in ('error', 'critical'): self.error_dialog.new_error(entry) return def start_jupyter_widget(self): - """Starts a qudi IPython kernel in a separate process and connects it to the console widget""" + """ Starts a qudi IPython kernel in a separate process and connects it to the console widget + """ self._has_console = False try: # Create and start kernel process - kernel_manager = QtKernelManager(kernel_name="qudi", autorestart=False) + kernel_manager = QtKernelManager(kernel_name='qudi', autorestart=False) # kernel_manager.kernel.gui = 'qt4' kernel_manager.start_kernel() # create kernel client and connect to console widget - banner = ( - "This is an interactive IPython console. A reference to the running qudi " - 'instance can be accessed via "qudi". View the current namespace with dir().\n' - "Go, play.\n" - ) + banner = 'This is an interactive IPython console. A reference to the running qudi ' \ + 'instance can be accessed via "qudi". View the current namespace with dir().\n' \ + 'Go, play.\n' self.mw.console_widget.banner = banner self.mw.console_widget.font_size = self._console_font_size self.mw.console_widget.reset_font() - self.mw.console_widget.set_default_style(colors="linux") + self.mw.console_widget.set_default_style(colors='linux') kernel_client = kernel_manager.client() kernel_client.hb_channel.time_to_dead = 10.0 kernel_client.hb_channel.kernel_died.connect(self.kernel_died_callback) @@ -331,22 +296,23 @@ def start_jupyter_widget(self): self.mw.console_widget.kernel_manager = kernel_manager self.mw.console_widget.kernel_client = kernel_client self._has_console = True - self.log.info("IPython kernel for qudi main GUI successfully started.") + self.log.info('IPython kernel for qudi main GUI successfully started.') except jupyter_client.kernelspec.NoSuchKernel: self.log.warn( - "Qudi IPython kernelspec not installed.\n" - "IPython console and jupyter notebook integration not available.\n" + 'Qudi IPython kernelspec not installed.\n' + 'IPython console and jupyter notebook integration not available.\n' 'Run "qudi-install-kernel" from within the qudi Python environment to fix this. ' ) except: self.log.exception( - "Exception while trying to start IPython kernel for qudi main GUI. Qudi IPython " - "console not available." + 'Exception while trying to start IPython kernel for qudi main GUI. Qudi IPython ' + 'console not available.' ) @QtCore.Slot() def kernel_died_callback(self): - """ """ + """ + """ try: self.mw.console_widget.kernel_client.stop_channels() except: @@ -354,34 +320,30 @@ def kernel_died_callback(self): if self._has_console: self._has_console = False self.log.error( - "Qudi IPython kernel has unexpectedly died. This can be caused by a corrupt qudi " + 'Qudi IPython kernel has unexpectedly died. This can be caused by a corrupt qudi ' 'kernelspec installation. Try to run "qudi-install-kernel" from within the qudi ' - "Python environment and restart qudi." + 'Python environment and restart qudi.' ) def stop_jupyter_widget(self): - """Stops the qudi IPython kernel process and detaches it from the console widget""" + """ Stops the qudi IPython kernel process and detaches it from the console widget + """ try: self.mw.console_widget.kernel_client.stop_channels() except: - self.log.exception( - "Exception while trying to shutdown qudi IPython client:" - ) + self.log.exception('Exception while trying to shutdown qudi IPython client:') try: self.mw.console_widget.kernel_manager.shutdown_kernel() except: - self.log.exception( - "Exception while trying to shutdown qudi IPython kernel:" - ) + self.log.exception('Exception while trying to shutdown qudi IPython kernel:') self._has_console = False - self.log.info("IPython kernel process for qudi main GUI has shut down.") + self.log.info('IPython kernel process for qudi main GUI has shut down.') def keep_settings(self): - """Write old values into settings dialog.""" + """ Write old values into settings dialog. + """ self.mw.settings_dialog.font_size_spinbox.setValue(self._console_font_size) - self.mw.settings_dialog.show_error_popups_checkbox.setChecked( - self._show_error_popups - ) + self.mw.settings_dialog.show_error_popups_checkbox.setChecked(self._show_error_popups) self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked( self._automatic_status_var_dump ) @@ -390,7 +352,8 @@ def keep_settings(self): ) def apply_settings(self): - """Apply values from settings dialog.""" + """ Apply values from settings dialog. + """ # Console font size font_size = self.mw.settings_dialog.font_size_spinbox.value() self.mw.console_widget.font_size = font_size @@ -415,22 +378,23 @@ def apply_settings(self): @QtCore.Slot() def _error_dialog_enabled_changed(self): - """Callback for the error dialog disable checkbox""" + """ Callback for the error dialog disable checkbox + """ self._show_error_popups = self.error_dialog.enabled - self.mw.settings_dialog.show_error_popups_checkbox.setChecked( - self._show_error_popups - ) + self.mw.settings_dialog.show_error_popups_checkbox.setChecked(self._show_error_popups) @QtCore.Slot(object) def update_config_widget(self, config=None): - """Clear and refill the tree widget showing the configuration.""" + """ Clear and refill the tree widget showing the configuration. + """ if config is None: config = self._qudi_main.configuration self.mw.config_widget.set_config(config.config_map) @QtCore.Slot(dict) def update_configured_modules(self, modules=None): - """Clear and refill the module list widget""" + """ Clear and refill the module list widget + """ if modules is None: modules = self._qudi_main.module_manager self.mw.module_widget.update_modules(modules) @@ -445,7 +409,8 @@ def update_module_app_data(self, base, name, exists): self.mw.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.""" + """ Try to determine the software version in case the program is in a git repository. + """ # Try to get repository information if qudi has been checked out as git repo if Repo is not None: try: @@ -456,35 +421,31 @@ def get_qudi_version(self): except InvalidGitRepositoryError: pass except: - self.log.exception("Unexpected error while trying to get git repo:") + self.log.exception('Unexpected error while trying to get git repo:') # Try to get qudi.core version number try: from qudi.core import __version__ - return __version__ except: - self.log.exception("Unexpected error while trying to get qudi version:") - return "unknown" + self.log.exception('Unexpected error while trying to get qudi version:') + return 'unknown' def load_configuration(self): - """Ask the user for a file where the configuration should be loaded from""" - filename = QtWidgets.QFileDialog.getOpenFileName( - self.mw, - "Load Configuration", - get_default_config_dir(True), - "Configuration files (*.cfg)", - )[0] + """ Ask the user for a file where the configuration should be loaded from + """ + filename = QtWidgets.QFileDialog.getOpenFileName(self.mw, + 'Load Configuration', + get_default_config_dir(True), + 'Configuration files (*.cfg)')[0] if filename: reply = QtWidgets.QMessageBox.question( self.mw, - "Restart", - "Do you want to restart to use the configuration?\n" + 'Restart', + 'Do you want to restart to use the configuration?\n' 'Choosing "No" will use the selected config file for the next start of Qudi.', - QtWidgets.QMessageBox.Yes - | QtWidgets.QMessageBox.No - | QtWidgets.QMessageBox.Cancel, - QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, + QtWidgets.QMessageBox.No ) if reply == QtWidgets.QMessageBox.Cancel: return @@ -493,23 +454,21 @@ def load_configuration(self): self._qudi_main.restart() def new_configuration(self): - """Prompt the user to open the graphical config editor in a subprocess in order to + """ Prompt the user to open the graphical config editor in a subprocess in order to edit/create config files for qudi. """ reply = QtWidgets.QMessageBox.question( - self.mw, - "Open Configuration Editor", - "Do you want open the graphical qudi configuration editor to create or edit qudi " - "config files?\n", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.Yes, + self.mw, + 'Open Configuration Editor', + 'Do you want open the graphical qudi configuration editor to create or edit qudi ' + 'config files?\n', + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.Yes ) if reply == QtWidgets.QMessageBox.Yes: - process = subprocess.Popen( - args=[sys.executable, "-m", "tools.config_editor"], - close_fds=False, - env=os.environ.copy(), - stdin=sys.stdin, - stdout=sys.stdout, - stderr=sys.stderr, - ) + process = subprocess.Popen(args=[sys.executable, '-m', 'tools.config_editor'], + close_fds=False, + env=os.environ.copy(), + stdin=sys.stdin, + stdout=sys.stdout, + stderr=sys.stderr) From e6d0c73c3d28380bd9bb73da9d5b1b2a9ac3168b Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 2 Sep 2024 11:28:17 +0200 Subject: [PATCH 15/26] elevated _dump_status_variables to public method --- docs/design_concepts/measurement_modules.md | 192 ++++++++++---------- src/qudi/core/module.py | 4 +- src/qudi/core/modulemanager.py | 2 +- 3 files changed, 99 insertions(+), 99 deletions(-) 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/module.py b/src/qudi/core/module.py index 4268758e3..18dbed35b 100644 --- a/src/qudi/core/module.py +++ b/src/qudi/core/module.py @@ -314,7 +314,7 @@ 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() return True def _load_status_variables(self) -> None: @@ -340,7 +340,7 @@ 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 diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 3e98156a8..99e0e984b 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -256,7 +256,7 @@ def dump_status_variables(self): with self._lock: for _, module in self.modules.items(): if module.is_active: - module.instance._dump_status_variables() + module.instance.dump_status_variables() def toggle_automated_status_variable_dumping(self, toggle): """ From d64536a67e52e9d469df2077b1557445e6b431cd Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 2 Sep 2024 12:02:17 +0200 Subject: [PATCH 16/26] changed modulemanager to handle dumping_timer_interval in seconds --- src/qudi/core/gui/main_gui/main_gui.py | 12 ++++++------ src/qudi/core/modulemanager.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 4bf1d66f6..d11cc9d64 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -51,7 +51,7 @@ class QudiMainGui(GuiBase): _automatic_status_var_dump = StatusVar( name="automatic_status_var_dump", default=False ) - _automatic_status_var_dump_interval = StatusVar( + _automatic_status_var_dump_interval_min = StatusVar( name="automatic_status_var_dump_interval", default=5 ) signal_update_automatic_status_var_checkstate = QtCore.Signal(bool) @@ -115,10 +115,10 @@ def on_activate(self): self.reset_default_layout() # set correct automatic status variable dumping behaviour self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( - self._automatic_status_var_dump_interval + self._automatic_status_var_dump_interval_min ) self.signal_update_automatic_status_var_interval.emit( - self._automatic_status_var_dump_interval + self._automatic_status_var_dump_interval_min * 60 # convert min to s ) self.signal_update_automatic_status_var_checkstate.emit( self._automatic_status_var_dump @@ -348,7 +348,7 @@ def keep_settings(self): self._automatic_status_var_dump ) self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( - self._automatic_status_var_dump_interval + self._automatic_status_var_dump_interval_min ) def apply_settings(self): @@ -369,8 +369,8 @@ def apply_settings(self): interval = ( self.mw.settings_dialog.dump_status_variables_interval_spinbox.value() ) - self.signal_update_automatic_status_var_interval.emit(interval) - self._automatic_status_var_dump_interval = interval + self.signal_update_automatic_status_var_interval.emit(interval*60) # convert min to s + self._automatic_status_var_dump_interval_min = interval toggle = self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.isChecked() self.signal_update_automatic_status_var_checkstate.emit(toggle) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 99e0e984b..864f5f19a 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -63,7 +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 = 1 + self._automated_status_variable_dumping_timer_interval = 60 @classmethod def instance(cls): @@ -274,11 +274,11 @@ def toggle_automated_status_variable_dumping(self, toggle): return logger.info( - f"Automated status variable saving enabled with interval {self.automated_status_variable_dumping_timer_interval} min." + f"Automated status variable saving enabled with interval {self.automated_status_variable_dumping_timer_interval} s." ) self.automated_status_variable_dumping_timer.setInterval( - self.automated_status_variable_dumping_timer_interval * 60e3 + self.automated_status_variable_dumping_timer_interval * 1e3 ) self.automated_status_variable_dumping_timer.timeout.connect( self.dump_status_variables, QtCore.Qt.QueuedConnection @@ -288,33 +288,33 @@ def toggle_automated_status_variable_dumping(self, toggle): @property def automated_status_variable_dumping_timer_interval(self): """ - Property for the timer interval of the automatic status variable saving in min. + Property for the timer interval of the automatic status variable saving in s. - @return int: timer interval in min + @return float: timer interval in s """ return self._automated_status_variable_dumping_timer_interval @automated_status_variable_dumping_timer_interval.setter - def automated_status_variable_dumping_timer_interval(self, interval): + def automated_status_variable_dumping_timer_interval(self, interval: float): """ Setter method for the timer used to automatically dump status variables. - @param int interval: interval of the timer in min + @param float interval: interval of the timer in s """ self._automated_status_variable_dumping_timer_interval = interval + self.automated_status_variable_dumping_timer.setInterval(interval * 1e3) + logger.info( + f"Setting automated status variable saving timer interval to {interval} s." + ) def automated_status_variable_dumping_timer_interval_slot(self, interval): """ Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter and simultaneously sets the interval of the timer. - @param int interval: interval of the timer in min + @param int interval: interval of the timer in s """ self.automated_status_variable_dumping_timer_interval = interval - self.automated_status_variable_dumping_timer.setInterval(interval * 60e3) - logger.info( - f"Setting automated status variable saving timer interval to {interval} min." - ) class ManagedModule(QtCore.QObject): From 1d8695e24b8816240bbead0958ae85bb85613e42 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 2 Sep 2024 12:03:33 +0200 Subject: [PATCH 17/26] docstring fix --- src/qudi/core/modulemanager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 864f5f19a..d96588cfb 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -309,8 +309,7 @@ def automated_status_variable_dumping_timer_interval(self, interval: float): def automated_status_variable_dumping_timer_interval_slot(self, interval): """ - Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter - and simultaneously sets the interval of the timer. + Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter. @param int interval: interval of the timer in s """ From d01c31316e4ea00e3719bada54e63407f630a4a5 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 2 Sep 2024 12:13:23 +0200 Subject: [PATCH 18/26] introduced timer interval Exception if interval <= 0 --- src/qudi/core/modulemanager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index d96588cfb..0426b4b17 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -299,8 +299,10 @@ def automated_status_variable_dumping_timer_interval(self, interval: float): """ Setter method for the timer used to automatically dump status variables. - @param float interval: interval of the timer in s + @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._automated_status_variable_dumping_timer_interval = interval self.automated_status_variable_dumping_timer.setInterval(interval * 1e3) logger.info( From c73d2482053208a21f714a347a7497f1ce0c136c Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 2 Sep 2024 12:48:51 +0200 Subject: [PATCH 19/26] settings dialog correctly shows, when no status variable has been saved yet --- src/qudi/core/gui/main_gui/main_gui.py | 11 +++++++---- src/qudi/core/gui/main_gui/settingsdialog.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index d11cc9d64..0914a8975 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -151,10 +151,10 @@ def _connect_signals(self): qudi_main.module_manager.toggle_automated_status_variable_dumping, QtCore.Qt.QueuedConnection, ) - self.signal_update_automatic_status_var_checkstate.connect( - self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled, - QtCore.Qt.QueuedConnection, - ) + #self.signal_update_automatic_status_var_checkstate.connect( + # self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled, + # QtCore.Qt.QueuedConnection, + #) self.signal_update_automatic_status_var_interval.connect( qudi_main.module_manager.automated_status_variable_dumping_timer_interval_slot, QtCore.Qt.QueuedConnection, @@ -259,6 +259,9 @@ def reset_default_layout(self): self.mw.action_view_console.setChecked(self._has_console) self.mw.action_view_console.setVisible(self._has_console) + + self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked(self._automatic_status_var_dump) + self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled(self._automatic_status_var_dump) return def handle_log_record(self, entry): diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index 01ae86ef4..1913714e4 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -80,6 +80,7 @@ def __init__(self, parent=None, **kwargs): self.dump_status_variables_interval_spinbox.setMaximum(1440) self.dump_status_variables_interval_spinbox.setMinimumSize(QtCore.QSize(80, 0)) self.dump_status_variables_interval_spinbox.setValue(1) + self.dump_status_variables_interval_spinbox.setEnabled(False) layout.addWidget(label, 3, 0) layout.addWidget(self.dump_status_variables_interval_spinbox, 3, 1) From 6634e3a3ececaebc27dbcbef3ab27c3e811a710c Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 03:36:32 +0200 Subject: [PATCH 20/26] added automated dumping of status variables to Base --- src/qudi/core/module.py | 83 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/qudi/core/module.py b/src/qudi/core/module.py index 18dbed35b..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:') @@ -315,6 +397,7 @@ def __deactivation_callback(self, event=None) -> bool: finally: # save status variables even if deactivation failed self.dump_status_variables() + self._deactivate_automatically_dump_status_variables() return True def _load_status_variables(self) -> None: From 373a23e3c8d92ac59d918469dd1de0ffbc9aac07 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 04:06:47 +0200 Subject: [PATCH 21/26] added GUI dumping of status variables button to modules widget shorter tooltip --- src/qudi/core/gui/main_gui/main_gui.py | 3 +++ src/qudi/core/gui/main_gui/modulewidget.py | 19 +++++++++++++++++++ src/qudi/core/modulemanager.py | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 0914a8975..9b006708b 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -180,6 +180,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 @@ -208,6 +210,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) diff --git a/src/qudi/core/gui/main_gui/modulewidget.py b/src/qudi/core/gui/main_gui/modulewidget.py index d61121d7e..e13f9745d 100644 --- a/src/qudi/core/gui/main_gui/modulewidget.py +++ b/src/qudi/core/gui/main_gui/modulewidget.py @@ -33,6 +33,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 +46,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 +74,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 +82,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 +94,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 +108,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 +116,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 +124,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,6 +144,10 @@ 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) @@ -229,6 +244,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 +259,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): @@ -307,6 +324,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 +346,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): diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 4f82de8f5..ed7033ea1 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -237,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: From ed408079e4283d34b4d6ece25d2df357d9b8974b Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 04:08:40 +0200 Subject: [PATCH 22/26] debug message improvement --- src/qudi/core/modulemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index ed7033ea1..92e2d1477 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -273,7 +273,7 @@ def dump_status_variables(self): """ Method that dumps the status variables of all active modules. """ - logger.debug(f"Dumping status variables") + logger.debug(f"Dumping status variables of all modules") with self._lock: for _, module in self.modules.items(): if module.is_active: From 3121159d7866f820d5f02674efbbd5844eb96d21 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 04:40:34 +0200 Subject: [PATCH 23/26] removed modulemanager automatic status var saving fixed disconnection error --- src/qudi/core/gui/main_gui/main_gui.py | 57 ++----------------------- src/qudi/core/modulemanager.py | 59 -------------------------- 2 files changed, 3 insertions(+), 113 deletions(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 9b006708b..0198c1fdc 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -48,12 +48,7 @@ class QudiMainGui(GuiBase): # status vars _console_font_size = StatusVar(name='console_font_size', default=10) _show_error_popups = StatusVar(name='show_error_popups', default=True) - _automatic_status_var_dump = StatusVar( - name="automatic_status_var_dump", default=False - ) - _automatic_status_var_dump_interval_min = StatusVar( - name="automatic_status_var_dump_interval", default=5 - ) + signal_update_automatic_status_var_checkstate = QtCore.Signal(bool) signal_update_automatic_status_var_interval = QtCore.Signal(int) @@ -113,16 +108,7 @@ def on_activate(self): self._init_remote_modules_widget() self.reset_default_layout() - # set correct automatic status variable dumping behaviour - self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( - self._automatic_status_var_dump_interval_min - ) - self.signal_update_automatic_status_var_interval.emit( - self._automatic_status_var_dump_interval_min * 60 # convert min to s - ) - self.signal_update_automatic_status_var_checkstate.emit( - self._automatic_status_var_dump - ) + self.show() def on_deactivate(self): @@ -147,22 +133,7 @@ def _connect_signals(self): self.mw.action_dump_status_variables.triggered.connect( qudi_main.module_manager.dump_status_variables, QtCore.Qt.QueuedConnection ) - self.signal_update_automatic_status_var_checkstate.connect( - qudi_main.module_manager.toggle_automated_status_variable_dumping, - QtCore.Qt.QueuedConnection, - ) - #self.signal_update_automatic_status_var_checkstate.connect( - # self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled, - # QtCore.Qt.QueuedConnection, - #) - self.signal_update_automatic_status_var_interval.connect( - qudi_main.module_manager.automated_status_variable_dumping_timer_interval_slot, - QtCore.Qt.QueuedConnection, - ) - self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.connect( - self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled, - 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) @@ -192,9 +163,6 @@ def _disconnect_signals(self): 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.signal_update_automatic_status_var_checkstate.disconnect() - self.signal_update_automatic_status_var_interval.disconnect() - self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.stateChanged.disconnect() self.mw.action_view_default.triggered.disconnect() # Disconnect signals from manager qudi_main.configuration.sigConfigChanged.disconnect(self.update_config_widget) @@ -263,8 +231,6 @@ def reset_default_layout(self): self.mw.action_view_console.setChecked(self._has_console) self.mw.action_view_console.setVisible(self._has_console) - self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked(self._automatic_status_var_dump) - self.mw.settings_dialog.dump_status_variables_interval_spinbox.setEnabled(self._automatic_status_var_dump) return def handle_log_record(self, entry): @@ -350,12 +316,6 @@ def keep_settings(self): """ self.mw.settings_dialog.font_size_spinbox.setValue(self._console_font_size) self.mw.settings_dialog.show_error_popups_checkbox.setChecked(self._show_error_popups) - self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.setChecked( - self._automatic_status_var_dump - ) - self.mw.settings_dialog.dump_status_variables_interval_spinbox.setValue( - self._automatic_status_var_dump_interval_min - ) def apply_settings(self): """ Apply values from settings dialog. @@ -371,17 +331,6 @@ def apply_settings(self): self.error_dialog.set_enabled(error_popups) self._show_error_popups = error_popups - # Automatic status variable dumping - interval = ( - self.mw.settings_dialog.dump_status_variables_interval_spinbox.value() - ) - self.signal_update_automatic_status_var_interval.emit(interval*60) # convert min to s - self._automatic_status_var_dump_interval_min = interval - - toggle = self.mw.settings_dialog.checkbox_automatic_status_variable_dumping.isChecked() - self.signal_update_automatic_status_var_checkstate.emit(toggle) - self._automatic_status_var_dump = toggle - @QtCore.Slot() def _error_dialog_enabled_changed(self): """ Callback for the error dialog disable checkbox diff --git a/src/qudi/core/modulemanager.py b/src/qudi/core/modulemanager.py index 92e2d1477..a07c35049 100644 --- a/src/qudi/core/modulemanager.py +++ b/src/qudi/core/modulemanager.py @@ -279,65 +279,6 @@ def dump_status_variables(self): if module.is_active: module.instance.dump_status_variables() - def toggle_automated_status_variable_dumping(self, toggle): - """ - Method that creates or destroys the QTimer for handling the automatic dumping of status variables. - - @param bool toggle: boolean that determines whether timer is created or destroyed. - """ - if not toggle: - logger.info(f"Automated status variable saving disabled.") - self.automated_status_variable_dumping_timer.stop() - try: - self.automated_status_variable_dumping_timer.timeout.disconnect() - except RuntimeError: - pass - return - - logger.info( - f"Automated status variable saving enabled with interval {self.automated_status_variable_dumping_timer_interval} s." - ) - - self.automated_status_variable_dumping_timer.setInterval( - self.automated_status_variable_dumping_timer_interval * 1e3 - ) - self.automated_status_variable_dumping_timer.timeout.connect( - self.dump_status_variables, QtCore.Qt.QueuedConnection - ) - self.automated_status_variable_dumping_timer.start() - - @property - def automated_status_variable_dumping_timer_interval(self): - """ - Property for the timer interval of the automatic status variable saving in s. - - @return float: timer interval in s - """ - return self._automated_status_variable_dumping_timer_interval - - @automated_status_variable_dumping_timer_interval.setter - def automated_status_variable_dumping_timer_interval(self, interval: float): - """ - 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._automated_status_variable_dumping_timer_interval = interval - self.automated_status_variable_dumping_timer.setInterval(interval * 1e3) - logger.info( - f"Setting automated status variable saving timer interval to {interval} s." - ) - - def automated_status_variable_dumping_timer_interval_slot(self, interval): - """ - Method that acts as slot method for calling automated_status_variable_dumping_timer_interval setter. - - @param int interval: interval of the timer in s - """ - self.automated_status_variable_dumping_timer_interval = interval - @QtCore.Slot() def _activate_module_slot(self): """ From f8b70da259892fccb9a9466d01cd4784db1b2fdb Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 05:05:31 +0200 Subject: [PATCH 24/26] add ModuleWidget to SettingsDialog --- src/qudi/core/gui/main_gui/main_gui.py | 3 +++ src/qudi/core/gui/main_gui/settingsdialog.py | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/qudi/core/gui/main_gui/main_gui.py b/src/qudi/core/gui/main_gui/main_gui.py index 0198c1fdc..b74eca1ef 100644 --- a/src/qudi/core/gui/main_gui/main_gui.py +++ b/src/qudi/core/gui/main_gui/main_gui.py @@ -353,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/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index 1913714e4..e9b0806c2 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -20,6 +20,7 @@ """ from PySide2 import QtCore, QtWidgets +from qudi.core.gui.main_gui.modulewidget import ModuleWidget from qudi.util.widgets.scientific_spinbox import ScienSpinBox @@ -84,11 +85,22 @@ def __init__(self, parent=None, **kwargs): layout.addWidget(label, 3, 0) layout.addWidget(self.dump_status_variables_interval_spinbox, 3, 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 = ModuleWidget() + self.module_widget.setObjectName('moduleTabWidgetStatus') + groupbox_layout.addWidget(self.module_widget, 0, 0) + layout.addWidget(groupbox, 4, 0, 1, 2) + buttonbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Apply) buttonbox.setOrientation(QtCore.Qt.Horizontal) - layout.addWidget(buttonbox, 4, 0, 1, 2) + layout.addWidget(buttonbox, 5, 0, 1, 2) + # Add internal signals buttonbox.accepted.connect(self.accept) From 5ef54c8c1d5322996fbb89dbe290b496c4a30540 Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 06:10:24 +0200 Subject: [PATCH 25/26] first GUI for changing auto status variable dumping --- src/qudi/core/gui/main_gui/modulewidget.py | 171 +++++++++++++++++++ src/qudi/core/gui/main_gui/settingsdialog.py | 4 +- 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/qudi/core/gui/main_gui/modulewidget.py b/src/qudi/core/gui/main_gui/modulewidget.py index e13f9745d..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): @@ -153,6 +154,87 @@ 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): """ """ @@ -289,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): """ """ @@ -365,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 e9b0806c2..96fbbb070 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -20,7 +20,7 @@ """ from PySide2 import QtCore, QtWidgets -from qudi.core.gui.main_gui.modulewidget import ModuleWidget +from qudi.core.gui.main_gui.modulewidget import ModuleStatusVariablesWidget from qudi.util.widgets.scientific_spinbox import ScienSpinBox @@ -90,7 +90,7 @@ def __init__(self, parent=None, **kwargs): groupbox_layout = QtWidgets.QGridLayout() groupbox_layout.setRowStretch(1, 1) groupbox.setLayout(groupbox_layout) - self.module_widget = ModuleWidget() + self.module_widget = ModuleStatusVariablesWidget() self.module_widget.setObjectName('moduleTabWidgetStatus') groupbox_layout.addWidget(self.module_widget, 0, 0) layout.addWidget(groupbox, 4, 0, 1, 2) From 0a62d083a10204900a2079df41a833eebfd3007e Mon Sep 17 00:00:00 2001 From: Tobias Spohn Date: Mon, 14 Oct 2024 06:32:23 +0200 Subject: [PATCH 26/26] removed deprecated automatic status variable saving --- src/qudi/core/gui/main_gui/settingsdialog.py | 29 +------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/src/qudi/core/gui/main_gui/settingsdialog.py b/src/qudi/core/gui/main_gui/settingsdialog.py index 96fbbb070..0f121d9f9 100644 --- a/src/qudi/core/gui/main_gui/settingsdialog.py +++ b/src/qudi/core/gui/main_gui/settingsdialog.py @@ -58,33 +58,6 @@ def __init__(self, parent=None, **kwargs): layout.addWidget(label, 1, 0) layout.addWidget(self.show_error_popups_checkbox, 1, 1) - label = QtWidgets.QLabel("Automatic StatusVar saving") - label.setObjectName("autoStatusVarSavingLabel") - label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.checkbox_automatic_status_variable_dumping = QtWidgets.QCheckBox() - self.checkbox_automatic_status_variable_dumping.setChecked(False) - self.checkbox_automatic_status_variable_dumping.setToolTip( - "Activate / Deactivate automatic dumping of status variables" - ) - layout.addWidget(label, 2, 0) - layout.addWidget(self.checkbox_automatic_status_variable_dumping, 2, 1) - - label = QtWidgets.QLabel("Automatic StatusVar saving interval") - label.setObjectName("autoStatusVarSavingIntervalLabel") - label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.dump_status_variables_interval_spinbox = ScienSpinBox() - self.dump_status_variables_interval_spinbox.setToolTip( - "Time interval for automatic dumping of status variables" - ) - self.dump_status_variables_interval_spinbox.setSuffix("min") - self.dump_status_variables_interval_spinbox.setMinimum(1) - self.dump_status_variables_interval_spinbox.setMaximum(1440) - self.dump_status_variables_interval_spinbox.setMinimumSize(QtCore.QSize(80, 0)) - self.dump_status_variables_interval_spinbox.setValue(1) - self.dump_status_variables_interval_spinbox.setEnabled(False) - layout.addWidget(label, 3, 0) - layout.addWidget(self.dump_status_variables_interval_spinbox, 3, 1) - # Automatic status variable saving settings groupbox = QtWidgets.QGroupBox("Automatic Status Variable Saving") groupbox_layout = QtWidgets.QGridLayout() @@ -93,7 +66,7 @@ def __init__(self, parent=None, **kwargs): self.module_widget = ModuleStatusVariablesWidget() self.module_widget.setObjectName('moduleTabWidgetStatus') groupbox_layout.addWidget(self.module_widget, 0, 0) - layout.addWidget(groupbox, 4, 0, 1, 2) + layout.addWidget(groupbox, 2, 0, 1, 2) buttonbox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel