16 Commits

Author SHA1 Message Date
x01da
632d554245 add additional signals to nidaq
All checks were successful
CI for debye_bec / test (push) Successful in 1m3s
CI for debye_bec / test (pull_request) Successful in 1m6s
2026-03-25 09:48:25 +01:00
x01da
efd8842540 do not close pilatus curtain after measurements
All checks were successful
CI for debye_bec / test (push) Successful in 1m1s
CI for debye_bec / test (pull_request) Successful in 1m3s
2026-03-02 13:28:24 +01:00
x01da
fd1626fbcd nidaq improvement on_stage 2026-03-02 13:27:51 +01:00
x01da
062df3171b add additional signals for xrd tigger information 2026-03-02 13:27:51 +01:00
x01da
37a268fe7b add additional CI channels for NIDAQ. Add enable PV for dead time correction 2026-03-02 13:27:51 +01:00
x01da
e9e7d84e60 reworked function to get rid of (potentionally infinite) loop. 2026-03-02 13:27:51 +01:00
x01da
1c0c9ad53e add opencv dependency for hutch cameras 2026-03-02 13:27:51 +01:00
x01da
faeb991b75 create hutch camera class 2026-03-02 13:27:51 +01:00
x01da
7377613213 add config signals 2026-03-02 13:27:51 +01:00
x01da
d8383d3b73 add string representation of signals. add Pips class/device 2026-03-02 13:27:51 +01:00
x01da
c3bfab2056 add string representation of signals 2026-03-02 13:27:51 +01:00
x01da
0261c601ff uncomment Pilatus-Sample distance 2026-03-02 13:27:51 +01:00
x01da
d3dc130f11 uncomment ionization chamber and add pips diode 2026-03-02 13:27:51 +01:00
x01da
ed1e5a027f add hutch cameras to config 2026-03-02 13:27:51 +01:00
x01da
df2961ce8e add hutch cameras config 2026-03-02 13:27:51 +01:00
x01da
e179fc1a07 add gas sensors to config 2026-03-02 13:27:51 +01:00
14 changed files with 591 additions and 126 deletions

View File

@@ -386,4 +386,64 @@ es_light_toggle:
read_pv: "X01DA-EH-LIGHT:TOGGLE" read_pv: "X01DA-EH-LIGHT:TOGGLE"
onFailure: retry onFailure: retry
enabled: true enabled: true
softwareTrigger: false
es_gas_sensor_o2:
readoutPriority: baseline
description: ES Gas Sensor O2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-O2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_h2s:
readoutPriority: baseline
description: ES Gas Sensor H2S
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-H2S"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_no2:
readoutPriority: baseline
description: ES Gas Sensor NO2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-NO2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_co:
readoutPriority: baseline
description: ES Gas Sensor CO
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-CO"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_h2:
readoutPriority: baseline
description: ES Gas Sensor H2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-H2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_nh3:
readoutPriority: baseline
description: ES Gas Sensor NH3
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-NH3"
onFailure: retry
enabled: true
softwareTrigger: false softwareTrigger: false

View File

@@ -0,0 +1,34 @@
###################################
## Hutch Cameras ##
###################################
hutch_cam_1:
readoutPriority: baseline
description: Hutch Camera 1
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085420"
onFailure: retry
enabled: true
softwareTrigger: false
hutch_cam_2:
readoutPriority: baseline
description: Hutch Camera 2
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085436"
onFailure: retry
enabled: true
softwareTrigger: false
hutch_cam_3:
readoutPriority: baseline
description: Hutch Camera 3
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085435"
onFailure: retry
enabled: true
softwareTrigger: false

View File

@@ -51,7 +51,7 @@ optics_config:
## Experimental Hutch ## ## Experimental Hutch ##
################################### ###################################
## NIDAQ # ## NIDAQ
nidaq: nidaq:
readoutPriority: monitored readoutPriority: monitored
description: NIDAQ backend for data reading for debye scans description: NIDAQ backend for data reading for debye scans
@@ -67,8 +67,13 @@ xas_config:
- !include ./x01da_xas.yaml - !include ./x01da_xas.yaml
## XRD (Pilatus, pinhole, beamstop) ## XRD (Pilatus, pinhole, beamstop)
xrd_config: #xrd_config:
- !include ./x01da_xrd.yaml # - !include ./x01da_xrd.yaml
# Commented out because too slow
## Hutch cameras
# hutch_cams:
# - !include ./x01da_hutch_cameras.yaml
## Remaining experimental hutch ## Remaining experimental hutch
es_config: es_config:

View File

@@ -3,35 +3,45 @@
## Ionization Chambers ## ## Ionization Chambers ##
################################### ###################################
# ic0: ic0:
# readoutPriority: baseline readoutPriority: baseline
# description: Ionization chamber 0 description: Ionization chamber 0
# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0 deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0
# deviceConfig: deviceConfig:
# prefix: "X01DA-" prefix: "X01DA-"
# onFailure: retry onFailure: retry
# enabled: true enabled: true
# softwareTrigger: false softwareTrigger: false
# ic1: ic1:
# readoutPriority: baseline readoutPriority: baseline
# description: Ionization chamber 1 description: Ionization chamber 1
# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1 deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1
# deviceConfig: deviceConfig:
# prefix: "X01DA-" prefix: "X01DA-"
# onFailure: retry onFailure: retry
# enabled: true enabled: true
# softwareTrigger: false softwareTrigger: false
# ic2: ic2:
# readoutPriority: baseline readoutPriority: baseline
# description: Ionization chamber 2 description: Ionization chamber 2
# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2 deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2
# deviceConfig: deviceConfig:
# prefix: "X01DA-" prefix: "X01DA-"
# onFailure: retry onFailure: retry
# enabled: true enabled: true
# softwareTrigger: false softwareTrigger: false
pips:
readoutPriority: baseline
description: Pips diode
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.Pips
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
################################### ###################################
## Reference Foil Changer ## ## Reference Foil Changer ##

View File

@@ -86,7 +86,7 @@ pilatus_curtain:
softwareTrigger: false softwareTrigger: false
pilatus: pilatus:
readoutPriority: async readoutPriority: baseline
description: Pilatus description: Pilatus
deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus
deviceTags: deviceTags:
@@ -97,12 +97,12 @@ pilatus:
enabled: true enabled: true
softwareTrigger: true softwareTrigger: true
# sampl_pil: pilatus_smpl:
# readoutPriority: baseline readoutPriority: baseline
# description: Sample to pilatus distance description: Sample to pilatus distance
# deviceClass: ophyd.EpicsSignalRO deviceClass: ophyd.EpicsSignalRO
# deviceConfig: deviceConfig:
# read_pv: "X01DA-SAMPL-PIL" read_pv: "X01DA-ES2-DET:SMPLDIST"
# onFailure: retry onFailure: retry
# enabled: true enabled: true
# softwareTrigger: false softwareTrigger: false

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ophyd import ADBase from ophyd import ADBase, EpicsSignalRO
from ophyd import ADComponent as ADCpt from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt from ophyd import Component as Cpt
from ophyd_devices import PreviewSignal from ophyd_devices import PreviewSignal
@@ -20,6 +20,16 @@ if TYPE_CHECKING: # pragma: no cover
class BaslerCamBase(ADBase): class BaslerCamBase(ADBase):
"""BaslerCam Base class.""" """BaslerCam Base class."""
cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True)
_default_configuration_attrs = [
'cam1.acquire_time',
'cam1.detector_state',
'cam_detector_state_string',
'cam1.gain',
'cam1.model',
]
cam1 = ADCpt(AravisDetectorCam, "cam1:") cam1 = ADCpt(AravisDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:") image1 = ADCpt(ImagePlugin_V35, "image1:")

View File

@@ -0,0 +1,79 @@
"""EH Hutch Cameras"""
from __future__ import annotations
import cv2
import threading
from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from bec_lib.file_utils import get_full_path
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices import DeviceStatus
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
from bec_lib.messages import ScanStatusMessage
logger = bec_logger.logger
CAM_USERNAME = "camera_user"
CAM_PASSWORD = "camera_user1"
CAM_PORT = 554
class HutchCam(PSIDeviceBase):
"""Class for the Hutch Cameras"""
# image = Cpt(Signal, name='image', kind='config')
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, scan_info=scan_info, **kwargs)
self.hostname = prefix
self.status = None
# pylint: disable=E1101
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
if not cap.isOpened():
logger.error(self, "Connection Failed", "Could not connect to the camera stream.")
return
cap.release()
def on_stage(self) -> DeviceStatus:
"""Called while staging the device."""
scan_msg: ScanStatusMessage = self.scan_info.msg
file_path = get_full_path(scan_msg, name='hutch_cam_' + self.hostname).removesuffix('h5')
self.status = DeviceStatus(self)
thread = threading.Thread(target=self._save_picture, args=(file_path, self.status), daemon=True)
thread.start()
return self.status
def _save_picture(self, file_path, status):
try:
logger.info(f'Capture from camera {self.hostname}')
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
if not cap.isOpened():
logger.error("Connection Failed", "Could not connect to the camera stream.")
return
logger.info(f'Connection to camera {self.hostname} established')
ret, frame = cap.readAsync()
cap.release()
if not ret:
logger.error("Capture Failed", "Failed to capture image from camera.")
return
cv2.imwrite(file_path + 'png', frame)
status.set_finished()
logger.info(f'Capture from camera {self.hostname} done')
except Exception as e:
status.set_exception(e)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ophyd import ADBase from ophyd import ADBase, EpicsSignalRO
from ophyd import ADComponent as ADCpt from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt from ophyd import Component as Cpt
from ophyd_devices import PreviewSignal from ophyd_devices import PreviewSignal
@@ -20,6 +20,16 @@ if TYPE_CHECKING: # pragma: no cover
class ProsilicaCamBase(ADBase): class ProsilicaCamBase(ADBase):
"""Base class for Prosilica cameras.""" """Base class for Prosilica cameras."""
cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True)
_default_configuration_attrs = [
'cam1.acquire_time',
'cam1.detector_state',
'cam_detector_state_string',
'cam1.gain',
'cam1.model',
]
cam1 = ADCpt(ProsilicaDetectorCam, "cam1:") cam1 = ADCpt(ProsilicaDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:") image1 = ADCpt(ImagePlugin_V35, "image1:")

View File

@@ -33,22 +33,24 @@ class EpicsSignalSplit(EpicsSignal):
class GasMixSetupControl(Device): class GasMixSetupControl(Device):
"""GasMixSetup Control for Inonization Chamber 0""" """GasMixSetup Control for Inonization Chamber 0"""
gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="config", doc="Gas 1 requirement") gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="omitted", doc="Gas 1 requirement")
conc1_req = Cpt( conc1_req = Cpt(
EpicsSignalWithRBV, suffix="Conc1Req", kind="config", doc="Concentration 1 requirement" EpicsSignalWithRBV, suffix="Conc1Req", kind="omitted", doc="Concentration 1 requirement"
) )
gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="config", doc="Gas 2 requirement") gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="omitted", doc="Gas 2 requirement")
conc2_req = Cpt( conc2_req = Cpt(
EpicsSignalWithRBV, suffix="Conc2Req", kind="config", doc="Concentration 2 requirement" EpicsSignalWithRBV, suffix="Conc2Req", kind="omitted", doc="Concentration 2 requirement"
) )
press_req = Cpt( press_req = Cpt(
EpicsSignalWithRBV, suffix="PressReq", kind="config", doc="Pressure requirement" EpicsSignalWithRBV, suffix="PressReq", kind="omitted", doc="Pressure requirement"
) )
fill = Cpt(EpicsSignal, suffix="Fill", kind="config", doc="Fill the chamber") fill = Cpt(EpicsSignal, suffix="Fill", kind="config", doc="Fill the chamber")
status = Cpt(EpicsSignalRO, suffix="Status", kind="config", doc="Status") status = Cpt(EpicsSignalRO, suffix="Status", kind="config", doc="Status")
gas1 = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1") gas1 = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1")
gas1_string = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1", string=True)
conc1 = Cpt(EpicsSignalRO, suffix="Conc1", kind="config", doc="Concentration 1") conc1 = Cpt(EpicsSignalRO, suffix="Conc1", kind="config", doc="Concentration 1")
gas2 = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2") gas2 = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2")
gas2_string = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2", string=True)
conc2 = Cpt(EpicsSignalRO, suffix="Conc2", kind="config", doc="Concentration 2") conc2 = Cpt(EpicsSignalRO, suffix="Conc2", kind="config", doc="Concentration 2")
press = Cpt(EpicsSignalRO, suffix="PressTransm", kind="config", doc="Current Pressure") press = Cpt(EpicsSignalRO, suffix="PressTransm", kind="config", doc="Current Pressure")
@@ -84,10 +86,25 @@ class IonizationChamber0(PSIDeviceBase):
(f"ES:AMP5004:cFilter{num}_ENUM"), (f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
), ),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
} }
amp = Dcpt(amp_signals) amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}") gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES1-IC{num-1}:") hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES1-IC{num-1}:")
hv_en_signals = { hv_en_signals = {
"ext_ena": ( "ext_ena": (
@@ -275,10 +292,25 @@ class IonizationChamber1(IonizationChamber0):
(f"ES:AMP5004:cFilter{num}_ENUM"), (f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
), ),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
} }
amp = Dcpt(amp_signals) amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}") gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:") hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = { hv_en_signals = {
"ext_ena": ( "ext_ena": (
@@ -311,10 +343,25 @@ class IonizationChamber2(IonizationChamber0):
(f"ES:AMP5004:cFilter{num}_ENUM"), (f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"}, {"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
), ),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
} }
amp = Dcpt(amp_signals) amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}") gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status") gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:") hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = { hv_en_signals = {
"ext_ena": ( "ext_ena": (
@@ -325,3 +372,63 @@ class IonizationChamber2(IonizationChamber0):
"ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}), "ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
} }
hv_en = Dcpt(hv_en_signals) hv_en = Dcpt(hv_en_signals)
class Pips(IonizationChamber0):
"""Pips, prefix should be 'X01DA-'."""
USER_ACCESS = ["set_gain", "set_filter"]
num = 4
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = None
gmes_status_msg = None
hv = None
hv_en_signals = None
hv_en = None
@typechecked
def set_hv(self, *_) -> None:
"""Not available for the PIPS"""
return None
@typechecked
def set_grid(self, *_) -> None:
"""Not available for the PIPS"""
return None
@typechecked
def fill(self, *_) -> None:
"""Not available for the PIPS"""
return None

View File

@@ -146,6 +146,76 @@ class NidaqControl(Device):
doc="EPICS counter input 7", doc="EPICS counter input 7",
auto_monitor=True, auto_monitor=True,
) )
ci8 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI8",
kind=Kind.normal,
doc="EPICS counter input 8",
auto_monitor=True,
)
ci9 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI9",
kind=Kind.normal,
doc="EPICS counter input 9",
auto_monitor=True,
)
ci10 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI10",
kind=Kind.normal,
doc="EPICS counter input 0",
auto_monitor=True,
)
ci11 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI11",
kind=Kind.normal,
doc="EPICS counter input 1",
auto_monitor=True,
)
ci12 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI12",
kind=Kind.normal,
doc="EPICS counter input 2",
auto_monitor=True,
)
ci13 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI13",
kind=Kind.normal,
doc="EPICS counter input 3",
auto_monitor=True,
)
ci14 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI14",
kind=Kind.normal,
doc="EPICS counter input 4",
auto_monitor=True,
)
ci15 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI15",
kind=Kind.normal,
doc="EPICS counter input 5",
auto_monitor=True,
)
ci16 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI16",
kind=Kind.normal,
doc="EPICS counter input 6",
auto_monitor=True,
)
ci17 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI17",
kind=Kind.normal,
doc="EPICS counter input 7",
auto_monitor=True,
)
di0 = Cpt( di0 = Cpt(
EpicsSignalRO, EpicsSignalRO,
@@ -275,6 +345,36 @@ class NidaqControl(Device):
ci7_mean = Cpt( ci7_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN" SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN"
) )
ci8_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN"
)
ci9_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN"
)
ci10_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN"
)
ci11_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN"
)
ci12_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN"
)
ci13_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN"
)
ci14_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN"
)
ci15_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN"
)
ci16_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN"
)
ci17_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN"
)
ci0_std_dev = Cpt( ci0_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD" SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD"
@@ -300,12 +400,54 @@ class NidaqControl(Device):
ci7_std_dev = Cpt( ci7_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD" SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD"
) )
ci8_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD"
)
ci9_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD"
)
ci10_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD"
)
ci11_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD"
)
ci12_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD"
)
ci13_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD"
)
ci14_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD"
)
ci15_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD"
)
ci16_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD"
)
ci17_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD"
)
cisum = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter sum"
)
smpl_abs = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream sample absorption"
)
ref_abs = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream reference absorption"
)
xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp") xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp")
xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp") xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp")
xrd_angle = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD angle")
xrd_energy = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy") xrd_energy = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy")
xrd_ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 mean")
xrd_ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 std dev")
di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX") di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX")
di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX") di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX")
@@ -320,16 +462,21 @@ class NidaqControl(Device):
### Control PVs ### ### Control PVs ###
enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config) enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config)
enable_dead_time_correction = Cpt(EpicsSignal, suffix="NIDAQ-EnableDTC", kind=Kind.config)
kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config) kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config)
stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind=Kind.config) stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind=Kind.config)
state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind=Kind.config, auto_monitor=True) state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind=Kind.config, auto_monitor=True)
server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config) server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config)
compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config) compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config)
scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config) scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config)
scan_type_string = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config, string=True)
sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config) sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config)
sampling_rate_string = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config, string=True)
scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config) scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config)
readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config) readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config)
readout_range_string = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config, string=True)
encoder_factor = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config) encoder_factor = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config)
encoder_factor_string = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config, string=True)
stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config) stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config)
power = Cpt(EpicsSignal, suffix="NIDAQ-Power", kind=Kind.config) power = Cpt(EpicsSignal, suffix="NIDAQ-Power", kind=Kind.config)
heartbeat = Cpt(EpicsSignal, suffix="NIDAQ-Heartbeat", kind=Kind.config, auto_monitor=True) heartbeat = Cpt(EpicsSignal, suffix="NIDAQ-Heartbeat", kind=Kind.config, auto_monitor=True)
@@ -357,7 +504,7 @@ class Nidaq(PSIDeviceBase, NidaqControl):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs) super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.scan_info: ScanInfo self.scan_info: ScanInfo
self.timeout_wait_for_signal = 5 # put 5s firsts self.timeout_wait_for_signal = 5 # put 5s firsts
self._timeout_wait_for_pv = 3 # 3s timeout for pv calls self._timeout_wait_for_pv = 5 # 5s timeout for pv calls. editted due to timeout issues persisting
self.valid_scan_names = [ self.valid_scan_names = [
"xas_simple_scan", "xas_simple_scan",
"xas_simple_scan_with_xrd", "xas_simple_scan_with_xrd",
@@ -556,7 +703,11 @@ class Nidaq(PSIDeviceBase, NidaqControl):
# Stage call to IOC # Stage call to IOC
status = CompareStatus(self.state, NidaqState.STAGE) status = CompareStatus(self.state, NidaqState.STAGE)
self.cancel_on_stop(status) self.cancel_on_stop(status)
self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv) # TODO 11.11.25/HS64
# Switched from set to put in the hope to get rid of the rare event where nidaq is stopped at the start of a scan
# Problems consistently persisting, testing changing back to set, unconvinced this is the actual cause 14.11.25/AHC
# self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv)
self.stage_call.put(1)
status.wait(timeout=self.timeout_wait_for_signal) status.wait(timeout=self.timeout_wait_for_signal)
if self.scan_info.msg.scan_name != "nidaq_continuous_scan": if self.scan_info.msg.scan_name != "nidaq_continuous_scan":
status = self.on_kickoff() status = self.on_kickoff()

View File

@@ -6,13 +6,13 @@ import enum
import threading import threading
import time import time
import traceback import traceback
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Tuple
import numpy as np import numpy as np
from bec_lib.file_utils import get_full_path from bec_lib.file_utils import get_full_path
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from ophyd import Component as Cpt from ophyd import Component as Cpt
from ophyd import EpicsSignal, Kind from ophyd import EpicsSignal, EpicsSignalRO, Kind
from ophyd.areadetector.cam import ADBase, PilatusDetectorCam from ophyd.areadetector.cam import ADBase, PilatusDetectorCam
from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin
from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin
@@ -145,6 +145,19 @@ class Pilatus(PSIDeviceBase, ADBase):
# USER_ACCESS = ["start_live_mode", "stop_live_mode"] # USER_ACCESS = ["start_live_mode", "stop_live_mode"]
cam_gain_menu_string = Cpt(EpicsSignalRO, suffix='cam1:GainMenu', string=True)
_default_configuration_attrs = [
'cam.threshold_energy',
'cam.threshold_auto_apply',
'cam.gain_menu',
'cam_gain_menu_string',
'cam.pixel_cut_off',
'cam.acquire_time',
'cam.num_exposures',
'cam.model',
]
cam = Cpt(PilatusDetectorCam, "cam1:") cam = Cpt(PilatusDetectorCam, "cam1:")
hdf = Cpt(HDF5Plugin, "HDF1:") hdf = Cpt(HDF5Plugin, "HDF1:")
image1 = Cpt(ImagePlugin, "image1:") image1 = Cpt(ImagePlugin, "image1:")
@@ -203,22 +216,11 @@ class Pilatus(PSIDeviceBase, ADBase):
PreviewSignal, PreviewSignal,
name="preview", name="preview",
ndim=2, ndim=2,
num_rotation_90=0, # Check this num_rotation_90=3,
doc="Preview signal for the Pilatus Detector", doc="Preview signal for the Pilatus Detector",
) )
file_event = Cpt(FileEventSignal, name="file_event") file_event = Cpt(FileEventSignal, name="file_event")
@property
def baseline_signals(self):
"""Define baseline signals"""
return [
self.cam.acquire_time,
self.cam.num_exposures,
self.cam.threshold_energy,
self.cam.gain_menu,
self.cam.pixel_cut_off,
]
def __init__( def __init__(
self, self,
*, *,
@@ -366,68 +368,51 @@ class Pilatus(PSIDeviceBase, ADBase):
status = status_acquire & status_writing & status_cam_server status = status_acquire & status_writing & status_cam_server
return status return status
def _calculate_trigger(self, scan_msg: ScanStatusMessage): def _calculate_trigger(self, scan_msg: ScanStatusMessage) -> Tuple[float, float]:
self._update_scan_parameter() self._update_scan_parameter()
total_osc = 0 total_osc = 0
calc_duration = 0
total_trig_lo = 0 total_trig_lo = 0
total_trig_hi = 0 total_trig_hi = 0
calc_duration = 0 # Switching high/low is intended as angle is inverse to energy and settings in BEC are always in energy
n_trig_lo = 1 loc_break_enable_low = self.scan_parameter.break_enable_high
n_trig_hi = 1 loc_break_time_low = self.scan_parameter.break_time_high
init_lo = 1 loc_cycle_low = self.scan_parameter.cycle_high
init_hi = 1 loc_break_enable_high = self.scan_parameter.break_enable_low
lo_done = 0 loc_break_time_high = self.scan_parameter.break_time_low
hi_done = 0 loc_cycle_high = self.scan_parameter.cycle_low
if not self.scan_parameter.break_enable_low:
lo_done = 1 if not loc_break_enable_low:
if not self.scan_parameter.break_enable_high: loc_break_time_low = 0
hi_done = 1 loc_cycle_low = 1
start_time = time.time() if not loc_break_enable_high:
while True: loc_break_time_high = 0
# TODO, we should not use infinite loops, for now let's add the escape Timeout of 20s, but should eventually be reviewed. loc_cycle_high = 1
if time.time() - start_time > 20:
raise RuntimeError( total_osc = self.scan_parameter.scan_duration / (
f"Calculating the number of triggers for scan {scan_msg.scan_name} took more than 20 seconds, aborting." self.scan_parameter.scan_time +
) loc_break_time_low / (2 * loc_cycle_low) +
loc_break_time_high / (2 * loc_cycle_high)
)
total_osc = np.ceil(total_osc)
total_osc = total_osc + total_osc % 2 # round up to the next even number
if loc_break_enable_low:
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
if loc_break_enable_high:
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high
if calc_duration < self.scan_parameter.scan_duration:
# Due to inaccuracy in formula, this can happen, we then need to manually add two oscillations and recalculate the triggers
total_osc = total_osc + 2 total_osc = total_osc + 2
calc_duration = calc_duration + 2 * self.scan_parameter.scan_time if loc_break_enable_low:
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
if loc_break_enable_high:
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high
if self.scan_parameter.break_enable_low and n_trig_lo >= self.scan_parameter.cycle_low: return total_trig_lo, total_trig_hi
n_trig_lo = 1
calc_duration = calc_duration + self.scan_parameter.break_time_low
if init_lo:
lo_done = 1
init_lo = 0
else:
n_trig_lo += 1
if (
self.scan_parameter.break_enable_high
and n_trig_hi >= self.scan_parameter.cycle_high
):
n_trig_hi = 1
calc_duration = calc_duration + self.scan_parameter.break_time_high
if init_hi:
hi_done = 1
init_hi = 0
else:
n_trig_hi += 1
if lo_done and hi_done:
n = np.floor(self.scan_parameter.scan_duration / calc_duration)
total_osc = total_osc * n
if self.scan_parameter.break_enable_low:
total_trig_lo = n + 1
if self.scan_parameter.break_enable_high:
total_trig_hi = n + 1
calc_duration = calc_duration * n
lo_done = 0
hi_done = 0
if calc_duration >= self.scan_parameter.scan_duration:
break
return total_trig_lo + total_trig_hi
######################################## ########################################
# Beamline Specific Implementations # # Beamline Specific Implementations #
@@ -480,6 +465,14 @@ class Pilatus(PSIDeviceBase, ADBase):
""" """
# self.stop_live_mode() # Make sure that live mode is stopped if scan runs # self.stop_live_mode() # Make sure that live mode is stopped if scan runs
# If user has activated alignment mode on qt panel, switch back to multitrigger and stop acquisition
if self.cam.trigger_mode.get() != TRIGGERMODE.MULT_TRIGGER.value:
self.cam.trigger_mode.set(TRIGGERMODE.MULT_TRIGGER.value).wait(5)
if self.cam.acquire.get() == ACQUIREMODE.ACQUIRING.value:
self.cam.acquire.put(0)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_cam.wait(timeout=5)
scan_msg: ScanStatusMessage = self.scan_info.msg scan_msg: ScanStatusMessage = self.scan_info.msg
if scan_msg.scan_name in self.xas_xrd_scan_names: if scan_msg.scan_name in self.xas_xrd_scan_names:
self._update_scan_parameter() self._update_scan_parameter()

View File

@@ -69,11 +69,11 @@ class PilatusCurtain(PSIDeviceBase):
def on_unstage(self) -> DeviceStatus | None: def on_unstage(self) -> DeviceStatus | None:
"""Called while unstaging the device.""" """Called while unstaging the device."""
return self.close() # return self.close()
def on_stop(self) -> DeviceStatus | None: def on_stop(self) -> DeviceStatus | None:
"""Called when the device is stopped.""" """Called when the device is stopped."""
return self.close() # return self.close()
def open(self) -> DeviceStatus | None: def open(self) -> DeviceStatus | None:
"""Open the cover""" """Open the cover"""

View File

@@ -52,9 +52,15 @@ class Reffoilchanger(PSIDeviceBase):
status = Cpt( status = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status" EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status"
) )
status_string = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status", string=True
)
op_mode = Cpt( op_mode = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status" EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status"
) )
op_mode_string = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status", string=True
)
ref_set = Cpt(EpicsSignal, suffix="ES2-REF:SELN-SET", kind="config", doc="Requested reference") ref_set = Cpt(EpicsSignal, suffix="ES2-REF:SELN-SET", kind="config", doc="Requested reference")
ref_rb = Cpt( ref_rb = Cpt(
EpicsSignalRO, suffix="ES2-REF:SELN-RB", kind="config", doc="Currently set reference" EpicsSignalRO, suffix="ES2-REF:SELN-RB", kind="config", doc="Currently set reference"

View File

@@ -12,7 +12,7 @@ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering",
] ]
dependencies = ["numpy", "scipy", "bec_lib", "h5py", "ophyd_devices"] dependencies = ["numpy", "scipy", "bec_lib", "h5py", "ophyd_devices", "opencv-python==4.11.0.86"]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [