fix(ddg): fix logic to resolve trigger on ddg1.
CI for csaxs_bec / test (pull_request) Failing after 1m26s
CI for csaxs_bec / test (push) Failing after 1m28s

This commit is contained in:
2026-02-11 11:22:09 +01:00
parent f8d2af4c5b
commit 67ef20cfc8
@@ -488,12 +488,18 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
def on_trigger(self) -> DeviceStatus:
"""
This method is called from BEC as a software trigger. Here the logic is as follows:
We check the signal of the "fast_shutter_control". If it is low, we know that the shutter
open/closes for each trigger cycle. In this case, we can use the signal of the
"fast_shutter_readback" to check if the shutter transitioned from open to close, which
indicates the end of the burst. If the "fast_shutter_control" signal is high, we know
that the shutter is kept open during the scan. In this case, we can only rely on polling
the event status register to check if the burst finished.
We first check if the trigger_source is set to SINGLE_SHOT. Only then will we received,
otherwise we return a status object directly as the DDG is triggered by an external
source which will have to implement its own logic to wait for trigger signals to
be received.
I SINGLE_SHOT, the implementation here will send a software trigger. Now there are
two options to wait for the trigger (burst) cycle to be done. One is to rely on the
signal of the "mcs" card if it is present. However, this is only possible if the
scan_type is not "fly" as in fly scans the ef channel is not triggered to send the last
pulse to the card (but the card is finishing its acquisition in complete itself). Then
we rely on the polling of the event status register to check if the burst cycle is done.
It follows a specific procedure to ensure that the DDG1 and MCS card are properly handled
on a trigger event. The established logic is as follows:
@@ -512,8 +518,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
- Return the status object to BEC which will automatically resolve once the status register has
the END_OF_BURST bit set. The callback of the status object will also stop the polling loop.
"""
overall_start = time.time()
self._stop_polling()
self._poll_thread_poll_loop_done.wait(timeout=1)
# NOTE If the trigger source is not SINGLE_SHOT, the DDG is triggered by an external source
# thus we can not expect that trigger signals are meant to be awaited for. In this case,
@@ -525,40 +531,33 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# NOTE If the MCS card is present in the current session of BEC,
# we prepare the card for the next trigger. The procedure is implemented
# in the '_prepare_mcs_on_trigger' method.
# Prepare the MCS card for the next software trigger
# in the '_prepare_mcs_on_trigger' method. We will also use the mcs card
# to indicate when the burst cycle is done. If no mcs card is available
# the fallback is to use the polling of the DDG
mcs = self.device_manager.devices.get("mcs", None)
if mcs is None or mcs.enabled is False:
logger.info("Did not find mcs card with name 'mcs' in current session")
else:
status_mcs = self._prepare_mcs_on_trigger(mcs)
# NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash
# an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should
# be investigated why the EPICS interface is slow to respond.
status_mcs.wait(timeout=3)
# Use fast shutter readback in case it is not kept open
# NOTE: THe update frequency of the EpicsSIgnal is ~10Hz (100ms), needs to be checked if that can
# be adjusted to ~2kHz
if (
self.fast_shutter_control.get() != 1
): # Shutter is not kept open, we can rely on its close signal.
status = TransitionStatus(
self.fast_shutter_readback, [1, 0]
) # Add timeout error with explicit info update epics update freq
else:
# NOTE This sleep is needed for 20ms to make sure that the HW of the DDG is
# again ready to process new commands. It was transferred from just after the
# _stop_polling() call, as it should only be relevant in case of polling the
# event status register, which may only be if the shutter is kept open.
if mcs is None or mcs.enabled is False or self.scan_info.msg.scan_type == "fly":
self._poll_thread_poll_loop_done.wait(timeout=1)
logger.warning("Did not find mcs card with name 'mcs' in current session")
time.sleep(0.02)
# Shutter is kept open, we can only rely on the event status register
status = self._prepare_trigger_status_event()
# Start polling thread again to monitor event status
self._start_polling()
# Trigger the DDG1
self.cancel_on_stop(status)
else:
start_time = time.time()
logger.debug(f"Preparing mcs card ")
status_mcs = self._prepare_mcs_on_trigger(mcs)
# NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash
# an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should
# be investigated why the EPICS interface is slow to respond.
status_mcs.wait(timeout=3)
status = TransitionStatus(mcs.acquiring, [ACQUIRING.ACQUIRING, ACQUIRING.DONE])
logger.debug(f"Finished preparing mcs card {time.time()-start_time}")
# Send trigger
self.trigger_shot.put(1, use_complete=True)
self.cancel_on_stop(status)
logger.info(f"Configured ddg in {time.time()-overall_start}")
return status
def on_stop(self) -> None: