Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: cam-specific controls #28

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion camviewer.ui
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1106</width>
<height>1072</height>
<height>1129</height>
</rect>
</property>
<property name="sizePolicy">
Expand Down Expand Up @@ -774,6 +774,13 @@ Vmin\</string>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxControls">
<property name="title">
<string>Camera Controls</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxColor">
<property name="sizePolicy">
Expand Down
22 changes: 22 additions & 0 deletions camviewer_ui_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dialogs import markerdialog
from dialogs import specificdialog
from dialogs import forcedialog
from models import ModelScreenGenerator

import sys
import os
Expand Down Expand Up @@ -42,6 +43,7 @@
QAction,
QDialogButtonBox,
QApplication,
QFormLayout,
)
from PyQt5.QtGui import (
QClipboard,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand Down
204 changes: 204 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
@@ -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,
}