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/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 new file mode 100644 index 0000000..a748398 --- /dev/null +++ b/models.py @@ -0,0 +1,204 @@ +""" +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 threading import Lock + +from psp.Pv import Pv +from PyQt5.QtCore import QObject, pyqtSignal +from PyQt5.QtWidgets import QFormLayout, QLabel, QPushButton, QHBoxLayout, QSpinBox + + +STOP_TEXT = "Stopped" +START_TEXT = "Started" + + +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, form: QFormLayout, parent: QObject | None = None): + super().__init__(parent=parent) + self.form = form + self.base_pv = base_pv + self.manufacturer = "" + self.model = "" + 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(STOP_TEXT) + self.acq_label.setMinimumWidth(80) + start_button = QPushButton("Start") + stop_button = QPushButton("Stop") + acq_layout = QHBoxLayout() + acq_layout.addWidget(start_button) + acq_layout.addWidget(stop_button) + self.form.addRow(self.acq_label, acq_layout) + + # If we don't have PVs, stop here. + if not base_pv: + self.full_name = "Generic" + 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_to_clean_up.append(self.acq_status_pv) + self.acq_set_pv = Pv(f"{base_pv}:Acquire") + self.acq_set_pv.connect() + 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", + 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_monitor(self, error: Exception | None) -> None: + if error is None: + self.manufacturer = self.manuf_pv.value + self.manuf_ready.emit() + + def model_monitor(self, error: Exception | None) -> None: + if error is None: + self.model = self.model_pv.value + self.model_ready.emit() + + def finish_form(self) -> QFormLayout: + if not self.manufacturer or not self.model: + return + 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[self.full_name] + except KeyError: + print(f"Using basic controls for {self.full_name}") + return + else: + 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: + 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: + self.acq_set_pv.put(value) + except Exception: + ... + + def cleanup(self) -> None: + 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]: + """ + Update the basic form layout to include the andor em gain. + Return the list of Pvs so we can clean up later. + """ + pvs = [] + sigs = [] + + 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) + 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, sigs + + +form_finishers = { + "Andor DU888_BV": em_gain_andor, +}