''' cpdctrl_gui/ui/main_window.py ''' from PyQt6.QtCore import Qt, QTimer from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \ QVBoxLayout from PyQt6.QtWidgets import QToolBox, QTabWidget from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox from ..resources import get_resource_path from .widgets.menubar import MenuBar from .widgets.toolbar import ToolBar from .widgets.metadata_input import MetadataInput from .widgets.measurement_settings import MeasurementSettings, ScriptSelection from .widgets.plot import Plot from .widgets.device_select import ListChoice from .widgets.about import MarkdownView from .widgets.led_script import LedScriptViewer # from .widgets.treeview import TreeView import logging log = logging.getLogger(__name__) import multiprocessing as mp import threading as mt from ..utility.config import AppConfig import cpdctrl.voltage_measurement_device as vmd import cpdctrl.led_control_device as ledd from cpdctrl.led_script import LedScript from cpdctrl.utility.data import DataCollector from cpdctrl.measurement import measure class MainWindow(QMainWindow): """ MainWindow Args: QMainWindow (QMainWindow): Inheritance """ def __init__(self) -> None: """ Initialize the Main-Window. """ super().__init__() # Window-Settings self.setWindowTitle(AppConfig.APP_NAME) self.setGeometry(100, 100, 800, 600) central_widget = QWidget(self) self.setCentralWidget(central_widget) 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_leftbox = QToolBox(self) self.w_leftbox.setMinimumWidth(300) layout.addWidget(self.w_leftbox) metadata_init_dict = AppConfig.MEAS_CFG.get_or("metadata", {}) self.w_metadata = MetadataInput(metadata_init_dict) self.w_leftbox.addItem(self.w_metadata, "Measurement metadata") # Measurement settings self.w_measurement_settings = MeasurementSettings() self.w_leftbox.addItem(self.w_measurement_settings, "Measurement settings") self.w_measurement_settings.set_value("interval", AppConfig.MAIN_CFG.get_or("interval", 0.5)) # Right: Tabs: Script, Plot self.w_right_tab = QTabWidget() layout.addWidget(self.w_right_tab) self.w_plot = Plot() self.w_right_tab.addTab(self.w_plot, "Plot") # LED SCRIPT self.w_led_script = LedScriptViewer(LedScript(0)) self.w_right_tab.addTab(self.w_led_script, "LED Script") self.w_measurement_settings.w_led_script.script_changed.connect(self.led_script_load) self.verbose = True # devices self.vmdev = None self.leddev = None self.vmdev_autoconnect() self.leddev_autoconnect() # Measurement self.measurement_timer = None self.led_script = None self.data_collector = None self.data_queue = None self.proc_measure = None self.set_status("Ready") def set_status(self, msg): self.statusBar().showMessage(msg) 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.topbar.add_button("connect_vmdev", "CPD Devices", get_resource_path("icons/volt_device.svg"), self.vmdev_connect_from_dialog) self.topbar.add_button("connect_leddev", "LED Devices", get_resource_path("icons/led_device.svg"), self.leddev_connect_from_dialog) self.topbar.add_button("meas_start", "Start", QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart), self.measure_start) self.topbar.add_button("meas_stop", "Stop", QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStop), self.measure_stop) self.topbar.add_button("meas_save", "Save", QIcon.fromTheme(QIcon.ThemeIcon.DocumentSaveAs), self.measurement_save) self.topbar.add_button("app_about", "About", get_resource_path("icons/icon.svg"), self.app_open_about) self.topbar.add_separator() self.topbar.add_button("app_exit", "Exit", QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), self.app_exit) self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.topbar) # disable the Stop and Save buttons self.topbar.disable_button("meas_stop") self.topbar.disable_button("meas_save") # Right Toolbar [PyQt6.QtWidgets.QToolBar] # self.rightbar = ToolBar(self, orientation=Qt.Orientation.Vertical, style=Qt.ToolButtonStyle.ToolButtonIconOnly, icon_size=(24, 24)) # self.rightbar.add_separator() # self.rightbar.add_button("Settings", "resources/assets/icons/windows/shell32-315.ico", self.settings_window) # self.addToolBar(Qt.ToolBarArea.RightToolBarArea, self.rightbar) # def create_treeview(self) -> TreeView: """ Creates and adds the tree view widget to the main window. """ # return TreeView(self) # LED DEVICE MANAGEMENT def leddev_connect(self, leddev_type, leddev_name): log.info(f"Connecting to LED device {leddev_name} ({leddev_type})") self.leddev = ledd.connect_device(leddev_type, leddev_name) AppConfig.MAIN_CFG.set("led_device_last.type", leddev_type) AppConfig.MAIN_CFG.set("led_device_last.name", leddev_name) def leddev_autoconnect(self): if AppConfig.MAIN_CFG.get_or("led_device_auto_reconnect", False): try: leddev_type = AppConfig.MAIN_CFG.get("led_device_last.type") leddev_name = AppConfig.MAIN_CFG.get("led_device_last.name") self.leddev_connect(leddev_type, leddev_name) except KeyError: pass except Exception as e: log.error(f"Failed to auto-connect to LED device: {e}") def leddev_connect_from_dialog(self): """ Open a dialog that lets the user choose a LED control device, and then connects to the device. """ devices = ledd.list_devices() device_dialog = ListChoice(devices, window_title="Select LED device") if device_dialog.exec() == QDialog.DialogCode.Accepted: leddev_type, leddev_name = device_dialog.get_selected() self.leddev_connect(leddev_type, leddev_name) # VOLTAGE DEVICE MANAGEMENT def vmdev_connect(self, vmdev_type, vmdev_name): log.info(f"Connecting to voltage measurement device {vmdev_name} ({vmdev_type})") self.vmdev = vmd.connect_device(vmdev_type, vmdev_name) AppConfig.MAIN_CFG.set("voltage_measurement_device_last.type", vmdev_type) AppConfig.MAIN_CFG.set("voltage_measurement_device_last.name", vmdev_name) def vmdev_autoconnect(self): if AppConfig.MAIN_CFG.get_or("voltage_measurement_device_auto_reconnect", False): try: vmdev_type = AppConfig.MAIN_CFG.get("voltage_measurement_device_last.type") vmdev_name = AppConfig.MAIN_CFG.get("voltage_measurement_device_last.name") self.vmdev_connect(vmdev_type, vmdev_name) except KeyError: pass except Exception as e: log.error(f"Failed to auto-connect to voltage measurement device: {e}") def vmdev_connect_from_dialog(self): """ Open a dialog that lets the user choose a voltage measurement device, and then connects to the device. """ devices = vmd.list_devices() device_dialog = ListChoice(devices, window_title="Select CPD Measurement Device") if device_dialog.exec() == QDialog.DialogCode.Accepted: vmdev_type, vmdev_name = device_dialog.get_selected() self.vmdev_connect(vmdev_type, vmdev_name) # MEASUREMENT def measure_start(self): if self.vmdev is None: self.vmdev_connect_from_dialog() if self.vmdev is None: raise ValueError("No measurement device selected") if self.leddev is None: self.leddev_connect_from_dialog() if self.leddev is None: raise ValueError("No led control device selected") self.topbar.disable_button("meas_start") self.topbar.disable_button("connect_vmdev") self.topbar.disable_button("connect_leddev") self.topbar.disable_button("meas_save") self.topbar.enable_button("meas_stop") self.w_plot.clear_data() measurement_name = "guitest" script = self.w_measurement_settings.get_value("led_script") led_script = LedScript(script=script) flush_after = self.w_measurement_settings.get_value("flush_after") use_buffer = self.w_measurement_settings.get_value("use_buffer") max_measurements = self.w_measurement_settings.get_value("max_measurements") stop_on_script_end = self.w_measurement_settings.get_value("stop_on_script_end") interval = self.w_measurement_settings.get_value("interval") metadata = self.w_metadata.get_dict() metadata["interval"] = str(interval) metadata["name"] = measurement_name metadata["led"] = "led" metadata["led_script"] = str(script) self.w_metadata.update_from_dict({ "interval": str(interval), "led_script": str(script) }) log.info(f"Starting measurement with:\n\tinterval = {interval}\n\tflush_after = {flush_after}\n\tuse_buffer = {use_buffer}\n\tmax_measurements = {max_measurements}\n\tstop_on_script_end = {stop_on_script_end}") self.led_script = LedScript(script=script, auto_update=True, verbose=True) self.data_collector = DataCollector(metadata=metadata, data_path=AppConfig.MAIN_CFG.get("datadir"), data_name=measurement_name) # data_collector.clear() self.data_queue = mp.Queue() self.command_queue = mp.Queue() # Argument order must match the definition self.proc_measure = mt.Thread(target=measure, args=( self.vmdev, self.leddev, self.led_script, self.data_collector, interval, flush_after, use_buffer, max_measurements, stop_on_script_end, self.verbose, # verbose self.command_queue, self.data_queue )) self.proc_measure.start() self.measurement_timer = QTimer(self) self.measurement_timer.timeout.connect(self.measure_update) self.measurement_timer.start(300) # TODO: set interval def measure_stop(self): log.info("Stopping measurement") if not self.measurement_is_running(): raise RuntimeError("measure_stop: Measurement is not running") self.command_queue.put("stop") self.measurement_timer.stop() self.proc_measure.join() self.set_status("Ready") self.led_script.stop_updating() # stop watching for file updates (if enabled) self.data_collector.save_csv(verbose=True) data, metadata = self.data_collector.get_data() self.proc_measure = None self.led_script = None self.topbar.enable_button("meas_start") self.topbar.enable_button("connect_vmdev") self.topbar.enable_button("connect_leddev") self.topbar.enable_button("meas_save") self.topbar.disable_button("meas_stop") def measure_update(self): if self.proc_measure.is_alive(): while not self.data_queue.empty(): # print(data_queue.qsize(), "\n\n") current_data = self.data_queue.get(block=False) i, tval, vval, led_val = current_data print(f"Data {i:03}: {tval}s, {vval}V, {led_val}%") self.set_status(f"Data {i:03}: {tval}s, {vval}V, {led_val}%") # update the plot self.w_plot.update_plot(tval, vval, led_val) else: # measurement might have stopped after max N or script end self.measure_stop() def measurement_is_running(self): return self.proc_measure is not None 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 collector initialized") csv = self.data_collector.to_csv() # open file dialog filename = self.data_collector.dirname + ".csv" file_name, _ = QFileDialog.getSaveFileName(self, "Save File", filename, "CSV Files (*.csv)") if file_name: with open(file_name, "w") as f: f.write(csv) self.set_status(f"Saved data to {file_name}") else: self.set_status(f"Aborted saving data, no file selected") def led_script_load(self): script = self.w_measurement_settings.get_value("led_script") try: self.led_script = LedScript(script=script, auto_update=True, verbose=True) except ValueError as e: # show qt error QMessageBox.critical(self, "LED script error", str(e)) return self.w_led_script.set_script(self.led_script) def app_exit(self) -> None: """ Closes the application. """ self.close() def app_open_about(self) -> None: dialog = QDialog() buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) buttons.accepted.connect(dialog.accept) dialog.setLayout(QVBoxLayout()) # show the logo via a pixmap in a label img_path = get_resource_path("icons/logo2.svg") pixmap = QPixmap(img_path) pixmap = pixmap.scaled(128, 128, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) # qt cant find the file label = QLabel() label.setPixmap(pixmap) label.setAlignment(Qt.AlignmentFlag.AlignCenter) # center the image dialog.layout().addWidget(label) # show about.md dialog.layout().addWidget(MarkdownView("about2.md")) dialog.layout().addWidget(buttons) dialog.exec() def closeEvent(self, event): if self.measurement_is_running(): self.measure_stop() # save the metadata metadata = self.w_metadata.get_dict() AppConfig.MEAS_CFG.set("metadata", metadata) event.accept()