From 039230e79eccce289df2df32103f0bf80301ab0e Mon Sep 17 00:00:00 2001 From: appel_c Date: Tue, 23 Jun 2026 12:51:42 +0200 Subject: [PATCH] refactor(mcs): add write timeout to mcs card --- csaxs_bec/devices/epics/mcs_card/mcs_card.py | 32 ++++++++++++++++++- .../devices/epics/mcs_card/mcs_card_csaxs.py | 6 ++-- csaxs_bec/scans/scans_v4/cont_grid.py | 6 ++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card.py b/csaxs_bec/devices/epics/mcs_card/mcs_card.py index ef0360e..6d18dfb 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card.py @@ -164,6 +164,8 @@ class MCSCard(Device): - EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html """ + WRITE_TIMEOUT = 4.0 # seconds + snl_connected = Cpt( EpicsSignalRO, "SNL_Connected", @@ -175,18 +177,21 @@ class MCSCard(Device): EpicsSignal, "EraseAll", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0. Please note that this operation sends the mca or waveform records to process after erasing, potentially also 0s.", ) erase_start = Cpt( EpicsSignal, "EraseStart", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Erases all mca or waveform records and starts acquisition.", ) start_all = Cpt( EpicsSignal, "StartAll", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Starts or resumes acquisition without erasing first.", ) acquiring = Cpt( @@ -196,11 +201,18 @@ class MCSCard(Device): auto_monitor=True, doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.", ) - stop_all = Cpt(EpicsSignal, "StopAll", kind=Kind.omitted, doc="Stops acquisition.") + stop_all = Cpt( + EpicsSignal, + "StopAll", + kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, + doc="Stops acquisition.", + ) preset_real = Cpt( EpicsSignal, "PresetReal", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Preset real time. If non-zero then acquisition will stop when this time is reached.", ) elapsed_real = Cpt( @@ -213,72 +225,84 @@ class MCSCard(Device): EpicsSignal, "DoReadAll.VAL", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Forces a read of all mca or waveform records from the hardware. This record can be set to periodically process to update the records during acquisition. Note that even if this record has SCAN=Passive the mca or waveform records will always process once when acquisition completes.", ) read_mode = Cpt( EpicsSignal, "ReadAll.SCAN", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Readout mode for transferring data from FIFO buffer to mca EPICS scalars.", ) num_use_all = Cpt( EpicsSignal, "NuseAll", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The number of channels to use for the mca or waveform records. Acquisition will automatically stop when the number of channel advances reaches this value.", ) dwell = Cpt( EpicsSignal, "Dwell", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The dwell time per channel when using internal channel advance mode.", ) channel_advance = Cpt( EpicsSignal, "ChannelAdvance", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The channel advance mode. Choices are 'Internal' (count for a preset time per channel) or 'External' (advance on external hardware channel advance signal).", ) count_on_start = Cpt( EpicsSignal, "CountOnStart", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Flag controlling whether the module begins counting immediately when acquisition starts. This record only applies in External channel advance mode. If No (=0) then counting does not start in channel 0 until receipt of the first external channel advance pulse. If Yes (=1) then counting in channel 0 starts immediately when acquisition starts, without waiting for the first external channel advance pulse.", ) software_channel_advance = Cpt( EpicsSignal, "SoftwareChannelAdvance", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Processing this record causes a channel advance to occur immediately, without waiting for the current dwell time to be reached or the next external channel advance pulse to arrive.", ) channel1_source = Cpt( EpicsSignal, "Channel1Source", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Controls the source of pulses into the first counter. The choices are 'Int. clock' which selects the internal clock, and 'External' which selects the external pulse input to counter 1.", ) prescale = Cpt( EpicsSignal, "Prescale", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The prescale factor for external channel advance pulses. If the prescale factor is N then N external channel advance pulses must be received before a channel advance will occur.", ) enable_client_wait = Cpt( EpicsSignal, "EnableClientWait", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Flag to force acquisition to wait until a client clears the ClientWait busy record before proceeding to the next acquisition. This can be useful with the scan record.", ) client_wait = Cpt( EpicsSignal, "ClientWait", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Flag that will be set to 1 when acquisition completes, and which a client must set back to 0 to allow acquisition to proceed. This only has an effect if EnableClientWait is 1.", ) acquire_mode = Cpt( EpicsSignal, "AcquireMode", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.", ) # NOTE: Setting mux_output programmatically results in occasional errors on the IOC; it is recommended to avoid using it. @@ -286,36 +310,42 @@ class MCSCard(Device): EpicsSignal, "MUXOutput", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3. NOTE: This settings seems to occasionally result in errors on the IOC; it is recommended to avoid using it.", ) user_led = Cpt( EpicsSignal, "UserLED", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="Toggles the user LED and also output signal 8 on the SIS3820.", ) input_mode = Cpt( EpicsSignal, "InputMode", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The input mode. Supported input modes vary for SIS3801 and SIS3820.", ) input_polarity = Cpt( EpicsSignal, "InputPolarity", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The polarity of the input control signals on the SIS3820. Choices are Normal and Inverted.", ) output_mode = Cpt( EpicsSignal, "OutputMode", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The output mode. Supported output modes vary for SIS3801 and SIS3820.", ) output_polarity = Cpt( EpicsSignal, "OutputPolarity", kind=Kind.omitted, + write_timeout=WRITE_TIMEOUT, doc="The polarity of the output control signals on the SIS3820. Choices are Normal and Inverted.", ) model = Cpt( diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py index 96ebb53..9948784 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -381,11 +381,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._num_lines = self.scan_parameters.additional_scan_parameters.get("num_lines", 1) self._current_line = 1 self._acquisition_group = "monitored" if triggers == 1 else "burst_group" - self.preset_real.set(0).wait(timeout=self._pv_timeout) + self.preset_real.set(0).wait() # TODO consider using put... if self.scan_parameters.scan_type == "software_triggered": - self.num_use_all.set(triggers).wait(timeout=self._pv_timeout) + self.num_use_all.set(triggers).wait() elif self.scan_parameters.scan_type == "hardware_triggered": - self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout) + self.num_use_all.set(self._num_total_triggers).wait() # Clear any previous data, just to be sure with self._rlock: diff --git a/csaxs_bec/scans/scans_v4/cont_grid.py b/csaxs_bec/scans/scans_v4/cont_grid.py index ec58c0e..2863fbb 100644 --- a/csaxs_bec/scans/scans_v4/cont_grid.py +++ b/csaxs_bec/scans/scans_v4/cont_grid.py @@ -17,6 +17,7 @@ Scan procedure: from __future__ import annotations import time +from copy import deepcopy from typing import Annotated, TypedDict import numpy as np @@ -193,19 +194,20 @@ class ContGrid(ScanBase): # Count only the end point of each line as a valid position, as the fast axis is continuously moving and only triggered at # the beginning of the line moving to the end point. positions = positions[:, ::-1] - self.positions = positions[(frames_per_trigger - 1) :: frames_per_trigger, :] # Get device specific parameters self._fetch_device_params() # Adjust relative positions if needed if self.relative: self.start_positions = self.components.get_start_positions(self.motors) - self.positions += self.start_positions + positions += self.start_positions self.fast_start += self.start_positions[0] self.fast_end += self.start_positions[0] self.stepper_start += self.start_positions[1] self.stepper_stop += self.start_positions[1] + self.positions = deepcopy(positions[(frames_per_trigger - 1) :: frames_per_trigger, :]) + # Adjust premove self.fast_start -= self._cont_motor_params["premove_distance"] self.fast_end += self._cont_motor_params["premove_distance"]