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,
+}