cpdctrl-gui/app/ui/main_window.py
2025-03-04 18:12:59 +01:00

290 lines
11 KiB
Python

''' app/ui/main_window.py '''
from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QTextEdit, QLabel, QDialog, QToolButton, QStatusBar
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.save_file)
self.topbar.add_separator()
self.topbar.add_button("app_exit", "Exit", QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit), self.exit_app)
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.enable_button("meas_stop")
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 open_file(self) -> None:
"""
Event handler for the "Open" button. Displays the "Open File" dialog.
"""
print("Open")
def save_file(self) -> None:
"""
Event handler for the "Save" button. Displays the "Save File" dialog.
"""
print("Save")
def exit_app(self) -> None:
"""
Event handler for the "Exit" button. 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()