diff --git a/debye_bec/device_configs/x01da_test_config.yaml b/debye_bec/device_configs/x01da_test_config.yaml index 4a9b128..ce7048c 100644 --- a/debye_bec/device_configs/x01da_test_config.yaml +++ b/debye_bec/device_configs/x01da_test_config.yaml @@ -17,3 +17,13 @@ dummy_pv: onFailure: retry enabled: true softwareTrigger: false +nidaq: + readoutPriority: async + description: NIDAQ backend for data reading for debye scans + deviceClass: debye_bec.devices.nidaq.NIDAQ + deviceConfig: + prefix: "X01DA-PC-SCANSERVER:" + onFailure: retry + enabled: true + softwareTrigger: false + diff --git a/debye_bec/devices/nidaq.py b/debye_bec/devices/nidaq.py new file mode 100644 index 0000000..8bf15d0 --- /dev/null +++ b/debye_bec/devices/nidaq.py @@ -0,0 +1,196 @@ +import enum + +from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase, CustomDetectorMixin +from ophyd import Device, Kind, DeviceStatus, Component as Cpt +from ophyd import EpicsSignal, EpicsSignalRO +from ophyd_devices.sim.sim_signals import SetableSignal +from bec_lib.logger import bec_logger + +logger = bec_logger.logger + +class NidaqError(Exception): + """ Nidaq specific error""" + +class NIDAQCompression(str, enum.Enum): + """ Options for Compression""" + OFF = 0 + ON = 1 + +class ScanType(int, enum.Enum): + """ Triggering options of the backend""" + TRIGGERED = 0 + CONTINUOUS = 1 + +class NidaqState(int, enum.Enum): + """ Possible States of the NIDAQ backend""" + DISABLED = 0 + STANDBY = 1 + PRERUN = 2 + ACQUIRING = 3 + STOPPED = 4 + ERRORSTOP = 5 + +class ScanRates(int, enum.Enum): + """ Sampling Rate options for the backend, in kHZ and MHz""" + HUNDRED_KHZ = 0 + FIVE_HUNDRED_KHZ = 1 + ONE_MHZ = 2 + FIVE_MHZ = 3 + TEN_MHZ = 4 + ELEVEN_ONE_MHZ = 5 + TWELVE_FIVE_MHZ = 6 + FOURTEEN_THREE_MTHZ = 7 + +class ReadoutRange(int, enum.Enum): + """ReadoutRange in +-V""" + ONE_V = 0 + TWO_V = 1 + FIVE_V = 2 + TEN_V = 3 + +class EncoderTypes(int, enum.Enum): + """ Encoder Types""" + TWO_PULSE_COUNTING = 0 + X_1 = 1 + X_2 = 2 + X_4 = 3 + +class NIDAQCustomMixin(CustomDetectorMixin): + """ NIDAQ Custom Mixin class to implement the device and beamline-specific actions + to the psidetectorbase class via custom_prepare methods""" + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + super().__init__(args=_args, parent=parent, kwargs=_kwargs) + self.timeout_wait_for_signal = 5 # put 5s firsts + self.valid_scan_names = ["xas_simple_scan", + "xas_simple_scan_with_xrd", + "xas_advanced_scan", + "xas_advanced_scan_with_xrd"] + + def _check_if_scan_name_is_valid(self) -> bool: + """ Check if the scan is within the list of scans for which the backend is working""" + scan_name = self.parent.scaninfo.scan_msg.content["info"].get("scan_name", "") + if scan_name in self.valid_scan_names: + return True + return False + + def on_connection_established(self) -> None: + """Method called once wait_for_connection is called on the parent class. + This should be used to implement checks that require the device to be connected, i.e. setting standard pvs. + """ + if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)], + timeout = self.timeout_wait_for_signal, + check_stopped=True): + raise NidaqError(f"Device {self.parent.name} has not been reached in state STANDBY, current state {NidaqState(self.parent.state.get())}") + self.parent.encoder_type.set(EncoderTypes.X_1).wait() + self.parent.readout_range.set(ReadoutRange.TEN_V).wait() + self.parent.scan_type.set(ScanType.TRIGGERED).wait() + # TODO, not working with scan_duration 0 for the moment to be fixed once solved + self.parent.scan_duration.set(999999).wait() + # To be checked as default + self.parent.scan_rate.set(ScanRates.TEN_MHZ).wait() + + def on_stop(self): + """ Stop the NIDAQ backend""" + self.parent.stop_call.set(1).wait() + + def on_complete(self) -> None | DeviceStatus: + """ Complete actions. For the NIDAQ we use this method to stop the backend since it + would not stop by itself in its current implementation since the number of points are not predefined. + """ + if not self._check_if_scan_name_is_valid(): + return None + self.on_stop() + #TODO check if this wait can be removed. We are waiting in unstage anyways which will always be called afterwards + # Wait for device to be stopped + status = self.wait_with_status(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)], + check_stopped= True, + timeout=self.timeout_wait_for_signal, + ) + return status + + def on_stage(self): + """ Prepare the device for the upcoming acquisition. If the upcoming scan is not in the list + of valid scans, return immediately. """ + if not self._check_if_scan_name_is_valid(): + return None + if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)], + timeout = self.timeout_wait_for_signal, + check_stopped=True): + raise NidaqError(f"Device {self.parent.name} has not been reached in state STANDBY, current state {NidaqState(self.parent.state.get())}") + self.parent.scan_type.set(ScanType.TRIGGERED).wait() + # TODO, not working with scan_duration 0 for the moment to be fixed once solved + self.parent.scan_duration.set(999999).wait() + self.parent.arm.set(1).wait() + if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.PRERUN)], + timeout = self.timeout_wait_for_signal, + check_stopped=True, + ): + raise NidaqError(f"Device {self.parent.name} has not been reached in state PRERUN, current state {NidaqState(self.parent.state.get())}") + self.parent.trigger_call.set(1).wait() + logger.info(f"Device {self.parent.name} was staged: {NidaqState(self.parent.state.get())}") + + def on_pre_scan(self) -> None: + """ Execute time critical actions. Here we ensure that the NIDAQ master task is running + before the motor starts its oscillation. This is needed for being properly homed. + The NIDAQ should go into Acquiring mode. """ + if not self._check_if_scan_name_is_valid(): + return None + if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.ACQUIRING)], + timeout = self.timeout_wait_for_signal, + check_stopped=True, + ): + raise NidaqError(f"Device {self.parent.name} failed to reach state ACQUIRING during pre scan, current state {NidaqState(self.parent.state.get())}") + logger.info(f"Device {self.parent.name} ready to take data after pre_scan: {NidaqState(self.parent.state.get())}") + + def on_unstage(self) -> None: + """ Unstage actions, the NIDAQ has to be in STANDBY state.""" + if not self.wait_for_signals(signal_conditions=[(self.parent.state.get, NidaqState.STANDBY)], + timeout = self.timeout_wait_for_signal, + check_stopped=False): + raise NidaqError(f"Device {self.parent.name} has not been reached in state STANDBY, current state {NidaqState(self.parent.state.get())}") + logger.info(f"Device {self.parent.name} was unstaged: {NidaqState(self.parent.state.get())}") + + +class NIDAQ(PSIDetectorBase): + """ NIDAQ ophyd wrapper around the NIDAQ backend currently running at x01da-cons-05 + + Args: + prefix (str) : Prefix to the NIDAQ soft ioc, currently X01DA-PC-SCANSERVER: + name (str) : Name of the device + kind (Kind) : Ophyd Kind of the device + parent (Device) : Parent clas + device_manager : device manager as forwarded by BEC + """ + encoder_angle = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_1 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_2 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_3 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_4 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_5 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_6 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_7 = Cpt(SetableSignal,value=0, kind=Kind.normal) + signal_8 = Cpt(SetableSignal,value=0, kind=Kind.normal) + + + enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config) + trigger_call = Cpt(EpicsSignal, suffix="NIDAQ-Trigger", kind=Kind.config) + arm = Cpt(EpicsSignal, suffix="NIDAQ-Arm", kind = Kind.config) + state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind= Kind.config) + server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config) + compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config) + scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config) + sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config) + scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config) + readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config) + encoder_type = Cpt(EpicsSignal, suffix="NIDAQ-EncoderType", kind=Kind.config) + stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config) + + # ai_channels_6396 = # To be added NIDAQ-AIChans + # ci_channels_6396 = # NIDAQ-CIChans6396 + # ci_channels_6614 = # NIDAQ-CIChans6614 + + custom_prepare_cls = NIDAQCustomMixin + + def __init__(self, prefix="", *, name, kind=None, parent=None, device_manager=None, **kwargs): + super().__init__(prefix, name=name, kind=kind, parent=parent, device_manager=device_manager, **kwargs)