Source code for prsctrl_gui.gui.main_window

"""
Implements QMainWindow
"""
from devctrl.gui.widgets.settings import FieldWidgetDict, FieldWidget
import os.path
from typing import Optional

from PyQt6.QtCore import Qt, QTimer, QFileSystemWatcher, QSize
from PyQt6.QtWidgets import QMainWindow, QScrollArea, QSizePolicy, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \
    QVBoxLayout
from PyQt6.QtWidgets import QTabWidget
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QDragEnterEvent
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox

from devctrl import gui as devgui
from devctrl import devices as d
from devctrl.gui.widgets import ToolBar, KeyValueInput, wrap_in_scrollarea, About
from devctrl.utility.event import Event
from devctrl.gui.devices import DeviceStartupWidget
from devctrl.gui.widgets.settings_db import SettingsDbWidget
from devctrl.gui import task as devtask
from prsctrl.measurement.pump_power_calibration import PowerCalibration
from pyqtgraph.Qt import App

from prsctrl.utility.setup import MeasurementSetupInfo

from ..measurement.pump_power_calibration import PowerCalibrationWidget

from ..app_state import device_manager
from ..app_state import sample_db, power_calibration_db, setup_info_db
from .. import devices

# load custom tasks
# from .task import MeasurementTaskWidget
from ..task.tasks import MeasurementTask
from .. import task  # import for side effect... not pretty
from .widgets.help import HelpMenu
from ..resources import get_resource_path, get_resource_path_from, load_icon
from .widgets.menubar import MenuBar
from .widgets.settings import AppSettings
from .widgets.help import HelpMenu
# from .widgets.treeview import TreeView
from ..utility.config import AppConfig
from ..config.runtime import RuntimeConfig

# TODO: use argparse
parsed_args = RuntimeConfig()

import time
import pickle

import logging
log = logging.getLogger(__name__)

import time

import multiprocessing as mp
import threading as mt

import devctrl.devices.lock_in as mod_lock_in
import devctrl.devices.led_controller as mod_led
import devctrl.devices.switch as mod_switch
import devctrl.devices.voltmeter as mod_voltmeter
from devctrl.gui.widgets import PythonConsole, LogSignal, LogViewer, SettingsWidget
from devctrl.utility.sample import Sample
from devctrl.utility.settings_db import YamlFileBackend, SettingsDb

from prsctrl.data import PrsData
from prsctrl.measurement.measurement import MeasurementHandler
from prsctrl.measurement import settings as prs_settings

from devctrl import devices as d
from devctrl.utility.config_file import ConfigFile
from devctrl.gui.utility import constrain_to_screen

from prsctrl import device_keys as dk




from devctrl.devices.shutter import DummyShutter
DummyShutter.add_device("Test Dummy #2")



[docs] class MainWindow(QMainWindow): """The main window of the app. Contains most logic as well as measurement functionality with live updates. """
[docs] def __init__(self) -> None: """ Initialize the Main-Window. """ super().__init__() # Initialize log capture for the logviewer window log.debug(f"Adding log viewer log handler") self._log_signal = LogSignal() logging.getLogger().addHandler(self._log_signal) self._log_signal.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s')) # Initialize log viewer. The viewer must be initialized here to capture signals before it is opened self._logviewer = LogViewer(self._log_signal) self._logviewer.setWindowTitle("Log Viewer") self._logviewer.resize(600, 400) # Init databases, keep reference member for console sample_db_path = AppConfig.get_path(AppConfig.MAIN_CFG.sample_db) self.sample_db = SettingsDb(YamlFileBackend(sample_db_path), Sample) sample_db.init(self.sample_db) setup_info_db_path = AppConfig.get_path(AppConfig.MAIN_CFG.setup_info_db) self.setup_info_db = SettingsDb(YamlFileBackend(setup_info_db_path), MeasurementSetupInfo) setup_info_db.init(self.setup_info_db) power_calibration_db_path = AppConfig.get_path(AppConfig.MAIN_CFG.power_calibration_db) self.power_calib_db = SettingsDb(YamlFileBackend(power_calibration_db_path), PowerCalibration) power_calibration_db.init(self.power_calib_db) # Window-Settings self.setWindowTitle(AppConfig.APP_NAME) x = AppConfig.CACHE_CFG.geometry_x y = AppConfig.CACHE_CFG.geometry_y width = AppConfig.CACHE_CFG.geometry_width height = AppConfig.CACHE_CFG.geometry_height self.setGeometry(x, y, width, height) central_widget = QWidget(self) self.setCentralWidget(central_widget) # Actions self.a_open_help = QAction(text="Help", parent=self) self.a_open_help.setIcon(load_icon("helpcenter")) # self.a_open_help.setIcon(QIcon.fromTheme(QIcon.ThemeIcon.HelpFaq)) self.a_open_help.setShortcut(QKeySequence("F1")) self.a_open_help.triggered.connect(lambda: self.app_open_help()) self.a_open_about = QAction(text="About", parent=self) self.a_open_about.setIcon(QIcon.fromTheme(get_resource_path("icons/icon.svg"))) self.a_open_about.triggered.connect(lambda: self.app_open_about()) self.a_open_log_viewer = QAction(text="Log Viewer") self.a_open_log_viewer.setShortcut(QKeySequence("F8")) self.a_open_log_viewer.setIcon(load_icon("log")) self.a_open_log_viewer.triggered.connect(lambda: self.app_open_log_viewer()) self.a_open_console = QAction(text="Console") self.a_open_console.setShortcut(QKeySequence("F7")) self.a_open_console.setIcon(load_icon("terminal")) self.a_open_console.triggered.connect(lambda: self.app_open_console()) self.a_init_devices = QAction(text="Initialize devices") self.a_init_devices.setIcon(load_icon("device")) self.a_init_devices.triggered.connect(lambda: self.init_devices()) self.a_save_state = QAction(text=self.tr("Save App State")) self.a_save_state.setIcon(load_icon("save-one")) self.a_save_state.triggered.connect(lambda: self.app_save_state()) self.create_toolbars() self.setMenuBar(MenuBar(self)) # must come after toolbars self.setStatusBar(QStatusBar(self)) layout = QHBoxLayout(central_widget) central_widget.setLayout(layout) # Left: Toolbox self.w_lefttab = QTabWidget(self) self.w_lefttab.setMinimumWidth(300) # self.w_lefttab.setMaximumWidth(400) layout.addWidget(self.w_lefttab) # Devices device_config_path = AppConfig.get_path(AppConfig.MAIN_CFG.device_connection_infos_path) self._device_config = ConfigFile(device_config_path) devmg = device_manager.DeviceManager(config_file=self._device_config, prompt_choice_func=devgui.devices.select_device_dialog) # Register the global instance device_manager.init(devmg) # register device manager with widgets that require it but dont have access to app_state devtask.DeviceActionTaskRunWidget.DEVICE_MANAGER = devmg # Keep a reference to the global devmg to make it available in the console via app.devmg self.devmg = devmg self.w_devmg = devgui.devices.device_manager.DeviceManagerWidget(devmg) w = QWidget() w.setLayout(QVBoxLayout()) self.w_devmg_settings = devgui.devices.device_manager.DeviceManagerSettingsWidget(devmg) w.layout().addWidget(self.w_devmg_settings) w.layout().addWidget(self.w_devmg) self.w_devmg_scroll = QScrollArea() self.w_devmg_scroll.setWidgetResizable(True) self.w_devmg_scroll.setWidget(w) self.w_lefttab.addTab(self.w_devmg_scroll, load_icon("devices"), self.tr("Devices")) # Measurement metadata metadata_init_dict = AppConfig.CACHE_CFG.global_metadata_input # global_metadata.init(metadata_init_dict) # self.w_metadata = KeyValueInput(metadata_init_dict) field = FieldWidget.get_dummy_field("metadata", dict[str, str]) self.w_metadata = FieldWidgetDict(metadata_init_dict, field) # self.w_metadata.dataChanged.connect(self._metadata_updated) self.w_lefttab.addTab(self.w_metadata, load_icon("list-add"), self.tr("Measurement Metadata")) from ..measurement import PrsGuiSettings self.w_measurement_settings_2 = SettingsWidget(PrsGuiSettings()) self.w_lefttab.addTab(self.w_measurement_settings_2, "Measurement Settings 2") from devctrl.gui.task import TaskListWidget, TaskRunWidget, TaskManagerWidget from devctrl.gui.task.run import DebugTaskRunWidget, WaitTaskRunWidget, UserConfirmTaskRunWidget, SendEmailTaskRunWidget, DeviceActionTaskRunWidget from devctrl.task.tasks import Task, DebugTask, WaitTask, UserConfirmTask, SendEmailTask, DeviceActionTask from ..task.run import MeasurementTaskRunWidget TaskManagerWidget.set_runner(devtask.DebugTask, devtask.DebugTaskRunWidget) TaskManagerWidget.set_runner(devtask.WaitTask, devtask.WaitTaskRunWidget) TaskManagerWidget.set_runner(MeasurementTask, MeasurementTaskRunWidget) TaskManagerWidget.set_runner(devtask.UserConfirmTask, devtask.UserConfirmTaskRunWidget) TaskManagerWidget.set_runner(devtask.SendEmailTask, devtask.SendEmailTaskRunWidget) TaskManagerWidget.set_runner(devtask.DeviceActionTask, devtask.DeviceActionTaskRunWidget) devtask.SendEmailTaskRunWidget.SMTP_SETTINGS_GETTER = lambda *args: AppConfig.MAIN_CFG.smtp_settings # setup load/save tasks as file/preset task_dir = AppConfig.get_path(AppConfig.MAIN_CFG.task_preset_dir) self.task_preset_manager = devtask.TaskPresetManager(task_dir) initial_task_directory = AppConfig.get_path(AppConfig.CACHE_CFG.last_task_dir) initial_tasklist_directory = AppConfig.get_path(AppConfig.CACHE_CFG.last_tasklist_dir) self.w_taskmanager = TaskManagerWidget(preset_manager=self.task_preset_manager, initial_task_directory=initial_task_directory, initial_tasklist_directory=initial_tasklist_directory) # store directory in which a task was last saved/loaded def set_task_dir(last_task_dir): AppConfig.CACHE_CFG.last_task_dir = last_task_dir self.w_taskmanager.w_tasklist.taskDirectoryChanged.connect(set_task_dir) def set_tasklist_dir(last_tasklist_dir): AppConfig.CACHE_CFG.last_tasklist_dir = last_tasklist_dir self.w_taskmanager.w_tasklist.tasklistDirectoryChanged.connect(set_tasklist_dir) # init tasklist from last time tasklist_path = AppConfig.get_path(AppConfig.MAIN_CFG.tasklist_path) self.w_taskmanager.w_tasklist.load_tasklist(tasklist_path) self.w_taskmanager.tasks_started.connect(self._on_tasks_started) self.w_taskmanager.tasks_stopped.connect(self._on_tasks_stopped) self.w_lefttab.addTab(self.w_taskmanager, load_icon("list-success"), self.tr("Tasks")) # Databases self.w_sample_db = SettingsDbWidget(self.sample_db) self.w_lefttab.addTab(self.w_sample_db, load_icon("data"), self.tr(self.tr("Samples"))) self.w_power_calibration = PowerCalibrationWidget(self.power_calib_db) self.w_lefttab.addTab(self.w_power_calibration, load_icon("flashlight"), self.tr("Power Calibation")) self.w_setup_info_db = SettingsDbWidget(self.setup_info_db) self.w_lefttab.addTab(self.w_setup_info_db, load_icon("info"), self.tr("Setup Info")) # TODO: the call_f solution isnt pretty # TODO: only accept single file? # TODO: das drag-drop zeug muss später im lefttab implementiert werden # - if a folder, an pickle file or an hdf5 file is being dragged in -> open dataset in new tab # - if a yaml file is dragged in, read contents and determine whether its a task config or sth else and pass on to tasklist class RightTab(QTabWidget): def __init__(self, call_f, parent=None): super().__init__(parent) self.setAcceptDrops(True) self.call_f = call_f def dragEnterEvent(self, event): if not event.mimeData().hasUrls(): event.ignore() if len(event.mimeData().urls()) != 1: event.ignore() event.setDropAction(Qt.DropAction.LinkAction) event.accept() def dropEvent(self, event): for url in event.mimeData().urls(): file_path = url.path()[1:] self.call_f(file_path) # # Right: Tabs: Script, Plot # self.w_right_tab = RightTab(self.measurement_load) # layout.addWidget(self.w_right_tab) max_data_points = AppConfig.MAIN_CFG.plot_max_data_points from ..measurement.viewer import PrsLiveDataWidget from ..app_state import live_data self.w_live = PrsLiveDataWidget() # self.w_right_tab.addTab(self.w_plot, "Plot") self.w_lefttab.addTab(self.w_live, load_icon("chart-line-area"), self.tr("Live Plot")) live_data.set_datas.connect(self.w_live.set_modes) live_data.data_update.connect(self.w_live.add_data) live_data.set_message.connect(self.w_live.set_text) live_data.data_updated.connect(self.w_live.update_plot) testdata = "/home/matth/Uni/Physik_Master/Arbeit/wsi-share/photoreflectance/data-O-Ta3N5_TaO55-Q/2025-09-15_13-14_O-Ta3N5_TaO55-Q_wl=275nm_power=1mWcm2/2025-09-15_13-14_O-Ta3N5_TaO55-Q_wl=275nm_power=1mWcm2-ref" if os.path.isdir(testdata): data = PrsData(testdata) from .widgets.data import PrsDataWidget w = PrsDataWidget(data) self.w_lefttab.addTab(w, "PrsData") from devctrl.gui.test.settings import TestSettings # test_settings = TestSettings() # test = SettingsWidget(test_settings) _task = devtask.settings.DeviceActionTask() test = devtask.settings.DeviceActionTaskSettingsWidget(_task) self.w_lefttab.addTab(test, "Test") self.verbose = True # devices # TODO exe = AppConfig.MAIN_CFG.power_switch_exe # Measurement self.set_status(self.tr("Ready")) self.menuBar().m_file.addAction(self.a_open_about) self.menuBar().m_file.addAction(self.a_open_help) self._console: Optional[PythonConsole] = None self.idle_start() self.timer_handle_events = QTimer(self) self.timer_handle_events.timeout.connect(lambda: Event.handle_events()) self.timer_handle_events.start(500)
[docs] def set_status(self, msg): self.statusBar().showMessage(msg)
[docs] def create_toolbars(self) -> None: """ Creates and adds the top and right toolbars to the main window. """ # Top Toolbar [PyQt6.QtWidgets.QToolBar] self.topbar = ToolBar(self, orientation=Qt.Orientation.Horizontal, style=Qt.ToolButtonStyle.ToolButtonTextUnderIcon, icon_size=(24, 24)) # Top Toolbar Buttons self.a_meas_start = QAction(load_icon("play-two"), self.tr("Start Tasks")) self.a_meas_start.triggered.connect(self.tasks_start) self.a_meas_stop = QAction(load_icon("play-two"), self.tr("Stop Tasks")) self.a_meas_stop.triggered.connect(self.tasks_stop) self.a_meas_stop.setDisabled(True) self.a_meas_save = QAction(load_icon("save-one"), self.tr("Save")) self.a_meas_save.triggered.connect(self.measurement_save) self.a_meas_save.setDisabled(True) self.a_meas_load = QAction(load_icon("folder-open"), self.tr("Load")) self.a_meas_load.triggered.connect(self.measurement_load_dialog) self.a_power_off = QAction(load_icon("power_off"), self.tr("Power Off")) self.a_power_off.triggered.connect(self.power_off) self.a_open_settings = QAction(load_icon("setting-two"), self.tr("Settings")) self.a_open_settings.triggered.connect(self.app_open_settings) self.a_exit = QAction(load_icon("close"), self.tr("Exit")) self.a_exit.triggered.connect(self.app_exit) self.topbar.addAction(self.a_meas_start) self.topbar.addAction(self.a_meas_stop) self.topbar.addAction(self.a_meas_save) self.topbar.addAction(self.a_meas_load) self.topbar.add_separator() self.topbar.addAction(self.a_init_devices) self.topbar.addAction(self.a_power_off) self.topbar.add_separator() self.topbar.addAction(self.a_open_settings) self.topbar.addAction(self.a_open_help) self.topbar.addAction(self.a_open_about) self.topbar.addAction(self.a_exit) self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.topbar) self.a_run_alignment = QAction(load_icon("play"), self.tr("Alignment mode")) self.a_run_alignment.triggered.connect(self.alignment_mode) self.idle_timer = None
[docs] def todo(self): log.error(f"TODO")
# IDLE - NOT IN MEASUREMENT # check if devices stayed connected
[docs] def idle_start(self): if self.idle_timer is None: self.idle_timer = QTimer(self) self.idle_timer.timeout.connect(self.idle_update) self.idle_timer.start(AppConfig.MAIN_CFG.idle_update_interval_s *1000) else: log.debug("Not starting idle timer - it is already running")
[docs] def idle_update(self): pass
[docs] def idle_stop(self): if self.idle_timer is not None: self.idle_timer.stop() self.idle_timer = None else: log.debug("Not stopping idle timer - it is not running")
# MEASUREMENT
[docs] def tasks_start(self): self.w_taskmanager.run_tasks()
[docs] def _on_tasks_started(self): """Update UI when tasks are started""" self.topbar.disable_button("meas_start") self.topbar.disable_button("meas_save") self.topbar.disable_button("meas_load") self.a_init_devices.setDisabled(True) self.topbar.enable_button("meas_stop") self.set_status(self.tr("Tasks running...")) # if not AppConfig.MAIN_CFG.get_or("metadata_auto_update", True): # self.w_metadata.setEnabled(False) # self.w_live.clear_data() self.idle_stop()
[docs] def tasks_stop(self): log.info("Stopping tasks") if not self.tasks_are_running(): raise RuntimeError("tasks_stop: Tasks is not running") self.set_status(self.tr("Stopping tasks")) self.w_taskmanager.stop_tasks()
[docs] def _on_tasks_stopped(self): """Update UI when tasks are stopped""" self.topbar.enable_button("meas_start") self.topbar.enable_button("init_devices") self.topbar.enable_button("meas_save") self.topbar.enable_button("meas_load") self.topbar.disable_button("meas_stop") self.w_metadata.setEnabled(True) self.set_status(self.tr("Ready")) self.idle_start()
[docs] def tasks_are_running(self): return self.w_taskmanager.is_running()
[docs] def measurement_save(self) -> None: """ Save the last measurement data to a file. """ if self.data_collector is None: raise RuntimeError("Can not save, not data collecthttps://www.wsi.tum.de/views/groups.php?group=sharpor initialized") # if not loaded already, this will load the data into memory, which might take a while # this should be done before the dialog self.data_collector.get_data() # open file dialog with previous in directory with previous file extension last_dir = AppConfig.get_path(AppConfig.CACHE_CFG.last_measurement_save_dir) ext = AppConfig.CACHE_CFG.last_measurement_save_ext if ext not in ["csv", "pkl"]: ext = "csv" if last_dir is None: last_dir = AppConfig.get_path(AppConfig.MAIN_CFG.datadir) if isinstance(last_dir, list): last_dir = last_dir[0] init_path = os.path.join(last_dir, self.data_collector.dirname + "." + ext) file_path, _ = QFileDialog.getSaveFileName(self, "Save File", init_path, "CSV Files (*.csv);;Pickle Files (*.pkl)") if file_path: AppConfig.CACHE_CFG.last_measurement_load_dir = os.path.dirname(file_path) if file_path.endswith(".csv"): AppConfig.CACHE_CFG.last_measurement_save_ext = "csv" csv = self.data_collector.to_csv() with open(file_path, "w") as f: f.write(csv) elif file_path.endswith(".pkl"): AppConfig.CACHE_CFG.last_measurement_save_ext = "pkl" data = self.data_collector.get_data() with open(file_path, "wb") as f: pickle.dump(data, f) else: raise ValueError(f"Unknown file extension in path: '{file_path}'") self.set_status(f"Saved data to {file_path}") else: self.set_status(f"Aborted saving data, no file selected")
[docs] def measurement_load(self, filepath): """ Load measurement data from the filepath. Only updates the plot. Parameters ---------- filepath The path to the file or directory to load the data from. File may be csv or pickle containing (data, metadata) """ log.info(f"Loading measurement data from '{filepath}'") if self.tasks_are_running(): QMessageBox.critical(self, "Measurement running", "Can not load data while measurement is running") return if os.path.isfile(filepath): if filepath.endswith(".pkl"): with open(filepath, "rb") as file: data, mdata = pickle.load(file) elif filepath.endswith(".csv"): data, mdata = PrsData.load_data_from_csv(filepath) else: raise NotImplementedError(f"Unknown file extension in path: '{filepath}'.\nOnly .pkl and .csv can be loaded") elif os.path.isdir(filepath): data, mdata = PrsData.load_data_from_dir(filepath) else: raise FileNotFoundError(f"No such file or directory: '{filepath}'") self.w_live.set_data(data[:,1], data[:,2], data[:,3])
[docs] def measurement_load_dialog(self): """ Open a file dialog to load measurement data. This only updates the plot """ if self.tasks_are_running(): QMessageBox.critical(self, "Measurement running", "Can not load data while measurement is running") return last_dir = AppConfig.get_path(AppConfig.CACHE_CFG.last_measurement_load_dir) file_path, _ = QFileDialog.getOpenFileName(self, "Load File", last_dir, "CSV Files (*.csv)") if file_path: dir_name = os.path.dirname(file_path) AppConfig.CACHE_CFG.last_measurement_load_dir = dir_name self.measurement_load(file_path) else: self.set_status(f"Aborted loading data, no file selected") self.measurement_load(file_path)
[docs] def init_devices(self): # Register device roles with device manager, will be connected later self.devmg.add_device(key=dk.AMPLIFIER_REF, name="Amplifier (REF)", global_varname="ref_amplifier", device_type=d.Amplifier) self.devmg.add_device(key=dk.AMPLIFIER_TRA, name="Amplifier (TRA)", global_varname="tra_amplifier", device_type=d.Amplifier) self.devmg.add_device(key=dk.LAMP_HALOGEN, name="Halogen lamp", global_varname="halogen", device_type=d.Lamp) self.devmg.add_device(key=dk.LAMP_MIRROR, name="Lamp Mirror (SAM)", global_varname="sam", device_type=d.Switch) self.devmg.add_device(key=dk.LAMP_XENON, name="Xenon lamp", global_varname="xenon", device_type=d.Lamp) self.devmg.add_device(key=dk.LASER, name="Laser", global_varname="laser", device_type=d.Laser) self.devmg.add_device(key=dk.LED_CONTROLLER, name="LED Controller", global_varname="led", device_type=d.LedController) self.devmg.add_device(key=dk.LOCK_IN_REF, name="Lock-In (REF)", global_varname="ref_lockin", device_type=d.LockInAmp) self.devmg.add_device(key=dk.LOCK_IN_TRA, name="Lock-In (TRA)", global_varname="tra_lockin", device_type=d.LockInAmp) self.devmg.add_device(key=dk.MONOCHROMATOR_DETECTOR, name="Monochromator-Detector", global_varname="kymera", device_type=d.Monochromator) self.devmg.add_device(key=dk.MONOCHROMATOR_PROBE, name="Monochromator", global_varname="mcm", device_type=d.Monochromator) self.devmg.add_device(key=dk.POWER_METER_PUMP, name="Pump-Power-Meter", global_varname="power_meter", device_type=d.PowerMeter) self.devmg.add_device(key=dk.POWER_SWITCH_PRS, name="Power Switch (PRS)", global_varname="power_switch_PRS", device_type=d.Switch, connect_priority=98) self.devmg.add_device(key=dk.POWER_SWITCH_TAS, name="Power Switch (TAS)", global_varname="power_switch_TAS", device_type=d.Switch, connect_priority=99) self.devmg.add_device(key=dk.SHUTTER_PROBE, name="Shutter", global_varname="shutter", device_type=d.Shutter) self.devmg.add_device(key=dk.TEMPERATURE_SENSOR, name="Temperature Sensor", global_varname="temp_sensor", device_type=d.TemperatureSensor) self.devmg.add_device(key=dk.WHEEL, name="Sample Wheel", global_varname="wheel", device_type=d.Wheel) # init the devices if not (parsed_args.connect_no_devices or parsed_args.connect_dummy_devices): DeviceStartupWidget.open_dialog( device_manager=self.devmg, power_switch_autoconnect_keys=[dk.POWER_SWITCH_PRS, dk.POWER_SWITCH_TAS], power_switch_autoconnect_devices_timeout_s=10, parent=self ) elif parsed_args.connect_dummy_devices: log.info("Connecting dummy devices") for k in self.devmg: self.devmg.connect_dummy(k)
[docs] def power_off(self): self.devmg.get_device_or if self.power_switch is None: raise RuntimeError("No power switch configured") log.info("Powering off") self.idle_stop() # dont show device disconnected errors self.power_switch.off() self.idle_start()
[docs] def alignment_mode(self): """Set a visible light beam.""" mcm: d.Monochromator = self.devmg.get_device(dk.MONOCHROMATOR_PROBE) mcm.set_resolution_nm(1) mcm.set_wavelength_nm(532) sam: d.Switch = self.devmg.get_device(dk.LAMP_MIRROR) sam.on() shutter: d.Shutter = self.devmg.get_device(dk.SHUTTER_PROBE) shutter.open() amp_ref = self.devmg.get_device(dk.AMPLIFIER_REF) amp_ref.set_gain(6) amp_tra = self.devmg.get_device(dk.AMPLIFIER_TRA) amp_tra.set_gain(6)
[docs] def app_save_state(self) -> None: """Write all configuration and runtime changes to disk.""" log.info(f"Saving state") self.w_taskmanager.w_tasklist.save_tasklist(AppConfig.get_path(AppConfig.MAIN_CFG.tasklist_path)) self.sample_db.save() self.power_calib_db.save() self.setup_info_db.save() self.w_power_calibration.save_state() md = self.w_metadata.get_value() AppConfig.CACHE_CFG.global_metadata_input = md self.devmg.update_config_file() self._device_config.save() if self._console is not None: self._console.write_history()
[docs] def app_exit(self) -> None: """ Closes the application. """ if self.w_taskmanager.is_running(): self.w_taskmanager.stop_tasks() self.app_save_state() self.app_close_console() self.app_close_log_viewer() self.close()
[docs] def app_open_console(self) -> None: """Open and focus the log viewer window. The console is created if not existing already """ # Only create the console if it doesn't exist yet if self._console is None: local_variables = { "app": self, "print_widget_size_tree": print_widget_size_tree, } | self.devmg.get_globals() # | locals() banner = "Python console - use only if you know what you are doing!" banner += "\nThe following variables are available: " + ", ".join(local_variables.keys()) + "\n" try: # history_file = AppConfig.get_path(AppConfig.MAIN_CFG.console_history_path, check_can_read=True, check_can_write=True) history_file = AppConfig.get_path(AppConfig.MAIN_CFG.console_history_path) except ValueError as e: log.error(f"{e}") history_file = None self._console = PythonConsole(locals=local_variables, use_globals=True, banner=banner, history_file=history_file) self._console.setWindowTitle("Python Console") self._console.resize(600, 400) self._console.show() self._console.activateWindow() self._console.raise_()
[docs] def app_close_console(self) -> None: if hasattr(self, '_console') and self._console is not None: self._console.close()
[docs] def app_open_log_viewer(self) -> None: """ Open and focus the log viewer window. """ self._logviewer.show() self._logviewer.activateWindow() self._logviewer.raise_()
[docs] def app_close_log_viewer(self) -> None: self._logviewer.close()
[docs] def app_open_about(self) -> None: dialog = QDialog() dialog.setWindowTitle(("About prsctrl-gui")) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) buttons.accepted.connect(dialog.accept) dialog.setLayout(QVBoxLayout()) img_path = get_resource_path_from("prsctrl_gui.resources", "icons/logo.svg") md_path = get_resource_path_from("prsctrl_gui.resources", "about.md") pkg_versions = ["prsctrl_gui", "prsctrl", "devctrl"] dialog.layout().addWidget(About(img_path, md_path, pkg_versions=pkg_versions)) dialog.layout().addWidget(buttons) dialog.resize(constrain_to_screen(dialog.layout().sizeHint(), 100, 100, 0.5, 0.7)) dialog.exec()
[docs] def app_open_help(self) -> None: dialog = QDialog() dialog.setWindowTitle(self.tr("Help")) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) buttons.accepted.connect(dialog.accept) dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(HelpMenu()) dialog.layout().addWidget(buttons) # set larger window size dialog.resize(constrain_to_screen(dialog.layout().sizeHint(), 400, 400, 0.8, 0.8)) dialog.exec()
[docs] def app_open_settings(self) -> None: dialog = QDialog() dialog.setWindowTitle(self.tr("Settings")) layout = QVBoxLayout() # w_app_settings = SettingsWidget[AppConfig](AppConfig.MAIN_CFG) # w_app_settings.set_value(AppConfig.MAIN_CFG) w_app_settings = AppSettings() # w_app_settings = AppSettings() scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setWidget(w_app_settings) scroll.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) layout.addWidget(scroll) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) buttons.accepted.connect(dialog.accept) layout.addWidget(buttons) dialog.setLayout(layout) dialog.resize(constrain_to_screen(dialog.layout().sizeHint(), 800, 600, 0.8, 0.8)) dialog.exec()
[docs] def closeEvent(self, event): # if self.measurement_is_running(): # self.measure_stop() # save the metadata - should not be necessary as the widget represents the dict instance of the CACHE_CFG # metadata = self.w_metadata.get_value() # AppConfig.CACHE_CFG.metadata = metadata # store the geometry as text values in config geo = self.geometry() AppConfig.CACHE_CFG.geometry_x = geo.x() AppConfig.CACHE_CFG.geometry_y = geo.y() AppConfig.CACHE_CFG.geometry_width = geo.width() AppConfig.CACHE_CFG.geometry_height = geo.height()