feat: add logic for ext/en in ddg, mcs and flomni
CI for csaxs_bec / test (push) Failing after 1m24s
CI for csaxs_bec / test (pull_request) Failing after 1m33s

This commit is contained in:
x12sa
2026-01-30 12:55:06 +01:00
parent 82d47c7511
commit ed8d012632
9 changed files with 155 additions and 39 deletions
+6 -6
View File
@@ -425,7 +425,7 @@ sl5trxi:
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: x12sa-eb-smaract-mcs-02.psi.ch
host: x12sa-eb-smaract-mcs-05.psi.ch
limits:
- -200
- 200
@@ -447,7 +447,7 @@ sl5trxo:
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: D
host: x12sa-eb-smaract-mcs-02.psi.ch
host: x12sa-eb-smaract-mcs-05.psi.ch
limits:
- -200
- 200
@@ -469,7 +469,7 @@ sl5trxb:
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: E
host: x12sa-eb-smaract-mcs-02.psi.ch
host: x12sa-eb-smaract-mcs-05.psi.ch
limits:
- -200
- 200
@@ -491,7 +491,7 @@ sl5trxt:
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: F
host: x12sa-eb-smaract-mcs-02.psi.ch
host: x12sa-eb-smaract-mcs-05.psi.ch
limits:
- -200
- 200
@@ -513,7 +513,7 @@ xbimtrx:
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: x12sa-eb-smaract-mcs-02.psi.ch
host: x12sa-eb-smaract-mcs-05.psi.ch
limits:
- -200
- 200
@@ -535,7 +535,7 @@ xbimtry:
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: x12sa-eb-smaract-mcs-02.psi.ch
host: x12sa-eb-smaract-mcs-05.psi.ch
limits:
- -200
- 200
+14 -14
View File
@@ -408,20 +408,20 @@ cam_xeye:
readOnly: false
readoutPriority: async
cam_ids_rgb:
description: Camera flOMNI Xray eye ID203
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
deviceConfig:
camera_id: 203
bits_per_pixel: 24
num_rotation_90: 3
transpose: false
force_monochrome: true
m_n_colormode: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: async
# cam_ids_rgb:
# description: Camera flOMNI Xray eye ID203
# deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
# deviceConfig:
# camera_id: 203
# bits_per_pixel: 24
# num_rotation_90: 3
# transpose: false
# force_monochrome: true
# m_n_colormode: 1
# enabled: true
# onFailure: buffer
# readOnly: false
# readoutPriority: async
# ############################################################
@@ -37,7 +37,7 @@ to interrupt and ongoing sequence if needed.
- a = t0 + 2ms (2ms delay to allow the shutter to open)
- b = a + 1us (short pulse)
- c = t0
- d = a + exp_time * burst_count + 1ms (to allow the shutter to close)
- d = a + exp_time * burst_count
- e = d
- f = e + 1us (short pulse to OR gate for MCS triggering)
@@ -24,7 +24,7 @@ DELAY CHANNELS:
- a = t0 + 2ms (2ms delay to allow the shutter to open)
- b = a + 1us (short pulse)
- c = t0
- d = a + exp_time * burst_count + 1ms (to allow the shutter to close)
- d = a + exp_time * burst_count
- e = d
- f = e + 1us (short pulse to OR gate for MCS triggering)
"""
@@ -69,7 +69,7 @@ logger = bec_logger.logger
# This can be adapted as needed, or fine-tuned per channel. On every reload of the
# device configuration in BEC, these values will be set into the DDG1 device.
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
"amplitude": 5.0,
"amplitude": 4.5,
"offset": 0.0,
"polarity": OUTPUTPOLARITY.POSITIVE,
"mode": "ttl",
@@ -131,6 +131,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None.
"""
USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger"]
def __init__(
self,
name: str,
@@ -142,6 +144,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
super().__init__(
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
)
self._shutter_to_open_delay = 2e-3
self.device_manager = device_manager
self._poll_thread = threading.Thread(target=self._poll_event_status, daemon=True)
self._poll_thread_run_event = threading.Event()
@@ -192,6 +195,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
self.burst_delay.put(0)
self.burst_count.put(1)
def keep_shutter_open_during_scan(self, open:True) -> None:
if open is True:
self._shutter_to_open_delay = 0
else:
self._shutter_to_open_delay = 2e-3
def on_stage(self) -> None:
"""
@@ -239,27 +248,30 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# Burst Period DDG1
# Set burst_period to shutter width
# c/t0 + 2ms + exp_time * burst_count + 1ms
shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3
# c/t0 + self._shutter_to_open_delay + exp_time * burst_count
shutter_width = self._shutter_to_open_delay + exp_time * frames_per_trigger # Shutter starts closing at end of exposure
if self.burst_period.get() != shutter_width:
self.burst_period.put(shutter_width)
# Trigger DDG2
# a = t0 + 2ms, b = a + 1us
# a has reference to t0, b has reference to a
# Add delay of 2ms to allow shutter to open
self.set_delay_pairs(channel="ab", delay=2e-3, width=1e-6)
# Add delay of self._shutter_to_open_delay to allow shutter to open
self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=1e-6)
# Trigger shutter
# d = c/t0 + 2ms + exp_time * burst_count + 1ms
# d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms
# c has reference to t0, d has reference to c
# Shutter opens without delay at t0, closes after exp_time * burst_count + 3ms (2ms open, 1ms close)
# Shutter opens without delay at t0, closes after exp_time * burst_count + 2ms (self._shutter_to_open_delay)
self.set_delay_pairs(channel="cd", delay=0, width=shutter_width)
# Trigger extra pulse for MCS OR gate
# f = e + 1us
# e has refernce to d, f has reference to e
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
if self.scan_info.msg.scan_type == "fly":
self.set_delay_pairs(channel="ef", delay=0, width=0)
else:
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
# NOTE Add additional sleep to make sure that the IOC and DDG HW process the values properly
# This value has been choosen empirically after testing with the HW. It's
@@ -37,6 +37,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
ChannelConfig,
DelayGeneratorCSAXS,
LiteralChannels,
BURSTCONFIG,
)
logger = bec_logger.logger
@@ -47,7 +48,7 @@ logger = bec_logger.logger
# NOTE Default channel configuration for the DDG2 delay generator channels
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
"amplitude": 5.0,
"amplitude": 4.5,
"offset": 0.0,
"polarity": OUTPUTPOLARITY.POSITIVE,
"mode": "ttl",
@@ -134,6 +135,9 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
self.set_references_for_channels(DEFAULT_REFERENCES)
# Set burst config
self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value)
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""
+72
View File
@@ -0,0 +1,72 @@
import time
import socket
from ophyd import Component as Cpt
from ophyd import Device, Kind
from ophyd import EpicsSignal
class FastEpicsShutterError(Exception):
pass
class cSAXSFastEpicsShutter(Device):
"""
Fast EPICS shutter with automatic PV selection based on host subnet. IOC prefix is 'X12SA-ES1-TTL:'
"""
USER_ACCESS = ["fshopen", "fshclose", "fshstatus", "fshinfo", "fshstatus_readback", "help"]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
# PVs
shutter = Cpt(EpicsSignal, "OUT_01", kind=Kind.normal, auto_monitor=True)
shutter_readback = Cpt(EpicsSignal, "IN_01", kind=Kind.normal, auto_monitor=True)
# -----------------------------------------------------
# User-facing shutter control functions
# -----------------------------------------------------
def fshopen(self):
"""Open the fast shutter."""
try:
self.shutter.put(1, wait=True)
except Exception as ex:
raise OMNYFastEpicsShutterError(f"Failed to open shutter: {ex}")
def fshclose(self):
"""Close the fast shutter."""
try:
self.shutter.put(0, wait=True)
except Exception as ex:
raise OMNYFastEpicsShutterError(f"Failed to close shutter: {ex}")
def fshstatus(self):
"""Return the fast shutter status (0=closed, 1=open)."""
try:
return self.shutter.get()
except Exception as ex:
raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}")
def fshstatus_readback(self):
"""Return the fast shutter status (0=closed, 1=open)."""
try:
return self.shutter_readback.get()
except Exception as ex:
raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}")
def fshinfo(self):
"""Print information about which EPICS PV channel is being used."""
pvname = self.shutter.pvname
shutter_readback_pvname = self.shutter_readback.pvname
print(f"Fast shutter connected to EPICS channel: {pvname} with shutter readback: {shutter_readback_pvname}")
return pvname
def help(self):
"""Display available user methods."""
print("Available methods:")
for method in self.USER_ACCESS:
print(f" - {method}")
if __name__ == "__main__":
fsh = cSAXSFastEpicsShutter(name="fsh", prefix="X12SA-ES1-TTL:")
@@ -22,7 +22,7 @@ import numpy as np
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import EpicsSignalRO, Kind
from ophyd_devices import AsyncMultiSignal, CompareStatus, ProgressSignal, StatusBase
from ophyd_devices import AsyncMultiSignal, CompareStatus, ProgressSignal, StatusBase, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.epics.mcs_card.mcs_card import (
@@ -255,6 +255,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
**kwargs: Additional keyword arguments from the subscription, including 'obj' (the EpicsSignalRO instance).
"""
with self._rlock:
logger.info(f"Received update on mcs card {self.name}")
if self._omit_mca_callbacks.is_set():
return # Suppress callbacks when erasing all channels
self._mca_counter_index += 1
@@ -286,7 +287,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
)
# Once we have received all channels, push data to BEC and reset for next accumulation
logger.debug(
logger.info(
f"Received update for {attr_name}, index {self._mca_counter_index}/{self.NUM_MCA_CHANNELS}"
)
if len(self._current_data) == self.NUM_MCA_CHANNELS:
@@ -363,7 +364,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self._num_total_triggers = triggers * num_points
self._acquisition_group = "monitored" if triggers == 1 else "burst_group"
self.preset_real.set(0).wait(timeout=self._pv_timeout)
self.num_use_all.set(triggers).wait(timeout=self._pv_timeout)
if self.scan_info.msg.scan_type == "step":
self.num_use_all.set(triggers).wait(timeout=self._pv_timeout)
elif self.scan_info.msg.scan_type == "fly":
self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout)
# Clear any previous data, just to be sure
with self._rlock:
@@ -385,6 +390,12 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self._omit_mca_callbacks.clear()
logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
# For a fly scan we need to start the mcs card ourselves
if self.scan_info.msg.scan_type == "fly":
status_acquiring = TransitionStatus(self.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING])
self.cancel_on_stop(status_acquiring)
self.erase_start.put(1)
def on_unstage(self) -> None:
"""
@@ -422,9 +433,16 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
hasattr(self.scan_info.msg, "num_points")
and self.scan_info.msg.num_points is not None
):
if self._current_data_index == self.scan_info.msg.num_points:
for callback in self._scan_done_callbacks:
callback(exception=None)
if self.scan_info.msg.scan_type == "step":
if self._current_data_index == self.scan_info.msg.num_points:
for callback in self._scan_done_callbacks:
callback(exception=None)
else:
logger.info(f"Current data index is {self._current_data_index}")
if self._current_data_index >=1:
for callback in self._scan_done_callbacks:
callback(exception=None)
time.sleep(0.02) # 20ms delay to avoid busy loop
except Exception as exc: # pylint: disable=broad-except
content = traceback.format_exc()
@@ -478,6 +496,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
monitoring thread is stopped properly.
"""
self.software_channel_advance.put(1)
# Prepare and register status callback for the async monitoring loop
status_async_data = StatusBase(obj=self)
self._scan_done_callbacks.append(partial(self._status_callback, status_async_data))
@@ -629,6 +629,8 @@ class RtFlomniMotor(Device, PositionerBase):
SUB_CONNECTION_CHANGE = "connection_change"
_default_sub = SUB_READBACK
connectionTimeout = 20
def __init__(
self,
axis_Id,
+9 -2
View File
@@ -27,6 +27,7 @@ from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints
from bec_server.scan_server.errors import ScanAbortion
from bec_server.scan_server.scans import SyncFlyScanBase
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE
logger = bec_logger.logger
@@ -73,14 +74,13 @@ class FlomniFermatScan(SyncFlyScanBase):
>>> scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01)
"""
super().__init__(parameter=parameter, **kwargs)
super().__init__(parameter=parameter, exp_time=exp_time, **kwargs)
self.show_live_table = False
self.axis = []
self.fovx = fovx
self.fovy = fovy
self.cenx = cenx
self.ceny = ceny
self.exp_time = exp_time
self.step = step
self.zshift = zshift
self.angle = angle
@@ -151,6 +151,9 @@ class FlomniFermatScan(SyncFlyScanBase):
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
def _prepare_setup_part2(self):
# Prepare DDG1 to use
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value)
if self.flomni_rotation_status:
self.flomni_rotation_status.wait()
@@ -307,6 +310,10 @@ class FlomniFermatScan(SyncFlyScanBase):
logger.warning("No positions found to return to start")
def cleanup(self):
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.SINGLE_SHOT.value)
yield from super().cleanup()
def run(self):
self.initialize()
yield from self.read_scan_motors()