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
+ 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
-
-
PyDMCheckbox
QCheckBox
@@ -263,7 +294,13 @@
PyDMSlider
QFrame
-
+
+ 1
+
+
+ PyDMLabel
+ QLabel
+
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
-
- PyDMSlider
- QFrame
-
- 1
-
ImageView
QWidget
1
+
+ PyDMLineEdit
+ QLineEdit
+
+
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")