From 14d0937edc591fcc7286d394a7a4bb6aaee0414e Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 18 Jun 2026 13:01:31 +0200 Subject: [PATCH 1/3] feat: add uind_mode device --- xtreme_bec/devices/uind_mode.py | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 xtreme_bec/devices/uind_mode.py diff --git a/xtreme_bec/devices/uind_mode.py b/xtreme_bec/devices/uind_mode.py new file mode 100644 index 0000000..75f1e5f --- /dev/null +++ b/xtreme_bec/devices/uind_mode.py @@ -0,0 +1,71 @@ +from enum import Enum +from typing import Literal + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, Kind, StatusBase + + +class UIndModeEnum(str, Enum): + OFF = "Off" + LINEAR_H = "Linear H" + LINEAR_V_PLUS = "Linear V+" + LINEAR_V_MINUS = "Linear V-" + CIRCULAR_PLUS = "Circular+" + CIRCULAR_MINUS = "Circular-" + PLUS_45_DEG = "+45 deg" + MINUS_45_DEG = "-45 deg" + + +class UIndMode(Device): + USER_ACCESS = ["set_mode"] + + value = Cpt(EpicsSignal, "X07MA-UIND:MODE", auto_monitor=True, kind=Kind.hinted) + value_string = Cpt(EpicsSignal, "X07MA-UIND:MODE", auto_monitor=True, string=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.value.name = self.name + + def set_mode( + self, + mode: ( + Literal[ + "Off", + "Linear H", + "Linear V+", + "Linear V-", + "Circular+", + "Circular-", + "+45 deg", + "-45 deg", + ] + | int + ), + ) -> StatusBase: + """ + Set the undulator polarization mode. + + Args: + mode (Literal["Off", "Linear H", "Linear V+", "Linear V-", "Circular+", "Circular-", "+45 deg", "-45 deg"] | int): The desired polarization mode, either as a string or an integer index. + """ + if isinstance(mode, str): + try: + mode_enum = UIndModeEnum(mode) + mode_val = mode_enum.value + except ValueError: + # pylint: disable=raise-missing-from + raise ValueError( + f"Invalid mode string: {mode}. Valid options are: {[m.value for m in UIndModeEnum]}" + ) + elif isinstance(mode, int): + try: + mode_enum = list(UIndModeEnum)[mode] + mode_val = mode_enum.value + except IndexError: + # pylint: disable=raise-missing-from + raise ValueError( + f"Invalid mode index: {mode}. Valid indices are: 0 to {len(UIndModeEnum) - 1}, corresponding to: {[m.value for m in UIndModeEnum]}" + ) + else: + raise TypeError("Mode must be either a string or an integer index.") + return self.value.set(mode_val) -- 2.54.0 From 25c866965735716668d22c3dc66ead8b79bf3831 Mon Sep 17 00:00:00 2001 From: x07ma Date: Thu, 18 Jun 2026 13:13:24 +0200 Subject: [PATCH 2/3] chore: add configs --- xtreme_bec/device_configs/endstation.yaml | 102 +++++++++ xtreme_bec/device_configs/frontend.yaml | 209 ++++++++++++++++++ xtreme_bec/device_configs/machine.yaml | 49 ++++ xtreme_bec/device_configs/main.yaml | 12 + xtreme_bec/device_configs/optics.yaml | 10 + .../device_configs/x07ma_base_config.yaml | 18 -- 6 files changed, 382 insertions(+), 18 deletions(-) create mode 100644 xtreme_bec/device_configs/endstation.yaml create mode 100644 xtreme_bec/device_configs/frontend.yaml create mode 100644 xtreme_bec/device_configs/machine.yaml create mode 100644 xtreme_bec/device_configs/main.yaml create mode 100644 xtreme_bec/device_configs/optics.yaml diff --git a/xtreme_bec/device_configs/endstation.yaml b/xtreme_bec/device_configs/endstation.yaml new file mode 100644 index 0000000..f8bf0c6 --- /dev/null +++ b/xtreme_bec/device_configs/endstation.yaml @@ -0,0 +1,102 @@ +valve: + readoutPriority: baseline + description: 'Endstation valve' + deviceClass: xtreme_bec.devices.VacuumValve + deviceConfig: + prefix: 'X07MA-OP-VG13:' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +goldmesh1: + readoutPriority: baseline + description: 'Gold mesh 1' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MA-OP-IO1:TR1' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +goldmesh2: + readoutPriority: baseline + description: 'Gold mesh 2' + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: 'X07MA-OP-IO2:TR1' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +field_x: + readoutPriority: baseline + description: 'Magnetic field x' + deviceClass: xtreme_bec.devices.X07MAMagnetAxis + deviceConfig: + axis_id: X + ps_prefix: "X07MA-PC-PS2:" + prefix: 'X07MA-PC-MAG:' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +field_z: + readoutPriority: baseline + description: 'Magnetic field z' + deviceClass: xtreme_bec.devices.X07MAMagnetAxis + deviceConfig: + axis_id: Z + ps_prefix: "X07MA-PC-PS1:" + prefix: 'X07MA-PC-MAG:' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +keithley_1: + readoutPriority: baseline + description: 'Keithley 1 / 428 tey' + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: 'X07MA-PC-K428:1:setGain' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +keithley_2: + readoutPriority: baseline + description: 'Keithley 2 / 428 i0' + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: 'X07MA-PC-K428:2:setGain' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +keithley_3: + readoutPriority: baseline + description: 'Keithley 3 / 428 diode' + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: 'X07MA-PC-K428:3:setGain' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +temperature: + readoutPriority: baseline + description: 'Temperature controller' + deviceClass: xtreme_bec.devices.X07MATemperatureController + deviceConfig: + prefix: 'X07MA-PC-TC:' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false \ No newline at end of file diff --git a/xtreme_bec/device_configs/frontend.yaml b/xtreme_bec/device_configs/frontend.yaml new file mode 100644 index 0000000..529c199 --- /dev/null +++ b/xtreme_bec/device_configs/frontend.yaml @@ -0,0 +1,209 @@ +idgap: + description: 'Motor to control the IDGap of X07MA' + deviceClass: ophyd_devices.devices.undulator.UndulatorGap + deviceConfig: + prefix: 'X07MA-UIND:' + onFailure: retry + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + +uind_mode: + description: 'UIND mode of X07MA' + deviceClass: xtreme_bec.devices.uind_mode.UIndMode + deviceConfig: + onFailure: retry + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + +uind_harmonic: + readoutPriority: baseline + description: 'Undulator harmonic' + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: 'X07MA-UIND:HARMONIC' + auto_monitor: true + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +uind_energy: + readoutPriority: baseline + description: 'Undulator energy' + deviceClass: ophyd.EpicsSignalRO + deviceConfig: + read_pv: 'X07MA-UIND:ENERGY-RBV' + auto_monitor: true + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + + +energy: + readoutPriority: baseline + description: 'Energy in eV' + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: 'X07MA-PHS-E:GO.A' + auto_monitor: true + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +energy_mode: + readoutPriority: baseline + description: 'Energy mode. Either PGM = 0 or PGM+ID = 1' + deviceClass: ophyd.EpicsSignal + deviceConfig: + read_pv: 'X07MA-PHS-E:OPT' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false + +sl1xi: + description: 'slit 1 (frontend) x ring' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:TRXR' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1xo: + description: 'slit 1 (frontend) x wall' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:TRXW' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1yb: + description: 'slit 1 (frontend) y bottom' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:TRYB' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1yt: + description: 'slit 1 (frontend) y top' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:TRYT' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1xc: + description: 'slit 1 (frontend) x center' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:CENTERX' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1xs: + description: 'slit 1 (frontend) x size' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:SIZEX' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1yc: + description: 'slit 1 (frontend) y center' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:CENTERY' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +sl1ys: + description: 'slit 1 (frontend) y size' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-SL1:SIZEY' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + + +xbpm1x: + description: 'X-ray BPM1 in frontend translation x' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-XBPM1:TRX' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend + +xbpm1y: + description: 'X-ray BPM1 in frontend translation y' + deviceClass: ophyd_devices.EpicsMotorEC + deviceConfig: + prefix: 'X07MA-FE-XBPM1:TRY' + onFailure: raise + enabled: true + readoutPriority: baseline + readOnly: false + softwareTrigger: false + deviceTags: + - xtreme + - frontend \ No newline at end of file diff --git a/xtreme_bec/device_configs/machine.yaml b/xtreme_bec/device_configs/machine.yaml new file mode 100644 index 0000000..2e988a4 --- /dev/null +++ b/xtreme_bec/device_configs/machine.yaml @@ -0,0 +1,49 @@ +############################################## +################## Machine ################### +############################################## + +curr: + description: SLS ring current + deviceClass: ophyd_devices.EpicsSignalRO + deviceConfig: + read_pv: AGEBD-PARAMS:CURRENT + auto_monitor: true + onFailure: buffer + enabled: true + readoutPriority: monitored + readOnly: true + softwareTrigger: false + +ofb: + description: SLS OFB status + deviceClass: ophyd_devices.EpicsSignalRO + deviceConfig: + read_pv: ARSGE-CECL-FOFB1:FOFB-OPERATE + auto_monitor: true + onFailure: buffer + enabled: true + readoutPriority: baseline + readOnly: true + softwareTrigger: false + +shutter: + description: X07MA shutter status + deviceClass: ophyd_devices.EpicsSignalRO + deviceConfig: + read_pv: X07M-OP-PSYS:SH-A-OPEN + auto_monitor: true + onFailure: buffer + enabled: true + readoutPriority: baseline + readOnly: true + softwareTrigger: false + +operator: + description: SLS operator messages + deviceClass: ophyd_devices.devices.sls_devices.SLSOperatorMessages + deviceConfig: + onFailure: buffer + enabled: true + readoutPriority: on_request + readOnly: true + softwareTrigger: false \ No newline at end of file diff --git a/xtreme_bec/device_configs/main.yaml b/xtreme_bec/device_configs/main.yaml new file mode 100644 index 0000000..5470ce9 --- /dev/null +++ b/xtreme_bec/device_configs/main.yaml @@ -0,0 +1,12 @@ + +machine: + - !include ./machine.yaml + +frontend: + - !include ./frontend.yaml + +optics: + - !include ./optics.yaml + +endstation: + - !include ./endstation.yaml \ No newline at end of file diff --git a/xtreme_bec/device_configs/optics.yaml b/xtreme_bec/device_configs/optics.yaml new file mode 100644 index 0000000..80c8690 --- /dev/null +++ b/xtreme_bec/device_configs/optics.yaml @@ -0,0 +1,10 @@ +mono: + readoutPriority: baseline + description: 'PGM Monochromator' + deviceClass: xtreme_bec.devices.PGMMonochromator + deviceConfig: + prefix: 'X07MA-' + onFailure: retry + enabled: true + readOnly: false + softwareTrigger: false \ No newline at end of file diff --git a/xtreme_bec/device_configs/x07ma_base_config.yaml b/xtreme_bec/device_configs/x07ma_base_config.yaml index 1e4615b..5bd3cf3 100644 --- a/xtreme_bec/device_configs/x07ma_base_config.yaml +++ b/xtreme_bec/device_configs/x07ma_base_config.yaml @@ -243,22 +243,4 @@ tcontrol: readOnly: false softwareTrigger: false -sls_info: - readoutPriority: on_request - description: 'sls info' - deviceClass: ophyd_devices.SLSInfo - deviceConfig: {} - onFailure: buffer - enabled: true - readOnly: false - softwareTrigger: false -sls_operator: - readoutPriority: on_request - description: 'sls operator messages' - deviceClass: ophyd_devices.SLSOperatorMessages - deviceConfig: {} - onFailure: buffer - enabled: true - readOnly: false - softwareTrigger: false -- 2.54.0 From 7a374ac272026af8fc8dc89780a9cfb3cf47defc Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 18 Jun 2026 14:21:23 +0200 Subject: [PATCH 3/3] feat: add pgmotf device --- xtreme_bec/devices/pgm_otf.py | 146 ++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 xtreme_bec/devices/pgm_otf.py diff --git a/xtreme_bec/devices/pgm_otf.py b/xtreme_bec/devices/pgm_otf.py new file mode 100644 index 0000000..56d2419 --- /dev/null +++ b/xtreme_bec/devices/pgm_otf.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind +from ophyd_devices import AsyncSignal, DeviceStatus, PSIDeviceBase, StatusBase, TransitionStatus + + +class PGMOtfControl(Device): + """ + Ophyd device for controlling the PGM OTF (On-The-Fly) scan. + """ + + e1 = Cpt(EpicsSignal, "E1", kind=Kind.config) + e2 = Cpt(EpicsSignal, "E2", kind=Kind.config) + time = Cpt(EpicsSignal, "TIME", kind=Kind.config) + folder = Cpt(EpicsSignal, "FOLDER", kind=Kind.config) + file = Cpt(EpicsSignal, "FILE", kind=Kind.config) + acquire = Cpt(EpicsSignal, "START", auto_monitor=True) + raw_edata = Cpt(EpicsSignalRO, "EDATA", kind=Kind.hinted, auto_monitor=True) + raw_data = Cpt(EpicsSignalRO, "DATA", kind=Kind.hinted, auto_monitor=True) + raw_idata = Cpt(EpicsSignalRO, "IDATA", kind=Kind.hinted, auto_monitor=True) + raw_fdata = Cpt(EpicsSignalRO, "FDATA", kind=Kind.hinted, auto_monitor=True) + count = Cpt(EpicsSignalRO, "COUNT", kind=Kind.omitted, auto_monitor=True) + + +class PGMOtf(PSIDeviceBase, PGMOtfControl): + """ + Ophyd device for controlling the PGM OTF (On-The-Fly) scan. + Inherits from PSIDeviceBase and PGMOtfControl. + """ + + edata = Cpt( + AsyncSignal, + name="raw_edata", + ndim=1, + async_update={"type": "replace", "max_shape": [None]}, + max_size=1000, + docs="Aggregated edata signal from the PGM OTF scan.", + ) + data = Cpt( + AsyncSignal, + name="raw_data", + ndim=1, + async_update={"type": "replace", "max_shape": [None]}, + max_size=1000, + docs="Aggregated data signal from the PGM OTF scan.", + ) + idata = Cpt( + AsyncSignal, + name="raw_idata", + ndim=1, + async_update={"type": "replace", "max_shape": [None]}, + max_size=1000, + docs="Aggregated idata signal from the PGM OTF scan.", + ) + fdata = Cpt( + AsyncSignal, + name="raw_fdata", + ndim=1, + async_update={"type": "replace", "max_shape": [None]}, + max_size=1000, + docs="Aggregated fdata signal from the PGM OTF scan.", + ) + + # FIXME: Once we have the proper energy readback, we should set up the progress update + # to use the energy progress between e1 and e2. + # energy = Cpt(EpicsSignalRO, "ENERGY_RBV", kind=Kind.omitted, auto_monitor=True) + # progress = Cpt(ProgressSignal) + + ######################################## + # Beamline Specific Implementations # + ######################################## + + def on_init(self) -> None: + """ + Called when the device is initialized. + + No signals are connected at this point. If you like to + set default values on signals, please use on_connected instead. + """ + self.complete_status = None + + def on_connected(self) -> None: + """ + Called after the device is connected and its signals are connected. + Default values for signals should be set here. + """ + self.raw_edata.subscribe(self._update_data, run=False) + self.raw_data.subscribe(self._update_data, run=False) + self.raw_idata.subscribe(self._update_data, run=False) + self.raw_fdata.subscribe(self._update_data, run=False) + + def on_stage(self) -> DeviceStatus | StatusBase | None: + """ + Called while staging the device. + + Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object. + """ + if self.acquire.get() != 0: + raise RuntimeError("PGM OTF scan is already running. Please stop it before staging.") + + self.complete_status = TransitionStatus(self.acquire, transitions=[0, 1, 0]) + self.cancel_on_stop(self.complete_status) + + def on_unstage(self) -> DeviceStatus | StatusBase | None: + """Called while unstaging the device.""" + if self.complete_status is not None and not self.complete_status.done: + self.complete_status.set_finished() + self.acquire.put(0) + + self.complete_status = None + + def on_pre_scan(self) -> DeviceStatus | StatusBase | None: + """Called right before the scan starts on all devices automatically.""" + + def on_trigger(self) -> DeviceStatus | StatusBase | None: + """Called when the device is triggered.""" + + def on_complete(self) -> DeviceStatus | StatusBase | None: + """Called to inquire if a device has completed a scans.""" + return self.complete_status + + def on_kickoff(self) -> DeviceStatus | StatusBase | None: + """Called to kickoff a device for a fly scan. Has to be called explicitly.""" + self.acquire.put(1, use_complete=True) + + def on_stop(self) -> None: + """Called when the device is stopped.""" + self.acquire.put(0) + + def on_destroy(self) -> None: + """Called when the device is destroyed. Cleanup resources here.""" + + def _update_data(self, value, obj, **kwargs): + """ + Callback function to update the aggregated data signals when the raw signals change. + """ + match obj.name: + case "raw_edata": + self.edata.put(value) + case "raw_data": + self.data.put(value) + case "raw_idata": + self.idata.put(value) + case "raw_fdata": + self.fdata.put(value) -- 2.54.0