''' app/ui/main_window.py ''' from PyQt6.QtCore import Qt, QTimer from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QTextEdit, QLabel, QDialog, QToolButton, QStatusBar, QFileDialog from PyQt6.QtWidgets import QToolBox from PyQt6.QtGui import QIcon from .widgets.menubar import MenuBar from .widgets.toolbar import ToolBar from .widgets.metadata_input import MetadataInput from .widgets.measurement_settings import ScriptSelection, MeasurementSettings from .widgets.plot import Plot from .widgets.device_select import ListChoice # from .widgets.treeview import TreeView 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) layout = QHBoxLayout(central_widget) central_widget.setLayout(layout) self.create_toolbars() # Add Widgets to Window self.setMenuBar(MenuBar(self)) self.setStatusBar(QStatusBar(self)) self.w_leftbox = QToolBox(self) layout.addWidget(self.w_leftbox) init_elements = [("name1", "val1"), ("name2", "val2"), ("interval", 0.5)] self.w_metadata = MetadataInput(init_elements) 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)) self.w_plot = Plot() layout.addWidget(self.w_plot) 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("meas_devices", "Devices", QIcon.fromTheme(QIcon.ThemeIcon.Printer), self.vmdev_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_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) def leddev_connect(self, leddev_type, leddev_name): 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 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) if device_dialog.exec() == QDialog.DialogCode.Accepted: leddev_type, leddev_name = device_dialog.get_selected() if self.verbose: print(f"Connecting to {leddev_type}:{leddev_name}") self.leddev_connect(leddev_type, leddev_name) def vmdev_connect(self, vmdev_type, vmdev_name): 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: print(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) if device_dialog.exec() == QDialog.DialogCode.Accepted: vmdev_type, vmdev_name = device_dialog.get_selected() if self.verbose: print(f"Connecting to {vmdev_type}:{vmdev_name}") self.vmdev_connect(vmdev_type, vmdev_name) 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("meas_devices") self.topbar.disable_button("meas_save") self.topbar.enable_button("meas_stop") self.w_plot.clear_data() script = 100 measurement_name = "guitest" 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) }) if self.verbose: print(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): 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("meas_devices") 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) 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 app_exit(self) -> None: """ Closes the application. """ self.close() def settings_window(self) -> None: """ Event handler for the "Settings" button. Displays the "Settings" window. """ def __del__(self): self.measure_stop()