"""
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()