From b0c647ec2fbc187f12008e211d5e2342159cf462 Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Thu, 9 Jan 2025 17:54:25 -0800 Subject: [PATCH 1/2] WIP: start implementing model specific screens --- camviewer.ui | 9 ++- models.py | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 models.py diff --git a/camviewer.ui b/camviewer.ui index 1456484..6790f30 100644 --- a/camviewer.ui +++ b/camviewer.ui @@ -7,7 +7,7 @@ 0 0 1106 - 1072 + 1129 @@ -774,6 +774,13 @@ Vmin\ + + + + Camera Controls + + + diff --git a/models.py b/models.py new file mode 100644 index 0000000..6d44977 --- /dev/null +++ b/models.py @@ -0,0 +1,171 @@ +""" +Brief model-specific screens to display below the main controls. + +This is to be used when there are important model-specific controls +that are vital to the operation of the camera. + +These are limited to simple form layouts that will be included +inside of a stock QGroupBox in the main screen. +""" +from __future__ import annotations + +from functools import partial + +from psp.Pv import Pv +from PyQt5.QtCore import QObject, pyqtSignal +from PyQt5.QtWidgets import QFormLayout, QLabel, QPushButton, QHBoxLayout, QSpinBox + + +# groupBoxControls + + +class ModelScreenGenerator(QObject): + """ + This class creates a cam-specific QFormLayout to include in the main screen. + + The layout will include basic start/stop camera controls for all models + and additional special controls that have been requested for specific models. + """ + + # GUI needs this to update the QGroupBox with the full model name + final_name = pyqtSignal(str) + manuf_ready = pyqtSignal() + model_ready = pyqtSignal() + + def __init__(self, base_pv: str): + self.form = QFormLayout() + self.base_pv = base_pv + self.manufacturer = "" + self.model = "" + self.pvs: list[Pv] = [] + + # Put in the form elements that always should be there + self.acq_label = QLabel("Disconnected") + start_button = QPushButton("Start") + stop_button = QPushButton("Stop") + acq_layout = QHBoxLayout() + acq_layout.addWidget(self.acq_label) + acq_layout.addWidget(start_button) + acq_layout.addWidget(stop_button) + self.form.addRow("Acquire", acq_layout) + + # If we don't have PVs, stop here. + # This is used to display something before we select a camera. + if not base_pv: + self.final_name.emit("Disconnected") + return + + # If we have PVs, we can make the widgets work properly + self.manuf_ready.connect(self.finish_form) + self.model_ready.connect(self.finish_form) + self.acq_status_pv = Pv( + f"{base_pv}:Acquire_RBV", + monitor=self.new_acq_value, + initialize=True, + ) + self.pvs.append(self.acq_status_pv) + self.acq_set_pv = Pv(f"{base_pv}:Acquire") + self.acq_set_pv.connect() + self.pvs.append(self.acq_set_pv) + start_button.clicked.connect(partial(self.set_acq_value, 1)) + stop_button.clicked.connect(partial(self.set_acq_value, 0)) + + # Create a callback to finish the form later, given the model + self.manuf_pv = Pv(f"{base_pv}:Manufacturer_RBV") + self.pvs.append(self.manuf_pv) + self.manuf_cid = self.manuf_pv.add_connection_callback(self.manuf_ready) + self.manuf_pv.connect() + self.model_pv = Pv(f"{base_pv}:Model_RBV") + self.pvs.append(self.model_pv) + self.model_cid = self.model_pv.add_connection_callback(self.model_ready) + self.model_pv.connect() + + def get_layout(self) -> QFormLayout: + return self.form + + def manuf_read(self, is_connected: bool) -> None: + if is_connected: + self.manuf_pv.del_connection_callback(self.manuf_cid) + self.manufacturer = self.manuf_pv.value + self.manuf_pv.disconnect() + self.manuf_ready.emit() + + def model_read(self, is_connected: bool) -> None: + if is_connected: + self.model_pv.del_connection_callback(self.model_cid) + self.model = self.model_pv.value + self.model_pv.disconnect() + self.model_ready.emit() + + def finish_form(self) -> QFormLayout: + if not self.manufacturer or not self.model: + return + full_name = f"{self.manufacturer} {self.model}" + self.final_name.emit(full_name) + try: + finisher = form_finishers[full_name] + except KeyError: + print(f"Using basic controls for {full_name}") + return + else: + print(f"Loading special screen for {full_name}") + self.pvs.extend(finisher(self.form, self.base_pv)) + + def new_acq_value(self, error: Exception | None) -> None: + if error is None: + self.acq_label.setText(self.acq_status_pv.value) + + def set_acq_value(self, value: int) -> None: + try: + self.acq_set_pv.put(value) + except Exception: + ... + + def cleanup(self) -> None: + for pv in self.pvs: + pv.disconnect() + + +def em_gain_andor(form: QFormLayout, base_pv: str) -> list[Pv]: + """ + Update the basic form layout to include the andor em gain. + Return the list of Pvs so we can clean up later. + """ + pvs = [] + + gain_label = QLabel() + + def update_gain_label(error: Exception | None): + if error is None: + gain_label.setText(str(gain_rbv_pv.value)) + + gain_rbv_pv = Pv( + f"{base_pv}:AndorEMGain_RBV", + monitor=update_gain_label, + initialize=True, + ) + pvs.append(gain_rbv_pv) + + gain_set_pv = Pv(f"{base_pv}:AndorEMGain") + pvs.append(gain_set_pv) + gain_set_pv.connect() + + def set_gain_value(value: int): + try: + gain_set_pv.put(value) + except Exception: + ... + + gain_spinbox = QSpinBox() + gain_spinbox.valueChanged.connect(set_gain_value) + + gain_layout = QHBoxLayout() + gain_layout.addWidget(gain_label) + gain_layout.addWidget(gain_spinbox) + form.addRow("EM Gain", gain_layout) + return pvs + + +form_finishers = { + "Andor DU888_BV": em_gain_andor, +} From 695f30b503e3b3255cbb8638f718fd8fcdc0d08e Mon Sep 17 00:00:00 2001 From: Zachary Lentz Date: Fri, 10 Jan 2025 15:33:08 -0800 Subject: [PATCH 2/2] ENH: more or less get the model-specific controls to work --- camviewer_ui_impl.py | 22 +++++++++ models.py | 105 ++++++++++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 36 deletions(-) diff --git a/camviewer_ui_impl.py b/camviewer_ui_impl.py index 4c22538..c9b71cb 100644 --- a/camviewer_ui_impl.py +++ b/camviewer_ui_impl.py @@ -15,6 +15,7 @@ from dialogs import markerdialog from dialogs import specificdialog from dialogs import forcedialog +from models import ModelScreenGenerator import sys import os @@ -42,6 +43,7 @@ QAction, QDialogButtonBox, QApplication, + QFormLayout, ) from PyQt5.QtGui import ( QClipboard, @@ -246,6 +248,7 @@ def __init__( self.average = 1 param.orientation = param.ORIENT0 self.connected = False + self.ctrlBase = "" self.cameraBase = "" self.camera = None self.notify = None @@ -519,6 +522,9 @@ def __init__( self.refresh_timeout_display_timer.setInterval(1000 * 20) self.refresh_timeout_display_timer.start() + self.model_screen_generator = None + self.setup_model_specific() + self.ui.average.returnPressed.connect(self.onAverageSet) self.ui.comboBoxOrientation.currentIndexChanged.connect( self.onOrientationSelect @@ -2005,6 +2011,7 @@ def connectCamera(self, sCameraPv, index, sNotifyPv=None): self.colPv.monitor(pyca.DBE_VALUE) pyca.flush_io() # Deliberately after flush_io so we don't wait for them + self.setup_model_specific() self.launch_gui_pv = Pv( self.ctrlBase + ":LAUNCH_GUI", initialize=True, @@ -2026,6 +2033,21 @@ def connectCamera(self, sCameraPv, index, sNotifyPv=None): # Get camera configuration self.getConfig() + def setup_model_specific(self): + if self.model_screen_generator is None: + form = QFormLayout() + self.ui.groupBoxControls.setLayout(form) + else: + self.model_screen_generator.cleanup() + form = self.ui.groupBoxControls.layout() + self.model_screen_generator = ModelScreenGenerator(self.ctrlBase, form) + self.model_screen_generator.final_name.connect(self.new_model_name) + if self.model_screen_generator.full_name: + self.new_model_name(self.model_screen_generator.full_name) + + def new_model_name(self, name: str): + self.ui.groupBoxControls.setTitle(f"{name} Controls") + def normalize_selectors(self): """ Update the visual appearance of the camera combobox and the menu to be correct. diff --git a/models.py b/models.py index 6d44977..a748398 100644 --- a/models.py +++ b/models.py @@ -10,13 +10,15 @@ from __future__ import annotations from functools import partial +from threading import Lock from psp.Pv import Pv from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import QFormLayout, QLabel, QPushButton, QHBoxLayout, QSpinBox -# groupBoxControls +STOP_TEXT = "Stopped" +START_TEXT = "Started" class ModelScreenGenerator(QObject): @@ -32,27 +34,35 @@ class ModelScreenGenerator(QObject): manuf_ready = pyqtSignal() model_ready = pyqtSignal() - def __init__(self, base_pv: str): - self.form = QFormLayout() + def __init__(self, base_pv: str, form: QFormLayout, parent: QObject | None = None): + super().__init__(parent=parent) + self.form = form self.base_pv = base_pv self.manufacturer = "" self.model = "" - self.pvs: list[Pv] = [] + self.full_name = "" + self.pvs_to_clean_up: list[Pv] = [] + self.sigs_to_clean_up: list[pyqtSignal] = [ + self.final_name, + self.manuf_ready, + self.model_ready, + ] + self.finish_ran = False + self.finish_lock = Lock() # Put in the form elements that always should be there - self.acq_label = QLabel("Disconnected") + self.acq_label = QLabel(STOP_TEXT) + self.acq_label.setMinimumWidth(80) start_button = QPushButton("Start") stop_button = QPushButton("Stop") acq_layout = QHBoxLayout() - acq_layout.addWidget(self.acq_label) acq_layout.addWidget(start_button) acq_layout.addWidget(stop_button) - self.form.addRow("Acquire", acq_layout) + self.form.addRow(self.acq_label, acq_layout) # If we don't have PVs, stop here. - # This is used to display something before we select a camera. if not base_pv: - self.final_name.emit("Disconnected") + self.full_name = "Generic" return # If we have PVs, we can make the widgets work properly @@ -63,57 +73,71 @@ def __init__(self, base_pv: str): monitor=self.new_acq_value, initialize=True, ) - self.pvs.append(self.acq_status_pv) + self.pvs_to_clean_up.append(self.acq_status_pv) self.acq_set_pv = Pv(f"{base_pv}:Acquire") self.acq_set_pv.connect() - self.pvs.append(self.acq_set_pv) + self.pvs_to_clean_up.append(self.acq_set_pv) start_button.clicked.connect(partial(self.set_acq_value, 1)) stop_button.clicked.connect(partial(self.set_acq_value, 0)) # Create a callback to finish the form later, given the model - self.manuf_pv = Pv(f"{base_pv}:Manufacturer_RBV") - self.pvs.append(self.manuf_pv) - self.manuf_cid = self.manuf_pv.add_connection_callback(self.manuf_ready) - self.manuf_pv.connect() - self.model_pv = Pv(f"{base_pv}:Model_RBV") - self.pvs.append(self.model_pv) - self.model_cid = self.model_pv.add_connection_callback(self.model_ready) - self.model_pv.connect() + self.manuf_pv = Pv( + f"{base_pv}:Manufacturer_RBV", + monitor=self.manuf_monitor, + initialize=True, + ) + self.pvs_to_clean_up.append(self.manuf_pv) + self.model_pv = Pv( + f"{base_pv}:Model_RBV", + monitor=self.model_monitor, + initialize=True, + ) + self.pvs_to_clean_up.append(self.model_pv) def get_layout(self) -> QFormLayout: return self.form - def manuf_read(self, is_connected: bool) -> None: - if is_connected: - self.manuf_pv.del_connection_callback(self.manuf_cid) + def manuf_monitor(self, error: Exception | None) -> None: + if error is None: self.manufacturer = self.manuf_pv.value - self.manuf_pv.disconnect() self.manuf_ready.emit() - def model_read(self, is_connected: bool) -> None: - if is_connected: - self.model_pv.del_connection_callback(self.model_cid) + def model_monitor(self, error: Exception | None) -> None: + if error is None: self.model = self.model_pv.value - self.model_pv.disconnect() self.model_ready.emit() def finish_form(self) -> QFormLayout: if not self.manufacturer or not self.model: return - full_name = f"{self.manufacturer} {self.model}" - self.final_name.emit(full_name) + with self.finish_lock: + if self.finish_ran: + return + self.finish_ran = True + self.manuf_pv.disconnect() + self.model_pv.disconnect() + self.pvs_to_clean_up.remove(self.manuf_pv) + self.pvs_to_clean_up.remove(self.model_pv) + self.full_name = f"{self.manufacturer} {self.model}" + self.final_name.emit(self.full_name) try: - finisher = form_finishers[full_name] + finisher = form_finishers[self.full_name] except KeyError: - print(f"Using basic controls for {full_name}") + print(f"Using basic controls for {self.full_name}") return else: - print(f"Loading special screen for {full_name}") - self.pvs.extend(finisher(self.form, self.base_pv)) + print(f"Loading special screen for {self.full_name}") + finisher_pvs, finisher_sigs = finisher(self.form, self.base_pv) + self.pvs_to_clean_up.extend(finisher_pvs) + self.sigs_to_clean_up.extend(finisher_sigs) def new_acq_value(self, error: Exception | None) -> None: if error is None: - self.acq_label.setText(self.acq_status_pv.value) + if self.acq_status_pv.value: + text = START_TEXT + else: + text = STOP_TEXT + self.acq_label.setText(text) def set_acq_value(self, value: int) -> None: try: @@ -122,8 +146,15 @@ def set_acq_value(self, value: int) -> None: ... def cleanup(self) -> None: - for pv in self.pvs: + for pv in self.pvs_to_clean_up: pv.disconnect() + for sig in self.sigs_to_clean_up: + try: + sig.disconnect() + except TypeError: + ... + for _ in range(self.form.rowCount()): + self.form.removeRow(0) def em_gain_andor(form: QFormLayout, base_pv: str) -> list[Pv]: @@ -132,6 +163,7 @@ def em_gain_andor(form: QFormLayout, base_pv: str) -> list[Pv]: Return the list of Pvs so we can clean up later. """ pvs = [] + sigs = [] gain_label = QLabel() @@ -158,12 +190,13 @@ def set_gain_value(value: int): gain_spinbox = QSpinBox() gain_spinbox.valueChanged.connect(set_gain_value) + sigs.append(gain_spinbox.valueChanged) gain_layout = QHBoxLayout() gain_layout.addWidget(gain_label) gain_layout.addWidget(gain_spinbox) form.addRow("EM Gain", gain_layout) - return pvs + return pvs, sigs form_finishers = {