diff --git a/environment-designer.yml b/environment-designer.yml index def133f4..ab229e35 100644 --- a/environment-designer.yml +++ b/environment-designer.yml @@ -81,3 +81,4 @@ dependencies: # --- Extra Qt UI packages - qtawesome - qasync + - dask >= 2022 diff --git a/src/firefly/area_detector_overlay.ui b/src/firefly/area_detector_overlay.ui index c322d15a..bc9a68f0 100644 --- a/src/firefly/area_detector_overlay.ui +++ b/src/firefly/area_detector_overlay.ui @@ -6,8 +6,8 @@ 0 0 - 263 - 181 + 250 + 192 @@ -44,7 +44,7 @@ - haven://${AD}.overlays.overlay_${OV}.use + haven://${AD}-overlay-overlays-${OV}.use @@ -61,8 +61,28 @@ - haven://${AD}.overlays.overlay_${OV}.shape + haven://${AD}-overlay-overlays-${OV}.shape + + + CROSS + + + + + RECTANGLE + + + + + ELLIPSE + + + + + TEXT + + @@ -75,7 +95,7 @@ - haven://${AD}.overlays.overlay_${OV}.position_y + haven://${AD}-overlay-overlays-${OV}.center_y Qt::Vertical @@ -100,7 +120,7 @@ - haven://${AD}.overlays.overlay_${OV}.position_x + haven://${AD}-overlay-overlays-${OV}.center_x 0 @@ -132,7 +152,7 @@ - haven://${AD}.overlays.overlay_${OV}.size_y + haven://${AD}-overlay-overlays-${OV}.size_y Qt::Vertical @@ -157,7 +177,7 @@ - haven://${AD}.overlays.overlay_${OV}.size_x + haven://${AD}-overlay-overlays-${OV}.size_x @@ -178,6 +198,60 @@ + + + + + + + + + haven://${AD}-overlay-overlays-${OV}.width_y + + + Qt::Vertical + + + + + + + + + Width + + + Qt::AlignCenter + + + + + + + + + + haven://${AD}-overlay-overlays-${OV}.width_x + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -195,6 +269,7 @@ PyDMSlider QFrame
pydm.widgets.slider
+ 1
diff --git a/src/firefly/area_detector_roi.ui b/src/firefly/area_detector_roi.ui index 1f4f834a..de05f496 100644 --- a/src/firefly/area_detector_roi.ui +++ b/src/firefly/area_detector_roi.ui @@ -147,7 +147,16 @@ haven://${AD}.roi${R}.min_xyz.min_y - Qt::Vertical + 2 + + + 3000.000000000000000 + + + 0.000000000000000 + + + true @@ -174,6 +183,15 @@ 0 + + 3000.000000000000000 + + + 0.000000000000000 + + + true + @@ -204,7 +222,16 @@ haven://${AD}.roi${R}.size.y - Qt::Vertical + 2 + + + 3000.000000000000000 + + + 0.000000000000000 + + + true @@ -228,6 +255,15 @@ haven://${AD}.roi${R}.size.x + + 3000.000000000000000 + + + 0.000000000000000 + + + true + @@ -250,11 +286,6 @@ - - PyDMLabel - QLabel -
pydm.widgets.label
-
PyDMCheckbox QCheckBox @@ -263,7 +294,13 @@ PyDMSlider QFrame -
pydm.widgets.slider
+
pydm.widgets.slider
+ 1 +
+ + PyDMLabel + QLabel +
pydm.widgets.label
diff --git a/src/firefly/area_detector_viewer.py b/src/firefly/area_detector_viewer.py index 1eb3f186..99a25b78 100644 --- a/src/firefly/area_detector_viewer.py +++ b/src/firefly/area_detector_viewer.py @@ -4,6 +4,8 @@ import numpy as np import pydm import pyqtgraph +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QCheckBox, QPushButton, PyDMLineEdit from firefly import display from haven import beamline @@ -23,11 +25,35 @@ class AreaDetectorViewerDisplay(display.FireflyDisplay): ) image_is_new: bool = True + def __init__(self, parent=None, args=None, macros=None): + super().__init__(parent=parent, args=args, macros=macros) + + # Access Exposure widgets + self.exposure_checkbox = self.findChild(QCheckBox, "ExposureCheckBox") + self.exposure_push_button = self.findChild(QPushButton, "ExposurePushButton") + + # Connect Exposure signals to slots + self.exposure_checkbox.stateChanged.connect( + self.handle_exposure_checkbox_change + ) + self.exposure_push_button.clicked.connect( + self.handle_exposure_push_button_click + ) + + # Access Gain widgets + self.gain_line_edit = self.findChild(PyDMLineEdit, "GainLineEdit") + self.gain_checkbox = self.findChild(QCheckBox, "GainCheckBox") + self.gain_push_button = self.findChild(QPushButton, "GainPushButton") + + # Connect Gain signals to slots + self.gain_checkbox.stateChanged.connect(self.handle_gain_checkbox_change) + self.gain_push_button.clicked.connect(self.handle_gain_push_button_click) + def customize_device(self): device_name = name = self.macros()["AD"] device = beamline.devices[device_name] self.device = device - img_pv = device.pva.pv_name.get(as_string=True) + img_pv = device.pva._name #.get(as_string=True) addr = f"pva://{img_pv}" self.image_channel = pydm.PyDMChannel( address=addr, value_slot=self.update_image @@ -42,7 +68,8 @@ def customize_ui(self): # Set some text about the camera use_name = getattr(self.device, "description", None) in [self.device.name, None] if use_name: - lbl_text = self.device.cam.name + print(f"Available attributes in self.device:{dir(self.device.driver)}") + lbl_text = self.device.driver.name else: lbl_text = f"{self.device.description} ({self.device.cam.prefix})" self.ui.camera_description_label.setText(lbl_text) @@ -74,6 +101,54 @@ def launch_caqtdm(self): log.info(f"Launching caQtDM: {cmd}") self._open_caqtdm_subprocess(cmd) + @pyqtSlot(int) + def handle_exposure_checkbox_change(self, state): + """ + Handle the exposure checkbox state change. + Disable the push button when the checkbox is checked. + """ + if state == 2: # Checked + self.exposure_push_button.setEnabled(False) + else: # Checkbox is unchecked + self.exposure_push_button.setEnabled(True) + + @pyqtSlot() + def handle_exposure_push_button_click(self): + """ + Handle the exposure push button click. + Disable the checkbox when the button is clicked. + """ + self.exposure_checkbox.setChecked(False) + + @pyqtSlot(int) + def handle_gain_checkbox_change(self, state): + """ + Handle the gain checkbox state change. + Disable the push button when the checkbox is checked. + """ + if state == 2: # Checked + self.gain_push_button.setEnabled(False) + else: # Checkbox is unchecked + self.gain_push_button.setEnabled(True) + + @pyqtSlot() + def handle_gain_push_button_click(self): + """ + Handle the gain push button click. + Disable the checkbox when the button is clicked. + """ + self.gain_checkbox.setChecked(False) + + @pyqtSlot(bool) + def update_widget_states(self, connected): + """ + Enable or disable widgets based on PV connection state + """ + self.exposure_push_button.setEnabled(not connected) + self.exposure_check_box.setEnabled(not connected) + self.gain_line_edit.setEnabled(not connected) + self.gain_push_button.setEnabled(not connected) + self.gain_check_box.setEnabled(not connected) # ----------------------------------------------------------------------------- # :author: Mark Wolfman diff --git a/src/firefly/area_detector_viewer.ui b/src/firefly/area_detector_viewer.ui index 4dc59644..08b5b6ee 100644 --- a/src/firefly/area_detector_viewer.ui +++ b/src/firefly/area_detector_viewer.ui @@ -103,17 +103,146 @@ - - + + + + + + + + + Auto + + + haven://${AD}.driver.gain_auto + + + ONCE + + + + + + + Continuous + + + haven://${AD}.driver.gain_auto + + + CONTINUOUS + + + + + + + - Shape: + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + + + + + + + + + haven://${AD}.driver.acquire_time + + + + + + + s + + + Qt::AlignCenter + + + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + Auto + + + haven://${AD}.driver.acquire_time_auto + + + ONCE + + + + + + + Continuous + + + haven://${AD}.driver.acquire_time_auto + + + CONTINUOUS + + + + + + + + + + 0 + 0 + + + + + + + + + + haven://${AD}.driver.detector_state + + + + + + + State: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + 0 @@ -130,7 +259,7 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - haven://${AD}.cam.array_size.array_size_y + haven://${AD}.fileio.array_size0 @@ -153,7 +282,7 @@ 1224 - haven://${AD}.cam.array_size.array_size_x + haven://${AD}.fileio.array_size1 @@ -172,7 +301,17 @@ - + + + + Shape: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + Exposure: @@ -182,7 +321,17 @@ - + + + + Period: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + Gain: @@ -192,94 +341,49 @@ - - + + - - - false - + - - [] - - haven://${AD}.cam.gain - - - true - - - 0.000000000000000 - - - 20.000000000000000 - - - 101 + haven://${AD}.driver.gain - - - - + - Auto - - - haven://${AD}.cam.gain_auto + - - 1 + + Qt::AlignCenter - + - - - false - + - - [] - - haven://${AD}.cam.acquire_time - - - true - - - 0.000000000000000 - - - 0.200000000000000 + haven://${AD}.driver.acquire_period - - - - + - Auto - - - haven://${AD}.cam.acquire_time_auto + s - - 1 + + Qt::AlignCenter @@ -298,7 +402,7 @@ Start - [{"name": "enable_acquire_buttons", "property": "Enable", "initial_value": "False", "expression": "ch[0] == 0", "channels": [{"channel": "${PREFIX}cam1:Acquire", "trigger": true, "use_enum": false}]}] + [{"name": "enable_acquire_buttons", "property": "Enable", "initial_value": "", "expression": "ch[0] == 0", "channels": [{"channel": "${PREFIX}cam1:Acquire", "trigger": true, "use_enum": false}]}] false @@ -307,7 +411,7 @@ false - haven://${AD}.cam.acquire + haven://${AD}.driver.acquire false @@ -325,7 +429,7 @@ Are you sure you want to proceed? - 1 + ON None @@ -350,10 +454,10 @@ [{"name": "enable_acquire_buttons", "property": "Enable", "initial_value": "", "expression": "ch[0] == 1", "channels": [{"channel": "${PREFIX}cam1:Acquire", "trigger": true, "use_enum": false}]}] - haven://${AD}.cam.acquire + haven://${AD}.driver.acquire - 0 + OFF @@ -390,7 +494,7 @@ - 0 + 2 @@ -413,7 +517,7 @@ QFrame::NoFrame - AD=${AD},OV=6 + AD=${AD},OV=0 area_detector_overlay.ui @@ -436,7 +540,7 @@ QFrame::NoFrame - AD=${AD},OV=7 + AD=${AD},OV=1 area_detector_overlay.ui @@ -447,7 +551,7 @@ - Datetime + Cursor 3 @@ -459,7 +563,7 @@ QFrame::NoFrame - AD=${AD},OV=8 + AD=${AD},OV=2 area_detector_overlay.ui @@ -473,141 +577,6 @@ - - - - ROIs - - - - - - - - - - - - QTabWidget::South - - - QTabWidget::Rounded - - - 3 - - - false - - - - - - - - - - - - - - - - Full - - - - - - - - - QFrame::NoFrame - - - AD=${AD},R=4 - - - area_detector_roi.ui - - - - - - - - 1 - - - - - - - - - QFrame::NoFrame - - - AD=${AD},R=1 - - - area_detector_roi.ui - - - - - - - - 2 - - - - - - - - - QFrame::NoFrame - - - AD=${AD},R=2 - - - area_detector_roi.ui - - - - - - - - 3 - - - - - - - - - QFrame::NoFrame - - - AD=${AD},R=3 - - - area_detector_roi.ui - - - - - - - - - - @@ -651,18 +620,17 @@ QPushButton
pydm.widgets.pushbutton
- - PyDMSlider - QFrame -
pydm.widgets.slider
- 1 -
ImageView QWidget
pyqtgraph
1
+ + PyDMLineEdit + QLineEdit +
pydm.widgets.line_edit
+
diff --git a/src/firefly/pydm_plugin.py b/src/firefly/pydm_plugin.py index 3ac33ca7..b2216712 100644 --- a/src/firefly/pydm_plugin.py +++ b/src/firefly/pydm_plugin.py @@ -13,8 +13,9 @@ import numpy as np from bluesky.protocols import Movable, Subscribable -from ophyd import OphydObject +from ophyd import OphydObject from ophyd.utils.epics_pvs import AlarmSeverity +from ophyd_async.core._signal import SignalRW from pydm.data_plugins.plugin import PyDMConnection from qasync import asyncSlot from typhos.plugins.core import SignalConnection, SignalPlugin @@ -167,8 +168,11 @@ def connection_class(channel, address, protocol): sig = None is_ophyd_async = inspect.iscoroutinefunction(getattr(sig, "connect", None)) is_vanilla_ophyd = isinstance(sig, OphydObject) + is_signal_rw = isinstance(sig, SignalRW) # Get the right Connection class and build it - if is_ophyd_async: + if is_signal_rw: + return HavenAsyncConnection(channel, address, protocol) + elif is_ophyd_async: return HavenAsyncConnection(channel, address, protocol) elif is_vanilla_ophyd: return HavenConnection(channel, address, protocol) diff --git a/src/firefly/robot.ui b/src/firefly/robot.ui index b9c5c6a7..52389708 100644 --- a/src/firefly/robot.ui +++ b/src/firefly/robot.ui @@ -6,8 +6,8 @@ 0 0 - 408 - 270 + 410 + 359
@@ -15,173 +15,211 @@ - - - Sample Holder Numbers + + + + + Sample Holder Numbers + + + + + + + + + Current: + + + + + + + + + + 0 + + + false + + + true + + + false + + + true + + + + + + haven://${DEVICE}.current_sample + + + false + + + + + + + Qt::Horizontal + + + + 490 + 52 + + + + + + + + + 0 + 0 + + + + Sample holder + + + + + + + + + + Qt::Horizontal + + + + 490 + 52 + + + + + + + + + + + + Qt::Horizontal - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Current: - - - - - - - - - - 0 - - - false - - - true - - - false - - - true - - - - - - haven://${DEVICE}.current_sample - - - false - - - PyDMLabel::String - - - - - - - Qt::Horizontal - - - - 490 - 52 - - - - - - - - - 0 - 0 - - - - Sample holder - - - - - - - - - - Qt::Horizontal - - - - 490 - 52 - - - - - - - - - - Motors - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - Num. Motors - - - - - - - true - - - false - - - false - - - QAbstractSpinBox::CorrectToNearestValue - - - 0 - - - 0 - - - 10 + + + + + + + + 0 + 0 + + + + Num. Motors + + + + + + + true + + + false + + + false + + + QAbstractSpinBox::CorrectToNearestValue + + + 0 + + + 0 + + + 10 + + + + + + + Qt::Horizontal + + + + 490 + 52 + + + + + + + + + + + + true + + + + + 0 + 0 + 386 + 148 + + + + + + 3 + + + 3 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + - - - - - Qt::Horizontal - - - - 490 - 52 - - - - - - - - - - - + + + +
+
diff --git a/src/firefly/tests/test_area_detector_display.py b/src/firefly/tests/test_area_detector_display.py index 82fd5c0d..2610f7fd 100644 --- a/src/firefly/tests/test_area_detector_display.py +++ b/src/firefly/tests/test_area_detector_display.py @@ -4,6 +4,7 @@ import pydm import pyqtgraph import pytest +from PyQt5.QtCore import Qt from firefly.area_detector_viewer import AreaDetectorViewerDisplay @@ -48,6 +49,66 @@ def test_caqtdm_window(display, sim_camera): ) +def test_exposure_checkbox_push_button(display, qtbot): + """ + Test the interaction between the Exposure checkbox and push button. + """ + exposure_checkbox = display.exposure_checkbox + exposure_push_button = display.exposure_push_button + + # Initially both should be enabled + assert exposure_checkbox.isEnabled() + assert exposure_push_button.isEnabled() + + # Simulate checking the checkbox + exposure_checkbox.setChecked(True) + assert ( + not exposure_push_button.isEnabled() + ), "Exposure PushButton should be disabled when Checkbox is checked." + + # Simulate unchecking the checkbox + exposure_checkbox.setChecked(False) + assert ( + exposure_push_button.isEnabled() + ), "Exposure PushButton should be enabled when Checkbox is unchecked." + + # Simulate clicking the push button + qtbot.mouseClick(exposure_push_button, Qt.LeftButton) + assert ( + not exposure_checkbox.isChecked() + ), "Exposure Checkbox should be unchecked when PushButton is clicked." + + +def test_gain_checkbox_push_button(display, qtbot): + """ + Test the interaction between the Gain checkbox and push button. + """ + gain_checkbox = display.gain_checkbox + gain_push_button = display.gain_push_button + + # Initially both should be enabled + assert gain_checkbox.isEnabled() + assert gain_push_button.isEnabled() + + # Simulate checking the checkbox + gain_checkbox.setChecked(True) + assert ( + not gain_push_button.isEnabled() + ), "Gain PushButton should be disabled when Checkbox is checked." + + # Simulate unchecking the checkbox + gain_checkbox.setChecked(False) + assert ( + gain_push_button.isEnabled() + ), "Gain PushButton should be enabled when Checkbox is unchecked." + + # Simulate clicking the push button + qtbot.mouseClick(gain_push_button, Qt.LeftButton) + assert ( + not gain_checkbox.isChecked() + ), "Gain Checkbox should be unchecked when PushButton is clicked." + + # ----------------------------------------------------------------------------- # :author: Mark Wolfman # :email: wolfman@anl.gov diff --git a/src/haven/devices/area_detector.py b/src/haven/devices/area_detector.py index c009f165..0205a1f7 100644 --- a/src/haven/devices/area_detector.py +++ b/src/haven/devices/area_detector.py @@ -9,9 +9,7 @@ import pandas as pd from apstools.devices import CamMixin_V34, SingleTrigger_V34 from ophyd import ADComponent as ADCpt -from ophyd import ( - CamBase, -) +from ophyd import CamBase from ophyd import Component as Cpt from ophyd import DetectorBase as OphydDetectorBase from ophyd import ( @@ -43,10 +41,7 @@ ) from ophyd.areadetector.plugins import StatsPlugin_V31 as OphydStatsPlugin_V31 from ophyd.areadetector.plugins import StatsPlugin_V34 as OphydStatsPlugin_V34 -from ophyd.areadetector.plugins import ( - TIFFPlugin_V31, - TIFFPlugin_V34, -) +from ophyd.areadetector.plugins import TIFFPlugin_V31, TIFFPlugin_V34 from ophyd.flyers import FlyerInterface from ophyd.sim import make_fake_device from ophyd.status import Status, StatusBase, SubscriptionStatus diff --git a/src/haven/devices/detectors/aravis.py b/src/haven/devices/detectors/aravis.py index a8dcffa4..96f3db00 100644 --- a/src/haven/devices/detectors/aravis.py +++ b/src/haven/devices/detectors/aravis.py @@ -3,6 +3,9 @@ from ophyd_async.epics.core import epics_signal_rw_rbv from .area_detectors import default_path_provider +from .image_plugin import ImagePlugin +from .overlay import OverlayPlugin + class AravisTriggerSource(SubsetEnum): @@ -10,6 +13,12 @@ class AravisTriggerSource(SubsetEnum): LINE1 = "Line1" +class AutoMode(SubsetEnum): + OFF = "Off" + ONCE = "Once" + CONTINUOUS = "Continuous" + + class AravisDetector(AravisDetectorBase): _ophyd_labels_ = {"cameras", "detectors"} @@ -18,10 +27,25 @@ def __init__( ): if path_provider is None: path_provider = default_path_provider() + self.overlay = OverlayPlugin(f"{prefix}Over1:") + self.pva = ImagePlugin(f"{prefix}Pva1:", protocol="pva") super().__init__(*args, prefix=prefix, path_provider=path_provider, **kwargs) # Replace a signal that has different enum options self.driver.trigger_source = epics_signal_rw_rbv( AravisTriggerSource, # type: ignore f"{prefix}cam1:TriggerSource", ) + self.driver.gain = epics_signal_rw_rbv( + float, # type: ignore + f"{prefix}cam1:Gain", + ) + self.driver.gain_auto = epics_signal_rw_rbv( + AutoMode, # type: ignore + f"{prefix}cam1:GainAuto", + ) + self.driver.acquire_time_auto = epics_signal_rw_rbv( + AutoMode, # type: ignore + f"{prefix}cam1:ExposureAuto", + ) + self.set_name(self.name) diff --git a/src/haven/devices/detectors/eiger.py b/src/haven/devices/detectors/eiger.py index 8cbcad25..be68fc06 100644 --- a/src/haven/devices/detectors/eiger.py +++ b/src/haven/devices/detectors/eiger.py @@ -7,6 +7,8 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv from .area_detectors import default_path_provider +from .image_plugin import ImagePlugin +from .overlay import OverlayPlugin class EigerDriverIO(adcore.ADBaseIO): @@ -69,6 +71,9 @@ def __init__( ): if path_provider is None: path_provider = default_path_provider() + # Other (non-data) area detector plugins + self.overlay = OverlayPlugin(f"{prefix}Over1:") + self.pva = ImagePlugin(f"{prefix}Pva1:", protocol="pva") # Area detector IO devices driver = EigerDriverIO(f"{prefix}{drv_suffix}") controller = EigerController(driver) @@ -101,6 +106,9 @@ def __init__( def default_time_signal(self): return self.driver.acquire_time + @property + def default_period_signal(self): + return self.driver.period_time # ----------------------------------------------------------------------------- # :author: Mark Wolfman diff --git a/src/haven/devices/detectors/image_plugin.py b/src/haven/devices/detectors/image_plugin.py new file mode 100644 index 00000000..c0704298 --- /dev/null +++ b/src/haven/devices/detectors/image_plugin.py @@ -0,0 +1,9 @@ +import numpy as np +from ophyd_async.epics.adcore._core_io import NDPluginBaseIO +from ophyd_async.epics.core import epics_signal_r + + +class ImagePlugin(NDPluginBaseIO): + def __init__(self, prefix: str, name: str = "", protocol: str = "ca"): + self.image_array = epics_signal_r(np.ndarray, f"{protocol}://{prefix}ArrayData") + super().__init__(prefix=prefix, name=name) diff --git a/src/haven/devices/detectors/overlay.py b/src/haven/devices/detectors/overlay.py new file mode 100644 index 00000000..e0f0baae --- /dev/null +++ b/src/haven/devices/detectors/overlay.py @@ -0,0 +1,56 @@ +from ophyd_async.epics.adcore._core_io import NDPluginBaseIO +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv +from ophyd_async.core import DeviceVector, Device, StrictEnum + + +class Shape(StrictEnum): + CROSS = "Cross" + RECTANGLE = "Rectangle" + ELLIPSE = "Ellipse" + TEXT = "Text" + + +class DrawMode(StrictEnum): + SET = "Set" + XOR = "XOR" + + +class Font(StrictEnum): + SIX_BY_THIRTEEN = "6x13" + SIX_BY_THIRTEEN_BOLD = "6x13 Bold" + NINE_BY_FIFTEEN = "9x15" + NINE_BY_FIFTEEN_BOLD = "9x15 Bold" + + +class Overlay(Device): + def __init__(self, prefix: str, name: str = ""): + # Modal parameters + self.use = epics_signal_rw_rbv(bool, f"{prefix}Use") + self.description = epics_signal_rw_rbv(str, f"{prefix}Name") + self.shape = epics_signal_rw_rbv(Shape, f"{prefix}Shape") + self.draw_mode = epics_signal_rw_rbv(DrawMode, f"{prefix}DrawMode") + self.red = epics_signal_rw_rbv(int, f"{prefix}Red") + self.green = epics_signal_rw_rbv(int, f"{prefix}Green") + self.blue = epics_signal_rw_rbv(int, f"{prefix}Blue") + self.display_text = epics_signal_rw_rbv(str, f"{prefix}DisplayText") + self.time_format = epics_signal_rw_rbv(str, f"{prefix}TimeStampFormat") + self.font = epics_signal_rw_rbv(Font, f"{prefix}Font") + # Overlay parameters + self.position_x = epics_signal_rw_rbv(int, f"{prefix}PositionX") + self.center_x = epics_signal_rw_rbv(int, f"{prefix}CenterX") + self.size_x = epics_signal_rw_rbv(int, f"{prefix}SizeX") + self.width_x = epics_signal_rw_rbv(int, f"{prefix}WidthX") + self.position_y = epics_signal_rw_rbv(int, f"{prefix}PositionY") + self.center_y = epics_signal_rw_rbv(int, f"{prefix}CenterY") + self.size_y = epics_signal_rw_rbv(int, f"{prefix}SizeY") + self.width_y = epics_signal_rw_rbv(int, f"{prefix}WidthY") + super().__init__(name=name) + + +class OverlayPlugin(NDPluginBaseIO): + def __init__(self, prefix: str, name: str = "", *args, **kwargs): + overlays = 7 + self.overlays = DeviceVector({ + idx: Overlay(f"{prefix}{idx+1}:") for idx in range(overlays) + }) + super().__init__(prefix=prefix, name=name, *args, **kwargs) diff --git a/src/haven/devices/detectors/sim_detector.py b/src/haven/devices/detectors/sim_detector.py index 621b8bbb..3b44dcd8 100644 --- a/src/haven/devices/detectors/sim_detector.py +++ b/src/haven/devices/detectors/sim_detector.py @@ -2,12 +2,16 @@ from ophyd_async.epics.adsimdetector import SimDetector as SimDetectorBase from .area_detectors import default_path_provider +from .image_plugin import ImagePlugin +from .overlay import OverlayPlugin class SimDetector(SimDetectorBase): _ophyd_labels_ = {"area_detectors", "detectors"} def __init__(self, prefix, path_provider: PathProvider | None = None, **kwargs): + self.overlay = OverlayPlugin(f"{prefix}Over1:") + self.pva = ImagePlugin(f"{prefix}Pva1:", protocol="pva") if path_provider is None: path_provider = default_path_provider() super().__init__(prefix=prefix, path_provider=path_provider, **kwargs) diff --git a/src/haven/tests/test_aravis.py b/src/haven/tests/test_aravis.py index e9bb5385..1203b91d 100644 --- a/src/haven/tests/test_aravis.py +++ b/src/haven/tests/test_aravis.py @@ -18,7 +18,7 @@ async def camera(sim_registry): async def test_camera_trigger_source_choices(camera): """Confirm the camera device has the right signals. - Solves this inital error received when using detaul AravisDetector. + Solves this inital error received when using upstream AravisDetector. > drv: NotConnected: > trigger_source: TypeError: 25idcARV4:cam1:TriggerSource_RBV has choices ('Software', 'Line1', 'Line3', 'Action1'), which is not a superset of SubsetEnum['Freerun', 'Line1']. @@ -53,6 +53,11 @@ async def test_camera_signals(camera): desc = await camera.fileio.data_type.describe() hdf_source = desc["s255id-gige-A-fileio-data_type"]["source"] assert hdf_source == "mock+ca://255idgigeA:HDF1:DataType_RBV" + # Check a few additional signals needed for live viewing + assert camera.driver.gain.source == "mock+ca://255idgigeA:cam1:Gain_RBV" + assert camera.driver.gain_auto.source == "mock+ca://255idgigeA:cam1:GainAuto_RBV" + assert camera.driver.acquire_time_auto.source == "mock+ca://255idgigeA:cam1:ExposureAuto_RBV" + # ----------------------------------------------------------------------------- diff --git a/src/haven/tests/test_overlay.py b/src/haven/tests/test_overlay.py new file mode 100644 index 00000000..00ba079a --- /dev/null +++ b/src/haven/tests/test_overlay.py @@ -0,0 +1,31 @@ +import pytest + +from haven.devices.detectors.overlay import OverlayPlugin + + +@pytest.fixture() +async def overlay(): + ovrly = OverlayPlugin(name="overlay", prefix="255idSimDet:Over1:") + await ovrly.connect(mock=True) + return ovrly + + +def test_signals(overlay): + assert overlay.overlays[0].use.source == "mock+ca://255idSimDet:Over1:1:Use_RBV" + assert overlay.overlays[0].description.source == "mock+ca://255idSimDet:Over1:1:Name_RBV" + assert overlay.overlays[0].shape.source == "mock+ca://255idSimDet:Over1:1:Shape_RBV" + assert overlay.overlays[0].draw_mode.source == "mock+ca://255idSimDet:Over1:1:DrawMode_RBV" + assert overlay.overlays[0].red.source == "mock+ca://255idSimDet:Over1:1:Red_RBV" + assert overlay.overlays[0].green.source == "mock+ca://255idSimDet:Over1:1:Green_RBV" + assert overlay.overlays[0].blue.source == "mock+ca://255idSimDet:Over1:1:Blue_RBV" + assert overlay.overlays[0].display_text.source == "mock+ca://255idSimDet:Over1:1:DisplayText_RBV" + assert overlay.overlays[0].time_format.source == "mock+ca://255idSimDet:Over1:1:TimeStampFormat_RBV" + assert overlay.overlays[0].font.source == "mock+ca://255idSimDet:Over1:1:Font_RBV" + assert overlay.overlays[0].position_x.source == "mock+ca://255idSimDet:Over1:1:PositionX_RBV" + assert overlay.overlays[0].center_x.source == "mock+ca://255idSimDet:Over1:1:CenterX_RBV" + assert overlay.overlays[0].size_x.source == "mock+ca://255idSimDet:Over1:1:SizeX_RBV" + assert overlay.overlays[0].width_x.source == "mock+ca://255idSimDet:Over1:1:WidthX_RBV" + assert overlay.overlays[0].position_y.source == "mock+ca://255idSimDet:Over1:1:PositionY_RBV" + assert overlay.overlays[0].center_y.source == "mock+ca://255idSimDet:Over1:1:CenterY_RBV" + assert overlay.overlays[0].size_y.source == "mock+ca://255idSimDet:Over1:1:SizeY_RBV" + assert overlay.overlays[0].width_y.source == "mock+ca://255idSimDet:Over1:1:WidthY_RBV" diff --git a/src/haven/tests/test_sim_detector.py b/src/haven/tests/test_sim_detector.py new file mode 100644 index 00000000..1af84e9c --- /dev/null +++ b/src/haven/tests/test_sim_detector.py @@ -0,0 +1,37 @@ +import pytest + +from haven.devices import SimDetector + + +@pytest.fixture() +async def detector(): + det = SimDetector(name="sim_detector", prefix="255idSimDet:") + await det.connect(mock=True) + return det + + +def test_signals(detector): + assert ( + detector.driver.detector_state.source + == "mock+ca://255idSimDet:cam1:DetectorState_RBV" + ) + assert ( + detector.driver.acquire_time.source + == "mock+ca://255idSimDet:cam1:AcquireTime_RBV" + ) + assert detector.driver.acquire.source == "mock+ca://255idSimDet:cam1:Acquire_RBV" + # assert detector.driver.acquire_time_auto.source == "mock+ca://255idSimDet:cam1:AcquireTimeAuto" + assert ( + detector.fileio.array_size0.source + == "mock+ca://255idSimDet:HDF1:ArraySize0_RBV" + ) + assert ( + detector.fileio.array_size1.source + == "mock+ca://255idSimDet:HDF1:ArraySize1_RBV" + ) + # Plugins + assert ( + detector.overlay.overlays[0].use.source + == "mock+ca://255idSimDet:Over1:1:Use_RBV" + ) + assert detector.pva.image_array.source == "mock+pva://255idSimDet:Pva1:ArrayData" diff --git a/src/haven/tests/test_xspress.py b/src/haven/tests/test_xspress.py index c9abe61f..baa27bb8 100644 --- a/src/haven/tests/test_xspress.py +++ b/src/haven/tests/test_xspress.py @@ -106,6 +106,8 @@ async def test_ndattribute_params(): async def test_stage_ndattributes(detector): num_elem = 8 + set_mock_value(detector.driver.number_of_elements, num_elem) + set_mock_value(detector.driver.nd_attributes_file, "XSP3.xml") num_params = 1 set_mock_value(detector.driver.number_of_elements, num_elem) set_mock_value(detector.driver.nd_attributes_file, "XSP3.xml")