332 lines
14 KiB
Python
332 lines
14 KiB
Python
''' 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
|
|
from PyQt6.QtGui import QIcon, QPixmap
|
|
from PyQt6.QtWidgets import QDialog, QDialogButtonBox
|
|
|
|
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
|
|
from .widgets.plot import Plot
|
|
from .widgets.device_select import ListChoice
|
|
from .widgets.about import MarkdownView
|
|
# 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)
|
|
|
|
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", {})
|
|
print(metadata_init_dict)
|
|
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: Plot
|
|
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("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):
|
|
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, window_title="Select LED device")
|
|
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)
|
|
|
|
# VOLTAGE DEVICE MANAGEMENT
|
|
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, window_title="Select CPD Measurement Device")
|
|
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)
|
|
|
|
# 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)
|
|
})
|
|
|
|
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("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 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/logo.svg")
|
|
pixmap = QPixmap(img_path)
|
|
pixmap = pixmap.scaled(128, 128, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
label = QLabel()
|
|
label.setPixmap(pixmap)
|
|
label.setAlignment(Qt.AlignmentFlag.AlignCenter) # center the image
|
|
dialog.layout().addWidget(label)
|
|
# show about.md
|
|
dialog.layout().addWidget(MarkdownView("about.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()
|