From 8d6ae914dbb839834f9498f374ec60afa7a66c8d Mon Sep 17 00:00:00 2001
From: CPD <CPD@TUZEWSI-2LN203M.ads.mwn.de>
Date: Tue, 25 Feb 2025 12:12:42 +0100
Subject: [PATCH] Add device selection

---
 app/ui/main_window.py           | 161 ++++++++++++++++++++++++++++----
 app/ui/widgets/device_select.py |  27 ++++++
 2 files changed, 170 insertions(+), 18 deletions(-)
 create mode 100644 app/ui/widgets/device_select.py

diff --git a/app/ui/main_window.py b/app/ui/main_window.py
index 46cda62..55c14dd 100644
--- a/app/ui/main_window.py
+++ b/app/ui/main_window.py
@@ -1,14 +1,26 @@
 ''' app/ui/main_window.py '''
 from PyQt6.QtCore import Qt
-from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QTextEdit, QLabel
-from ..utils.config import AppConfig
+from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QTextEdit, QLabel, QDialog
+from PyQt6.QtGui import QIcon
 from .widgets.menubar import MenuBar
 from .widgets.toolbar import ToolBar
 from .widgets.statusbar import StatusBar
 from .widgets.metadata_input import MetadataInput
 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):
     """
@@ -50,6 +62,14 @@ class MainWindow(QMainWindow):
         layout.addWidget(MetadataInput(init_elements))
         layout.addWidget(Plot())
 
+        self.verbose = True
+
+        # devices
+        self.vmdev = None
+        self.leddev = None
+        self.vmdev_autoconnect()
+        self.leddev_autoconnect()
+
     def create_toolbars(self) -> None:
         """
         Creates and adds the top and right toolbars to the main window.
@@ -59,13 +79,11 @@ class MainWindow(QMainWindow):
                               style=Qt.ToolButtonStyle.ToolButtonTextUnderIcon, icon_size=(24, 24))
 
         # Top Toolbar Buttons
-        self.topbar.add_button(
-            "Open", "resources/assets/icons/windows/imageres-10.ico", self.open_file)
-        self.topbar.add_button(
-            "Save", "resources/assets/icons/windows/shell32-259.ico", self.save_file)
+        self.topbar.add_button("Devices", QIcon.fromTheme(QIcon.ThemeIcon.Printer), self.vmdev_connect_from_dialog)
+        self.topbar.add_button("Start", QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart), self.measure_start)
+        self.topbar.add_button("Save", "resources/assets/icons/windows/shell32-259.ico", self.save_file)
         self.topbar.add_separator()
-        self.topbar.add_button(
-            "Exit", "resources/assets/icons/windows/shell32-220.ico", self.exit_app)
+        self.topbar.add_button("Exit", "resources/assets/icons/windows/shell32-220.ico", self.exit_app)
 
         # Right Toolbar [PyQt6.QtWidgets.QToolBar]
         self.rightbar = ToolBar(self, orientation=Qt.Orientation.Vertical,
@@ -74,10 +92,7 @@ class MainWindow(QMainWindow):
 
         # Right Toolbar Buttons
         self.rightbar.add_separator()
-        self.rightbar.add_button(
-            "Privacy", "resources/assets/icons/windows/shell32-167.ico", self.privacy_window)
-        self.rightbar.add_button(
-            "Settings", "resources/assets/icons/windows/shell32-315.ico", self.settings_window)
+        self.rightbar.add_button("Settings", "resources/assets/icons/windows/shell32-315.ico", self.settings_window)
 
         self.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.topbar)
         self.addToolBar(Qt.ToolBarArea.RightToolBarArea, self.rightbar)
@@ -88,6 +103,122 @@ class MainWindow(QMainWindow):
         """
         # 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
+    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")
+        script = 100
+        measurement_name = "guitest"
+        led_script = LedScript(script=script)
+        flush_after = AppConfig.MAIN_CFG.get_or("flush_after", None)
+        use_buffer = AppConfig.MAIN_CFG.get_or("use_buffer", False)
+        max_measurements = AppConfig.MAIN_CFG.get_or("max_measurements", None)
+        stop_on_script_end = AppConfig.MAIN_CFG.get_or("stop_on_script_end", False)
+        interval = AppConfig.MAIN_CFG.get_or("measurement_interval_s", 1.0)
+
+        metadata = {}
+        metadata["interval"] = str(interval)
+        metadata["name"] = measurement_name
+        metadata["led"] = "led"
+        metadata["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}")
+
+        led_script = LedScript(script=script, auto_update=True, verbose=True)
+        data_collector = DataCollector(metadata=metadata, data_path=AppConfig.MAIN_CFG.get("datadir"), data_name=measurement_name)
+        # data_collector.clear()
+        data_queue = mp.Queue()
+        command_queue = mp.Queue()
+        # Argument order must match the definition
+
+        proc_measure = mt.Thread(target=measure, args=(
+            self.vmdev,
+            self.leddev,
+            led_script,
+            data_collector,
+            interval,
+            flush_after,
+            use_buffer,
+            max_measurements,
+            stop_on_script_end,
+            self.verbose,  # verbose
+            command_queue,
+            data_queue
+        ))
+        proc_measure.start()
+        try:
+            while proc_measure.is_alive():
+                while not data_queue.empty():
+                    # print(data_queue.qsize(), "\n\n")
+                    current_data = data_queue.get(block=False)
+                    i, tval, vval, led_val = current_data
+                    print(f"Data {i:03}: {tval}s, {vval}V, {led_val}%")
+
+        except KeyboardInterrupt:
+            pass
+        command_queue.put("stop")
+        proc_measure.join()
+        print("Measurement stopped" + " " * 50)
+        led_script.stop_updating()  # stop watching for file updates (if enabled)
+        data_collector.save_csv(verbose=True)
+        data, metadata = data_collector.get_data()
+
+
     def create_edit(self) -> QTextEdit:
         """
         Creates and adds the QTextEdit widget to the main window.
@@ -116,9 +247,3 @@ class MainWindow(QMainWindow):
         """
         Event handler for the "Settings" button. Displays the "Settings" window.
         """
-
-    def privacy_window(self) -> None:
-        """
-        Event handler for the "Privacy" button. Displays the "Privacy" window.
-        """
-        print("privacy_window")
diff --git a/app/ui/widgets/device_select.py b/app/ui/widgets/device_select.py
new file mode 100644
index 0000000..ef5c93d
--- /dev/null
+++ b/app/ui/widgets/device_select.py
@@ -0,0 +1,27 @@
+from PyQt6.QtWidgets import QWidget, QVBoxLayout, QListWidget, QListWidgetItem, QLabel, QDialogButtonBox, QDialog
+from typing import Callable
+
+# QT6 non-window that presents a list of devices of which one can be connected
+class ListChoice(QDialog):
+    def __init__(self, items: dict[str, list[str]], parent=None):
+        super().__init__(parent)
+        self.setWindowTitle("Select Device")
+        self.setLayout(QVBoxLayout())
+        self.layout().addWidget(QLabel("Select a device to connect to:"))
+        self.device_list = QListWidget()
+        self.layout().addWidget(self.device_list)
+        for key, items in items.items():
+            for item in items:
+                w = QListWidgetItem(f"{key}: {item}", parent=self.device_list)
+                w.key = key
+                w.item = item
+                # self.device_list.addItem(f"{key}: {item}")
+
+        self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
+        self.layout().addWidget(self.buttons)
+        self.buttons.accepted.connect(self.accept)
+        self.buttons.rejected.connect(self.reject)
+
+    def get_selected(self):
+        selected_item = self.device_list.currentItem()
+        return selected_item.key, selected_item.item
\ No newline at end of file