Compare commits

...

24 Commits

Author SHA1 Message Date
x12sa
5ab763ad38 test at beamline
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m28s
CI for csaxs_bec / test (push) Successful in 1m33s
2026-02-16 12:33:15 +01:00
fa35ddf1a9 refactor(eps): deactivate black for eps.py to keep signal component formatted in single line. 2026-02-16 12:33:15 +01:00
501bc52867 fix(eps): Fix tests and eps integration 2026-02-16 12:33:15 +01:00
f35c51efa7 test(eps): Add tests for EPS device 2026-02-16 12:33:15 +01:00
0c81c718d8 refactor(eps): Refactoring of EPS device from cSAXS 2026-02-16 12:33:15 +01:00
x12sa
ce88310125 fixed enable of water cooling 2026-02-16 12:33:15 +01:00
x12sa
e860571a64 water on method 2026-02-16 12:33:15 +01:00
x12sa
24bd5a71bc added water cooling channels 2026-02-16 12:33:15 +01:00
x12sa
541eb97096 added "kind" to channel list 2026-02-16 12:33:15 +01:00
x12sa
7911717142 create class dynamically, monos added 2026-02-16 12:33:15 +01:00
x12sa
bdf94533a5 first version of csaxs eps 2026-02-16 12:33:15 +01:00
5784331073 test(conftest): Add monkey patch for SocketSignal Readback_Timeout
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m35s
2026-02-16 11:10:47 +01:00
x12sa
7a2c6629f7 refactor: add gh signal for ddg1 with TTL for full acquisition
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m31s
CI for csaxs_bec / test (push) Successful in 1m33s
2026-02-16 10:18:37 +01:00
75cc672f08 tests: fix tests for ddg1
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m32s
CI for csaxs_bec / test (pull_request) Successful in 1m31s
2026-02-11 11:50:22 +01:00
67ef20cfc8 fix(ddg): fix logic to resolve trigger on ddg1.
Some checks failed
CI for csaxs_bec / test (pull_request) Failing after 1m26s
CI for csaxs_bec / test (push) Failing after 1m28s
2026-02-11 11:22:09 +01:00
f8d2af4c5b fix(ddg1): revert to shutter signal as mcs status signal is too slow
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m27s
CI for csaxs_bec / test (pull_request) Failing after 1m31s
2026-02-11 09:45:40 +01:00
8195c12a35 fix(fast-shutter): Add close shutter on 'stop' call.
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m25s
CI for csaxs_bec / test (pull_request) Failing after 1m26s
2026-02-11 08:53:00 +01:00
a45aa094ef refactor(ddg1): Use mcs acquiring status to resolve trigger of DDG1 2026-02-11 08:52:38 +01:00
x12sa
2814add2de fixes at beamline
Some checks failed
CI for csaxs_bec / test (push) Failing after 3m23s
CI for csaxs_bec / test (pull_request) Failing after 3m25s
2026-02-10 10:59:18 +01:00
32b4c39659 refactor(ddg): Add fsh signal to ddg, improve trigger logic.
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m26s
CI for csaxs_bec / test (pull_request) Failing after 1m27s
2026-02-09 14:11:00 +01:00
8849b9ffea fix(shutter); fix shutter_readback signal 2026-02-09 10:10:44 +01:00
c3aa882b1d fix(configs): add connectionTimeout=20s defaults to devices with SocketController
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m28s
CI for csaxs_bec / test (pull_request) Failing after 1m28s
2026-02-09 08:39:19 +01:00
6fad4f2034 fix(fast-shutter): Fix cSAXS fast shutter device
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m30s
CI for csaxs_bec / test (pull_request) Failing after 1m30s
2026-02-09 08:29:08 +01:00
x12sa
ed8d012632 feat: add logic for ext/en in ddg, mcs and flomni
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m24s
CI for csaxs_bec / test (pull_request) Failing after 1m33s
2026-01-30 12:55:06 +01:00
21 changed files with 1063 additions and 110 deletions

View File

@@ -37,6 +37,21 @@ mcs:
readoutPriority: monitored
softwareTrigger: false
##########################################################################
########################### FAST SHUTTER #################################
##########################################################################
fsh:
description: Fast shutter manual control and readback
deviceClass: csaxs_bec.devices.epics.fast_shutter.cSAXSFastEpicsShutter
deviceConfig:
prefix: 'X12SA-ES1-TTL:'
onFailure: raise
enabled: true
readoutPriority: monitored
##########################################################################
######################## SMARACT STAGES ##################################
##########################################################################
@@ -60,6 +75,7 @@ xbpm3x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -22.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -82,6 +98,7 @@ xbpm3y:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -2
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -104,6 +121,7 @@ sl3trxi:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -126,6 +144,7 @@ sl3trxo:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -148,6 +167,7 @@ sl3trxb:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -5.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -170,6 +190,7 @@ sl3trxt:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -192,6 +213,7 @@ fast_shutter_n1_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -7
in: 0
@@ -215,6 +237,7 @@ fast_shutter_o1_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -15.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -237,6 +260,7 @@ fast_shutter_o2_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -15.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -259,6 +283,7 @@ filter_array_1_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 25
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -281,6 +306,7 @@ filter_array_2_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 25.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -303,6 +329,7 @@ filter_array_3_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 25.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -325,6 +352,7 @@ filter_array_4_x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 25
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -347,6 +375,7 @@ sl4trxi:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -369,6 +398,7 @@ sl4trxo:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -391,6 +421,7 @@ sl4trxb:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -5.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -413,6 +444,7 @@ sl4trxt:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -425,7 +457,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
@@ -437,6 +469,7 @@ sl5trxi:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -447,7 +480,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
@@ -459,6 +492,7 @@ sl5trxo:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -469,7 +503,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
@@ -481,6 +515,7 @@ sl5trxb:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -491,7 +526,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
@@ -503,6 +538,7 @@ sl5trxt:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -513,7 +549,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
@@ -525,6 +561,7 @@ xbimtrx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: -14.7
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
@@ -535,7 +572,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
@@ -547,6 +584,7 @@ xbimtry:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
init_position: 0
# bl_smar_stage to use csaxs reference method. assign number according to axis channel

View File

@@ -89,6 +89,7 @@ xbpm2x:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 0
@@ -108,6 +109,7 @@ xbpm2y:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 1
@@ -127,6 +129,7 @@ cu_foilx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 2
@@ -146,6 +149,7 @@ scinx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 3

View File

@@ -17,6 +17,7 @@ feyex:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -16.267
out: -1
@@ -35,6 +36,7 @@ feyey:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -10.467
fheater:
@@ -52,6 +54,7 @@ fheater:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
foptx:
description: Optics X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -67,6 +70,7 @@ foptx:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -13.761
fopty:
@@ -84,6 +88,7 @@ fopty:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0.552
out: 0.752
@@ -102,6 +107,7 @@ foptz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 23
fsamroy:
@@ -119,6 +125,7 @@ fsamroy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
fsamx:
description: Sample coarse X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -134,6 +141,7 @@ fsamx:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -1.1
fsamy:
@@ -151,6 +159,7 @@ fsamy:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 2.75
ftracky:
@@ -168,6 +177,7 @@ ftracky:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
ftrackz:
description: Laser Tracker coarse Z
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -183,6 +193,7 @@ ftrackz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
ftransx:
description: Sample transer X
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -198,6 +209,7 @@ ftransx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
ftransy:
description: Sample transer Y
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -213,6 +225,7 @@ ftransy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
sensor_voltage: -2.4
ftransz:
@@ -230,6 +243,7 @@ ftransz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
ftray:
description: Sample transfer tray
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -245,6 +259,7 @@ ftray:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
############################################################
@@ -279,6 +294,7 @@ fosax:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 9.124
out: 5.3
@@ -297,6 +313,7 @@ fosay:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0.367
fosaz:
@@ -314,6 +331,7 @@ fosaz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 8.5
out: 6
@@ -334,6 +352,7 @@ rtx:
onFailure: buffer
readOnly: false
readoutPriority: on_request
connectionTimeout: 20
userParameter:
low_signal: 10000
min_signal: 9000
@@ -350,6 +369,7 @@ rty:
onFailure: buffer
readOnly: false
readoutPriority: on_request
connectionTimeout: 20
userParameter:
tomo_additional_offsety: 0
rtz:
@@ -364,6 +384,7 @@ rtz:
onFailure: buffer
readOnly: false
readoutPriority: on_request
connectionTimeout: 20
############################################################
####################### Cameras ############################
@@ -408,20 +429,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
# ############################################################
@@ -439,8 +460,8 @@ flomni_temphum:
# ########## OMNY / flOMNI / LamNI fast shutter ##############
# ############################################################
omnyfsh:
description: omnyfsh connects to read fast shutter at X12 if in that network
deviceClass: csaxs_bec.devices.omny.shutter.OMNYFastEpicsShutter
description: omnyfsh connects to fast shutter at X12 if device fsh exists
deviceClass: csaxs_bec.devices.omny.shutter.OMNYFastShutter
deviceConfig: {}
enabled: true
onFailure: buffer

View File

@@ -19,6 +19,7 @@ leyex:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 14.117
leyey:
@@ -38,6 +39,7 @@ leyey:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 48.069
out: 0.5
@@ -58,6 +60,7 @@ loptx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -0.244
out: -0.699
@@ -78,6 +81,7 @@ lopty:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 3.724
out: 3.53
@@ -98,6 +102,7 @@ loptz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
lsamrot:
description: Sample rotation
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
@@ -115,6 +120,7 @@ lsamrot:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
lsamx:
description: Sample coarse X
deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor
@@ -132,6 +138,7 @@ lsamx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
center: 8.768
lsamy:
@@ -151,6 +158,7 @@ lsamy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
center: 10.041
@@ -176,6 +184,7 @@ losax:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -1.442
losay:
@@ -195,6 +204,7 @@ losay:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -0.171
out: 3.8
@@ -215,6 +225,7 @@ losaz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -1
out: -3
@@ -238,6 +249,7 @@ rtx:
deviceTags:
- lamni
readoutPriority: baseline
connectionTimeout: 20
enabled: true
readOnly: False
rty:
@@ -255,6 +267,7 @@ rty:
deviceTags:
- lamni
readoutPriority: baseline
connectionTimeout: 20
enabled: true
readOnly: False

View File

@@ -69,6 +69,7 @@ rtx:
onFailure: buffer
readOnly: false
readoutPriority: on_request
connectionTimeout: 20
userParameter:
low_signal: 8500
min_signal: 8000
@@ -84,6 +85,7 @@ rty:
onFailure: buffer
readOnly: false
readoutPriority: on_request
connectionTimeout: 20
userParameter:
tomo_additional_offsety: 0
rtz:
@@ -98,6 +100,7 @@ rtz:
onFailure: buffer
readOnly: false
readoutPriority: on_request
connectionTimeout: 20
# ############################################################
# ##################### OMNY samples #########################
@@ -165,6 +168,7 @@ ofzpx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -0.4317
ofzpy:
@@ -184,6 +188,7 @@ ofzpy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0.7944
out: 0.6377
@@ -204,6 +209,7 @@ ofzpz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
otransx:
@@ -223,6 +229,7 @@ otransx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
otransy:
@@ -242,6 +249,7 @@ otransy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
up_position: -1.2
gripper_sensorvoltagetarget: -2.30
@@ -262,6 +270,7 @@ otransz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
osamx:
@@ -281,6 +290,7 @@ osamx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: -0.1
osamz:
@@ -300,6 +310,7 @@ osamz:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
oosay:
@@ -319,6 +330,7 @@ oosay:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
near_field_in: 0.531
far_field_in: 0.4122
@@ -339,6 +351,7 @@ oosax:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
near_field_in: 3.2044
far_field_in: 3.022
@@ -359,6 +372,7 @@ oosaz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
near_field_in: -0.4452
far_field_in: 6.5
@@ -379,6 +393,7 @@ oparkz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
oshuttleopen:
@@ -398,6 +413,7 @@ oshuttleopen:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
oshuttlealign:
@@ -417,6 +433,7 @@ oshuttlealign:
onFailure: buffer
readOnly: true
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
osamy:
@@ -436,6 +453,7 @@ osamy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
otracky:
@@ -455,6 +473,7 @@ otracky:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
start_pos: -4.3431
osamroy:
@@ -474,6 +493,7 @@ osamroy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
in: 0
otrackz:
@@ -493,6 +513,7 @@ otrackz:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
start_pos: -0.6948
oeyex:
@@ -512,6 +533,7 @@ oeyex:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
xray_in: -45.7394
oeyez:
@@ -531,6 +553,7 @@ oeyez:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
xray_in: -2
oeyey:
@@ -550,6 +573,7 @@ oeyey:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
xray_in: 0.0229
@@ -572,6 +596,7 @@ ocsx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
nothing: 0
ocsy:
@@ -589,6 +614,7 @@ ocsy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
nothing: 0
oshield:
@@ -606,5 +632,6 @@ oshield:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
userParameter:
nothing: 0

View File

@@ -1 +1,15 @@
############################################################
############################################################
##################### EPS ##################################
############################################################
x12saEPS:
description: X12SA EPS info and control
deviceClass: csaxs_bec.devices.epics.eps.EPS
deviceConfig: {}
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline

View File

@@ -64,6 +64,7 @@ npx:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
deviceTags:
- npoint
npy:
@@ -81,5 +82,6 @@ npy:
onFailure: buffer
readOnly: false
readoutPriority: baseline
connectionTimeout: 20
deviceTags:
- npoint

View File

@@ -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)

View File

@@ -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)
"""
@@ -37,7 +37,9 @@ import traceback
from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from ophyd_devices import CompareStatus, DeviceStatus, TransitionStatus
from ophyd import Component as Cpt
from ophyd import EpicsSignalRO, Kind
from ophyd_devices import CompareStatus, DeviceStatus, StatusBase, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
@@ -69,7 +71,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 +133,27 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None.
"""
USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger"]
# TODO Consider using the 'fsh' device instead.
fast_shutter_readback = Cpt(
EpicsSignalRO,
read_pv="X12SA-ES1-TTL:INP_01",
add_prefix=("",), # Add this to prevent the prefix to be added to the signal
kind=Kind.omitted,
auto_monitor=True,
)
# The shutter control PV can indicate if the shutter is requested to be kept open. If that
# is the case, we can not use the signal shutter_readback signal to check if the delay cycle
# finishes but have to use the polling of the event status register to check if the burst finished.
fast_shutter_control = Cpt(
EpicsSignalRO,
read_pv="X12SA-ES1-TTL:OUT_01",
add_prefix=("",), # Add this to prevent the prefix to be added to the signal
kind=Kind.omitted,
auto_monitor=True,
)
def __init__(
self,
name: str,
@@ -142,6 +165,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 +216,21 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
self.burst_delay.put(0)
self.burst_count.put(1)
def keep_shutter_open_during_scan(self, open: True) -> None:
"""
Method to configure the delay generator for keeping the shutter open during a scans.
This means that the additional delay to open the shutter needs to be removed (2e-3)
from the timing of the signals.
Args:
open (bool): If True, the shutter will be kept open during the scan.
If False, the shutter will be opened and closed for each trigger cycle.
"""
if open is True:
self._shutter_to_open_delay = 0
else:
self._shutter_to_open_delay = 2e-3
def on_stage(self) -> None:
"""
@@ -230,6 +269,18 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
if self.burst_count.get() != 1:
self.burst_count.put(1)
#####################################
## Setup trigger source if needed ###
#####################################
# NOTE Some scans may change the trigger source to an external trigger,
# so we will make sure that the default trigger source is set for the DDG1
# before each scan. If a scan requires a different trigger source, i.e.
# external triggers then the scan should implement this change after the
# on_stage method was called.
if self.trigger_source.get() != DEFAULT_TRIGGER_SOURCE:
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
#########################################
### Setup timing for burst and delays ###
#########################################
@@ -239,27 +290,34 @@ 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)
self.set_delay_pairs(channel="gh", delay=self._shutter_to_open_delay, width=(shutter_width-self._shutter_to_open_delay))
# 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
@@ -431,7 +489,19 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
def on_trigger(self) -> DeviceStatus:
"""
This method is called from BEC as a software trigger.
This method is called from BEC as a software trigger. Here the logic is as follows:
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:
@@ -450,33 +520,46 @@ 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: This sleep is important to ensure that the HW is ready to process new commands.
# It has been empirically determined after long testing that this improves stability.
time.sleep(0.02)
# 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,
# we can directly return.
if self.trigger_source.get() != TRIGGERSOURCE.SINGLE_SHOT.value:
status = StatusBase(obj=self)
status.set_finished()
return status
# 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")
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()
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}")
# Prepare StatusBitsCompareStatus to resolve once the END_OF_BURST bit was set.
status = self._prepare_trigger_status_event()
# Start polling thread again to monitor event status
self._start_polling()
# Trigger the DDG1
# 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:

View File

@@ -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:
"""

View File

@@ -488,6 +488,7 @@ class DelayGeneratorCSAXS(Device):
name="trigger_source",
kind=Kind.omitted,
doc="Trigger Source for the DDG, options in TRIGGERSOURCE",
auto_monitor=True,
)
trigger_level = Cpt(
EpicsSignal,

View File

@@ -0,0 +1,435 @@
"""EPS module for cSAXS beamline: defines the EPS device with its components and methods."""
# fmt: off
# Disable Black formatting for this file to preserve an easier readable structure for the component definitions.
# pylint: disable=line-too-long
from __future__ import annotations
import time
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind
from ophyd_devices import PSIDeviceBase
logger = bec_logger.logger
# ---------------------------
# Registry: sections/channels
# ---------------------------
class EPSSubDevices(Device):
"""Base class for EPS sub-device components (e.g. alarms, valves, shutters). with common methods if needed."""
def describe(self) -> dict:
desc = super().describe()
for walk in self.walk_signals():
if walk.item.attr_name not in desc:
desc[walk.item.attr_name] = walk.item.describe()
return desc
class EPSAlarms(EPSSubDevices):
"""EPS alarms at the cSAXS beamline."""
eps_alarm_cnt = Cpt(EpicsSignalRO, read_pv="X12SA-EPS-PLC:AlarmCnt_EPS", add_prefix=("",), name="eps_alarm_cnt", kind=Kind.omitted, doc="X12SA EPS Alarm count", auto_monitor=True, labels={"alarm"})
mis_alarm_cnt = Cpt(EpicsSignalRO, read_pv="ARS00-MIS-PLC-01:AlarmCnt_Frontends", add_prefix=("",), name="mis_alarm_cnt", kind=Kind.omitted, doc="FrontEnd MIS Alarm count", auto_monitor=True, labels={"alarm"})
class ValvesFrontend(EPSSubDevices):
"""Valves frontend at the cSAXS beamline."""
fe_vvpg_0000 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVPG-0000:PLC_OPEN", add_prefix=("",), name="fevvpg0000", kind=Kind.omitted, doc="FE-VVPG-0000", auto_monitor=True, labels={"valve"})
fe_vvpg_1010 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVPG-1010:PLC_OPEN", add_prefix=("",), name="fevvpg1010", kind=Kind.omitted, doc="FE-VVPG-1010", auto_monitor=True, labels={"valve"})
fe_vvfv_2010 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVFV-2010:PLC_OPEN", add_prefix=("",), name="fevvfv2010", kind=Kind.omitted, doc="FE-VVFV-2010", auto_monitor=True, labels={"valve"})
fe_vvpg_2010 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-VVPG-2010:PLC_OPEN", add_prefix=("",), name="fevvpg2010", kind=Kind.omitted, doc="FE-VVPG-2010", auto_monitor=True, labels={"valve"})
class ValvesOptics(EPSSubDevices):
"""Valves at the optics hutch."""
op_vvpg_1010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-1010:PLC_OPEN", add_prefix=("",), name="opvvpg1010", kind=Kind.omitted, doc="OP-VVPG-1010", auto_monitor=True, labels={"valve"})
op_vvpg_2010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-2010:PLC_OPEN", add_prefix=("",), name="opvvpg2010", kind=Kind.omitted, doc="OP-VVPG-2010", auto_monitor=True, labels={"valve"})
op_vvpg_3010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-3010:PLC_OPEN", add_prefix=("",), name="opvvpg3010", kind=Kind.omitted, doc="OP-VVPG-3010", auto_monitor=True, labels={"valve"})
op_vvpg_3020 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-3020:PLC_OPEN", add_prefix=("",), name="opvvpg3020", kind=Kind.omitted, doc="OP-VVPG-3020", auto_monitor=True, labels={"valve"})
op_vvpg_4010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-4010:PLC_OPEN", add_prefix=("",), name="opvvpg4010", kind=Kind.omitted, doc="OP-VVPG-4010", auto_monitor=True, labels={"valve"})
op_vvpg_5010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-5010:PLC_OPEN", add_prefix=("",), name="opvvpg5010", kind=Kind.omitted, doc="OP-VVPG-5010", auto_monitor=True, labels={"valve"})
op_vvpg_6010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-6010:PLC_OPEN", add_prefix=("",), name="opvvpg6010", kind=Kind.omitted, doc="OP-VVPG-6010", auto_monitor=True, labels={"valve"})
op_vvpg_7010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-VVPG-7010:PLC_OPEN", add_prefix=("",), name="opvvpg7010", kind=Kind.omitted, doc="OP-VVPG-7010", auto_monitor=True, labels={"valve"})
class ValvesEndstation(EPSSubDevices):
"""Endstation valves at the cSAXS beamline."""
es_vvpg_1010 = Cpt(EpicsSignalRO, read_pv="X12SA-ES-VVPG-1010:PLC_OPEN", add_prefix=("",), name="esvvpg1010", kind=Kind.omitted, doc="ES-VVPG-1010", auto_monitor=True, labels={"valve"})
class ShuttersFrontend(EPSSubDevices):
"""Shutters frontend."""
fe_psh1 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-PSH1-EMLS-0010:OPEN", add_prefix=("",), name="fepsh1", kind=Kind.omitted, doc="FE-PSH1-EMLS-0010", auto_monitor=True, labels={"shutter"})
fe_sto1 = Cpt(EpicsSignalRO, read_pv="X12SA-FE-STO1-EMLS-0010:OPEN", add_prefix=("",), name="festo1", kind=Kind.omitted, doc="FE-STO1-EMLS-0010", auto_monitor=True, labels={"shutter"})
class ShuttersEndstation(EPSSubDevices):
"""Shutters at the endstation."""
es_psh17010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-PSH1-EMLS-7010:OPEN", add_prefix=("",), name="espsh17010", kind=Kind.omitted, doc="OP-PSH1-EMLS-7010", auto_monitor=True, labels={"shutter"})
class DMMMonochromator(EPSSubDevices):
"""DMM monochromator signals at the cSAXS beamline."""
dmm_temp_surface_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3010:TEMP", add_prefix=("",), name="dmm_temp_surface_1", kind=Kind.omitted, doc="DMM Temp Surface 1", auto_monitor=True, labels={"temp"})
dmm_temp_surface_2 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3020:TEMP", add_prefix=("",), name="dmm_temp_surface_2", kind=Kind.omitted, doc="DMM Temp Surface 2", auto_monitor=True, labels={"temp"})
dmm_temp_shield_1_disaster = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3030:TEMP", add_prefix=("",), name="dmm_temp_shield_1_disaster", kind=Kind.omitted, doc="DMM Temp Shield 1 (disaster)", auto_monitor=True, labels={"temp"})
dmm_temp_shield_2_disaster = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-ETTC-3040:TEMP", add_prefix=("",), name="dmm_temp_shield_2_disaster", kind=Kind.omitted, doc="DMM Temp Shield 2 (disaster)", auto_monitor=True, labels={"temp"})
dmm_translation_thru = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3010:THRU", add_prefix=("",), name="dmm_translation_thru", kind=Kind.omitted, doc="DMM Translation ThruPos", auto_monitor=True, labels={"switch"})
dmm_translation_in = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3020:IN", add_prefix=("",), name="dmm_translation_in", kind=Kind.omitted, doc="DMM Translation InPos", auto_monitor=True, labels={"switch"})
dmm_bragg_thru = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3030:THRU", add_prefix=("",), name="dmm_bragg_thru", kind=Kind.omitted, doc="DMM Bragg ThruPos", auto_monitor=True, labels={"switch"})
dmm_bragg_in = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMLS-3040:IN", add_prefix=("",), name="dmm_bragg_in", kind=Kind.omitted, doc="DMM Bragg InPos", auto_monitor=True, labels={"switch"})
dmm_heater_fault_xtal_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMSW-3050:SWITCH", add_prefix=("",), name="dmm_heater_fault_xtal_1", kind=Kind.omitted, doc="DMM Heater Fault XTAL 1", auto_monitor=True, labels={"fault"})
dmm_heater_fault_xtal_2 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMSW-3060:SWITCH", add_prefix=("",), name="dmm_heater_fault_xtal_2", kind=Kind.omitted, doc="DMM Heater Fault XTAL 2", auto_monitor=True, labels={"fault"})
dmm_heater_fault_support_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM-EMSW-3070:SWITCH", add_prefix=("",), name="dmm_heater_fault_support_1", kind=Kind.omitted, doc="DMM Heater Fault Support 1", auto_monitor=True, labels={"fault"})
dmm_energy = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM1:ENERGY-GET", add_prefix=("",), name="dmm_energy", kind=Kind.omitted, doc="DMM Energy", auto_monitor=True, labels={"energy"})
dmm_position = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM1:POSITION", add_prefix=("",), name="dmm_position", kind=Kind.omitted, doc="DMM Position", auto_monitor=True, labels={"string"})
dmm_stripe = Cpt(EpicsSignalRO, read_pv="X12SA-OP-DMM1:STRIPE", add_prefix=("",), name="dmm_stripe", kind=Kind.omitted, doc="DMM Stripe", auto_monitor=True, labels={"string"})
class CCMMonochromator(EPSSubDevices):
"""CCM monochromator signals at the cSAXS beamline."""
ccm_temp_crystal = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-ETTC-4010:TEMP", add_prefix=("",), name="ccm_temp_crystal", kind=Kind.omitted, doc="CCM Temp Crystal", auto_monitor=True, labels={"temp"})
ccm_temp_shield_disaster = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-ETTC-4020:TEMP", add_prefix=("",), name="ccm_temp_shield_disaster", kind=Kind.omitted, doc="CCM Temp Shield (disaster)", auto_monitor=True, labels={"temp"})
ccm_heater_fault_1 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-EMSW-4010:SWITCH", add_prefix=("",), name="ccm_heater_fault_1", kind=Kind.omitted, doc="CCM Heater Fault 1", auto_monitor=True, labels={"fault"})
ccm_heater_fault_2 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-EMSW-4020:SWITCH", add_prefix=("",), name="ccm_heater_fault_2", kind=Kind.omitted, doc="CCM Heater Fault 2", auto_monitor=True, labels={"fault"})
ccm_heater_fault_3 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM-EMSW-4030:SWITCH", add_prefix=("",), name="ccm_heater_fault_3", kind=Kind.omitted, doc="CCM Heater Fault 3", auto_monitor=True, labels={"fault"})
ccm_energy = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM1:ENERGY-GET", add_prefix=("",), name="ccm_energy", kind=Kind.omitted, doc="CCM Energy", auto_monitor=True, labels={"energy"})
ccm_position = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CCM1:POSITION", add_prefix=("",), name="ccm_position", kind=Kind.omitted, doc="CCM Position", auto_monitor=True, labels={"string"})
class CoolingWater(EPSSubDevices):
"""Cooling water signals at the cSAXS beamline."""
op_sl1_efsw_2010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-SL1-EFSW-2010:FLOW", add_prefix=("",), name="op_sl1_efsw_2010_flow", kind=Kind.omitted, doc="OP-SL1-EFSW-2010", auto_monitor=True, labels={"flow"})
op_sl2_efsw_2010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-SL2-EFSW-2010:FLOW", add_prefix=("",), name="op_sl2_efsw_2010_flow", kind=Kind.omitted, doc="OP-SL2-EFSW-2010", auto_monitor=True, labels={"flow"})
op_eb1_efsw_5010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-EB1-EFSW-5010:FLOW", add_prefix=("",), name="op_eb1_efsw_5010_flow", kind=Kind.omitted, doc="OP-EB1-EFSW-5010", auto_monitor=True, labels={"flow"})
op_eb1_efsw_5020_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-EB1-EFSW-5020:FLOW", add_prefix=("",), name="op_eb1_efsw_5020_flow", kind=Kind.omitted, doc="OP-EB1-EFSW-5020", auto_monitor=True, labels={"flow"})
op_sl3_efsw_5010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-SL3-EFSW-5010:FLOW", add_prefix=("",), name="op_sl3_efsw_5010_flow", kind=Kind.omitted, doc="OP-SL3-EFSW-5010", auto_monitor=True, labels={"flow"})
op_kb_efsw_6010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-KB-EFSW-6010:FLOW", add_prefix=("",), name="op_kb_efsw_6010_flow", kind=Kind.omitted, doc="OP-KB-EFSW-6010", auto_monitor=True, labels={"flow"})
op_psh1_efsw_7010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-OP-PSH1-EFSW-7010:FLOW", add_prefix=("",), name="op_psh1_efsw_7010_flow", kind=Kind.omitted, doc="OP-PSH1-EFSW-7010", auto_monitor=True, labels={"flow"})
es_eb2_efsw_1010_flow = Cpt(EpicsSignalRO, read_pv="X12SA-ES-EB2-EFSW-1010:FLOW", add_prefix=("",), name="es_eb2_efsw_1010_flow", kind=Kind.omitted, doc="ES-EB2-EFSW-1010", auto_monitor=True, labels={"flow"})
op_cs_ecvw_0010 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CS-ECVW-0010:PLC_OPEN", add_prefix=("",), name="op_cs_ecvw_0010", kind=Kind.omitted, doc="OP-CS-ECVW-0010", auto_monitor=True, labels={"valve"})
op_cs_ecvw_0020 = Cpt(EpicsSignalRO, read_pv="X12SA-OP-CS-ECVW-0020:PLC_OPEN", add_prefix=("",), name="op_cs_ecvw_0020", kind=Kind.omitted, doc="OP-CS-ECVW-0020", auto_monitor=True, labels={"valve"})
class EPS(PSIDeviceBase):
"""EPS device for the cSAXS beamline."""
USER_ACCESS = [
"show_all",
"water_cooling_op",
]
alarms = Cpt(EPSAlarms, name="alarms", doc="EPS Alarms")
valves_frontend = Cpt(ValvesFrontend, name="valves_frontend", doc="Valves Frontend")
valves_optics = Cpt(ValvesOptics, name="valves_optics", doc="Valves Optics Hutch")
valves_es = Cpt(ValvesEndstation, name="valves_es", doc="Valves ES Hutch")
shutters_frontend = Cpt(ShuttersFrontend, name="shutters_frontend", doc="Shutters Frontend")
shutters_es = Cpt(ShuttersEndstation, name="shutters_es", doc="Shutters Endstation")
dmm_monochromator = Cpt(DMMMonochromator, name="dmm_monochromator", doc="DMM Monochromator")
ccm_monochromator = Cpt(CCMMonochromator, name="ccm_monochromator", doc="CCM Monochromator")
cooling_water = Cpt(CoolingWater, name="cooling_water", doc="Cooling Water")
# Acknowledgment signals for PLC communication (if needed for future use)
ackerr = Cpt(EpicsSignal, read_pv="X12SA-EPS-PLC:ACKERR-REQUEST", add_prefix=("",), name="ackerr", kind=Kind.omitted, doc="ACKERR request - OP-CS-ECVW-0020", auto_monitor=True, labels={"request"})
request = Cpt(EpicsSignal, read_pv="X12SA-OP-CS-ECVW:PLC_REQUEST", add_prefix=("",), name="op_cs_ecvw_request", kind=Kind.omitted, doc="PLC request - OP-CS-ECVW-PLC_REQUEST", auto_monitor=True, labels={"request"})
def _notify(self, msg: str, show_as_client_msg: bool = True):
"""Utility method to print a message, and optionally send it to the client UI if it should be shown also as a client message."""
try:
if show_as_client_msg:
self.device_manager.connector.send_client_info(msg, scope="", show_asap=True)
else:
print(msg)
except Exception:
logger.error(f"Failed to send client message, falling back to print: {msg}")
print(str(msg))
# ----------------------------------------------------------
# Water cooling operation
# ----------------------------------------------------------
def safe_get(self, sig, default=None):
"""Helper method to safely get a signal value, returning a default if there's an error."""
try:
return sig.get()
except Exception as ex:
logger.warning(f"Failed to get signal {sig.pvname}: {ex}")
return default
def water_cooling_op(self):
"""
Open ECVW valves, reset EPS alarms, monitor for 20s,
then ensure stability (valves remain open) for 10s.
All messages sent to client.
"""
POLL_PERIOD = 2
TIMEOUT = 20
STABILITY = 15
self._notify("=== Water Cooling Operation ===")
# --- Signals ---
eps_alarm_sig = self.alarms.eps_alarm_cnt
ackerr = self.ackerr
request = self.request
valves = [self.cooling_water.op_cs_ecvw_0010, self.cooling_water.op_cs_ecvw_0020]
# Flow channels list extracted from CHANNELS
flow_items = [walk.item for walk in self.cooling_water.walk_signals() if "flow" in walk.item._ophyd_labels_]
# --- Step 1: EPS alarm reset ---
alarm_value = self.safe_get(eps_alarm_sig, 0)
if alarm_value and alarm_value > 0:
self._notify(f"[WaterCooling] EPS alarms present ({alarm_value}) → resetting…")
try:
ackerr.put(1)
except Exception as ex:
self._notify(f"[WaterCooling] WARNING: ACKERR write failed: {ex}")
time.sleep(0.3)
else:
self._notify("[WaterCooling] No EPS alarms detected.")
# --- Step 2: Issue open request ---
self._notify("[WaterCooling] Sending cooling-valve OPEN request…")
try:
request.put(1)
except Exception as ex:
self._notify(f"[WaterCooling] ERROR: Failed to send OPEN request: {ex}")
return False
# --- Step 3: Monitoring loop (clean client table output) ---
start = time.time()
end = start + TIMEOUT
stable_until = None
# Print (server-side) header once
print("Monitoring valves and flow sensors...")
print(f" Valves: {valves[0].attr_name[-4:]}, {valves[1].attr_name[-4:]}")
print(f" Note: stability requires valves to remain OPEN for {STABILITY} seconds.")
# One table header to the client (via device manager)
# Fixed-width columns for alignment in monospaced UI
table_header = f"{'Time':>6} | {'Valves':<21} | {'Flows (OK/FAIL/N/A)':<20}"
self._notify(table_header)
def snapshot():
# Valve snapshot
v_states = [self.safe_get(v, None) for v in valves]
v1 = f"{valves[0].attr_name[-4:]}=" + ("OPEN " if v_states[0] is True or v_states[0] == 1 else "CLOSED" if v_states[0] is False or v_states[0] == 0 else "N/A ")
v2 = f"{valves[1].attr_name[-4:]}=" + ("OPEN " if v_states[1] is True or v_states[1] == 1 else "CLOSED" if v_states[1] is False or v_states[1] == 0 else "N/A ")
# 2 valves with a single space between => width ~ 21
valve_str = f"{v1} {v2}"
# Flow summary: OK/FAIL/N/A counts (compact)
flow_states = []
for fsig in flow_items:
fval = self.safe_get(fsig, None)
flow_states.append(True if fval in (1, True) else False if fval in (0, False) else None)
ok = sum(1 for f in flow_states if f is True)
fail = sum(1 for f in flow_states if f is False)
na = sum(1 for f in flow_states if f is None)
flow_summary = f"{ok:>2} / {fail:>2} / {na:>2}"
return v_states, valve_str, flow_summary
while True:
# TODO Consider adding a timeout to avoid infinite loop.
now = time.time()
elapsed = int(now - start)
if now > end:
# One last line to client
v_states, valves_s, flows_s = snapshot()
self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}")
print("→ TIMEOUT: Cooling valves failed to remain OPEN.")
return False
# Live snapshot
v_states, valves_s, flows_s = snapshot()
# Exactly one concise line to client per cycle
self._notify(f"{elapsed:>6}s | {valves_s:<21} | {flows_s:<20}")
both_open = all(s is not None and bool(s) for s in v_states)
if both_open:
if stable_until is None:
stable_until = now + STABILITY
print(f"[WaterCooling] Both valves OPEN → starting {STABILITY}s stability window…")
else:
if now >= stable_until:
print("→ SUCCESS: Valves remained OPEN during stability window.")
return True
else:
if stable_until is not None:
print("[WaterCooling] Valve closed again → restarting stability window.")
stable_until = None
time.sleep(POLL_PERIOD)
def show_all(self):
red = "\x1b[91m"
green = "\x1b[92m"
white = "\x1b[0m"
bold = "\x1b[1m"
cyan = "\x1b[96m"
# ---- New: enum maps for numeric -> string rendering ----
POSITION_ENUM = {0: "out of beam", 1: "in beam"}
STRIPE_ENUM = {0: "Stripe 1 W/B4C", 1: "Stripe 2 NiV/B4C"}
POSITION_ATTRS = {self.dmm_monochromator.dmm_position.attr_name, self.ccm_monochromator.ccm_position.attr_name}
STRIPE_ATTRS = {self.dmm_monochromator.dmm_stripe.attr_name}
def is_bool_like(v):
return isinstance(v, (bool, int)) and v in (0, 1, True, False)
# ---- Changed: accept attr in formatter so we can apply enum mapping ----
def fmt_value(value: any, signal: EpicsSignalRO):
if value is None:
return f"{red}MISSING{white}"
attr = signal.attr_name
# ---------- Explicit enum mappings by attribute ----------
if attr in POSITION_ATTRS:
# Position comes as numeric 0/1
try:
iv = int(value)
return POSITION_ENUM.get(iv, f"{iv}")
except Exception:
# Fallback if its already a string or unexpected
return f"{value}"
if attr in STRIPE_ATTRS:
# Stripe comes as numeric 0/1
try:
iv = int(value)
return STRIPE_ENUM.get(iv, f"{iv}")
except Exception:
return f"{value}"
# ------------------- TEMPERATURE -------------------
if "temp" in signal._ophyd_labels_ and isinstance(value, (int, float)):
return f"{value:.1f}"
# ------------------- ENERGY ------------------------
if "energy" in signal._ophyd_labels_ and isinstance(value, (int, float)):
return f"{value:.4f}"
# ------------------- STRINGS -----------------------
if "string" in signal._ophyd_labels_ or "position" in signal._ophyd_labels_:
# For other strings, just echo the value
return f"{value}"
# ------------------- SWITCH (ACTIVE/INACTIVE) ------
if "switch" in signal._ophyd_labels_ and is_bool_like(value):
return f"{green+'ACTIVE'+white if value else red+'INACTIVE'+white}"
# ------------------- FAULT (OK/FAULT) --------------
if "fault" in signal._ophyd_labels_ and is_bool_like(value):
return f"{green+'OK'+white if not value else red+'FAULT'+white}"
# ------------------- VALVE/SHUTTER -----------------
if ("valve" in signal._ophyd_labels_ or "shutter" in signal._ophyd_labels_) and is_bool_like(value):
return f"{green+'OPEN'+white if value else red+'CLOSED'+white}"
# ------------------- FLOW (OK/FAIL) ----------------
if "flow" in signal._ophyd_labels_ and is_bool_like(value):
return f"{green}OK{white}" if bool(value) else f"{red}FAIL{white}"
# ------------------- FALLBACK -----------------------
return f"{value}"
# ------------------- PRINT START ---------------------
print(f"{bold}X12SA EPS status{white}")
for name, component in self._sig_attrs.items():
sub_device = getattr(self, name)
rows = []
# Only print sub-devices, not individual request signals
if not isinstance(sub_device, Device):
continue
print(f"\n{bold}{component.doc}{white}")
for sub_walk in sub_device.walk_components():
cpt: Cpt = sub_walk.item
it: EpicsSignalRO = getattr(sub_device, cpt.attr)
val = self.safe_get(it)
rows.append((cpt.doc, val, it))
label_width = max(32, *(len(label) for (label, _, _) in rows))
for label, value, it in rows:
fv = fmt_value(value, it) # <-- pass attr to formatter
print(f" - {label:<{label_width}} {fv}")
if sub_device.attr_name == "cooling_water":
v1 = self.safe_get(self.cooling_water.op_cs_ecvw_0010)
v2 = self.safe_get(self.cooling_water.op_cs_ecvw_0020)
def closed(v):
return is_bool_like(v) and not bool(v)
if closed(v1) and closed(v2):
print(f"\n{cyan}Hint:{white} Both water cooling valves are CLOSED.\n" f"You can open them using: {bold}dev.x12saEPS.water_cooling_op(){white}")
# fmt: on
# ----------------------------------------------------------
# Consistency report
# ----------------------------------------------------------
# def consistency_report(self, *, verbose=True):
# missing = []
# dupes = []
# seen = {}
# for sub_device in self.walk_components():
# section = sub_device.name
# for walk in sub_device.walk_components():
# cpt: Cpt = walk.ancestors[-1]
# it: EpicsSignalRO = walk.item
# if not hasattr(self, it["attr"]):
# missing.append((section, it["attr"], it["label"], it["pv"]))
# pv = it["pv"]
# if pv in seen:
# dupes.append((pv, seen[pv], (section, it["attr"], it["label"])))
# else:
# seen[pv] = (section, it["attr"], it["label"])
# if verbose:
# print("=== Consistency Report ===")
# if missing:
# print("\nMissing attributes:")
# for sec, a, lbl, pv in missing:
# print(f" - [{sec}] {a} {lbl} pv={pv}")
# else:
# print("\nNo missing attributes.")
# if dupes:
# print("\nDuplicate PVs:")
# for pv, f1, f2 in dupes:
# print(f" {pv} → {f1} AND {f2}")
# else:
# print("\nNo duplicate PVs.")
# return {"missing_attrs": missing, "duplicate_pvs": dupes}

View File

@@ -0,0 +1,67 @@
"""
Shutter device for the cSAXS beamline with 2 PVs. One is connected to a
signal can be set to control the shutter signal, and the other is a readback signal
that can be monitored to check the shutter status as it may be controlled directly by
the delay generator."""
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind
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(EpicsSignalRO, "INP_01", kind=Kind.normal, auto_monitor=True)
# -----------------------------------------------------
# User-facing shutter control functions
# -----------------------------------------------------
# pylint: disable=protetced-access
def fshopen(self) -> None:
"""Open the fast shutter."""
self.shutter.set(1).wait(timeout=self.shutter._timeout) # 2s default for ES
# pylint: disable=protetced-access
def fshclose(self) -> None:
"""Close the fast shutter."""
self.shutter.set(0).wait(timeout=self.shutter._timeout) # 2s default for ES
def fshstatus(self) -> int:
"""Return the fast shutter control status (0=closed, 1=open)."""
return self.shutter.get() # Ensure we have the latest value from EPICS
def fshstatus_readback(self) -> int:
"""Return the fast shutter status (0=closed, 1=open)."""
return self.shutter_readback.get() # Ensure we have the latest value from EPICS
def fshinfo(self) -> None:
"""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}"
)
def stop(self, *, success: bool = False) -> None:
"""Stop the shutter device. Make sure to close it."""
self.shutter.put(0)
super().stop(success=success)
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:")

View File

@@ -22,7 +22,13 @@ 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 +261,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 +293,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 +370,10 @@ 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 +395,21 @@ 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":
self.erase_start.put(1)
def on_prescan(self) -> None | StatusBase:
"""
This method is called after on_stage and before the scan starts. For the MCS card, we need to make sure
that the card is properly started for fly scans. For step scans, this will be handled by the DDG,
so no action is required here.
"""
if self.scan_info.msg.scan_type == "fly":
status_acquiring = CompareStatus(self.acquiring, ACQUIRING.ACQUIRING)
self.cancel_on_stop(status_acquiring)
return status_acquiring
return None
def on_unstage(self) -> None:
"""
@@ -422,9 +447,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()
@@ -452,7 +484,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
"""Callback for status failure, the monitoring thread should be stopped."""
# NOTE Check for status.done and status.success is important to avoid
if status.done:
self._start_monitor_async_data_emission.clear() # Stop monitoring
def on_complete(self) -> CompareStatus:
@@ -478,6 +509,13 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
monitoring thread is stopped properly.
"""
# NOTE For fly scans with EXT/EN enabled triggering, the MCS card needs to receive an
# additional trigger at the end of the scan to advance the channel. This will ensure
# that the acquisition finishes on the card and that data is emitted to BEC. If the acquisition
# was already finished (i.e. normal step scan sends 1 extra pulse per burst cycle), this will
# not have any effect as the card will already be in DONE state and signal.
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))
@@ -491,7 +529,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
# Combine both statuses
ret_status = status & status_async_data
# Handle external stop/cancel, and stop monitoring
# NOTE: Handle external stop/cancel, and stop monitoring
ret_status.add_callback(self._status_failed_callback)
self.cancel_on_stop(ret_status)
return ret_status

View File

@@ -498,6 +498,9 @@ class RtFlomniController(Controller):
)
# while scan is running
while mode > 0:
#TODO here?: scan abortion if no progress in scan *raise error
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
time.sleep(0.01)
@@ -629,6 +632,8 @@ class RtFlomniMotor(Device, PositionerBase):
SUB_CONNECTION_CHANGE = "connection_change"
_default_sub = SUB_READBACK
connectionTimeout = 20
def __init__(
self,
axis_Id,

View File

@@ -1,82 +1,87 @@
import time
import socket
"""
Fast Shutter control for OMNY setup. If started with a config file in which the device_manager
has a 'fsh' device (cSAXSFastEpicsShutter), this device will be used as the shutter.
Otherwise, the device will create a dummy shutter device that will log warnings when shutter
methods are called, but will not raise exceptions.
"""
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device
from ophyd import EpicsSignal
from ophyd import Device, Signal
from ophyd_devices import PSIDeviceBase
logger = bec_logger.logger
class OMNYFastEpicsShutterError(Exception):
pass
def _detect_host_pv():
"""Detect host subnet and return appropriate PV name."""
try:
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
if local_ip.startswith("129.129.122."):
return "X12SA-ES1-TTL:OUT_01"
else:
return "XOMNYI-XEYE-DUMMYSHUTTER:0"
except Exception as ex:
print(f"Warning: could not detect IP subnet ({ex}), using dummy shutter.")
return "XOMNYI-XEYE-DUMMYSHUTTER:0"
class OMNYFastEpicsShutter(Device):
class OMNYFastShutter(PSIDeviceBase, Device):
"""
Fast EPICS shutter with automatic PV selection based on host subnet.
Fast Shutter control for OMNY setup. If started with at the beamline, it will expose
the shutter control methods (fshopen, fshclose, fshstatus, fshinfo) from the
cSAXSFastEpicsShutter device. The device is identified by the 'fsh' name in the device manager.
If the 'fsh' device is not found in the device manager, this device will create a dummy shutter
and log warnings when shutter methods are called, but will not raise exceptions.
"""
USER_ACCESS = ["fshopen", "fshclose", "fshstatus", "fshinfo", "help"]
USER_ACCESS = ["fshopen", "fshclose", "fshstatus", "fshinfo", "help", "fshstatus_readback"]
SUB_VALUE = "value"
_default_sub = SUB_VALUE
# PV is detected dynamically at import time
shutter = Cpt(EpicsSignal, name="shutter", read_pv=_detect_host_pv(), auto_monitor=True)
def __init__(self, prefix="", *, name, **kwargs):
super().__init__(prefix, name=name, **kwargs)
self.shutter.subscribe(self._emit_value)
def _emit_value(self, **kwargs):
timestamp = kwargs.pop("timestamp", time.time())
self.wait_for_connection()
self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self)
shutter = Cpt(Signal, name="shutter")
# -----------------------------------------------------
# User-facing shutter control functions
# -----------------------------------------------------
# pylint: disable=invalid-name
def _check_if_cSAXS_shutter_exists_in_config(self) -> bool:
"""
Check on the device manager if the shutter device exists.
Returns:
bool: True if the 'fsh' device exists in the device manager, False otherwise
"""
if self.device_manager.devices.get("fsh", None) is None:
logger.warning(f"Fast shutter device not found for {self.name}.")
return False
return True
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}")
if self._check_if_cSAXS_shutter_exists_in_config():
return self.device_manager.devices["fsh"].fshopen()
else:
self.shutter.put(1)
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}")
if self._check_if_cSAXS_shutter_exists_in_config():
return self.device_manager.devices["fsh"].fshclose()
else:
self.shutter.put(0)
def fshstatus(self):
"""Return the fast shutter status (0=closed, 1=open)."""
try:
if self._check_if_cSAXS_shutter_exists_in_config():
return self.device_manager.devices["fsh"].fshstatus()
else:
return self.shutter.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
print(f"Fast shutter connected to EPICS channel: {pvname}")
return pvname
if self._check_if_cSAXS_shutter_exists_in_config():
return self.device_manager.devices["fsh"].fshinfo()
else:
print("Using dummy fast shutter device. No EPICS channel is connected.")
def help(self):
"""Display available user methods."""
print("Available methods:")
for method in self.USER_ACCESS:
print(f" - {method}")
def fshstatus_readback(self):
"""Return the fast shutter status (0=closed, 1=open) from the readback signal."""
if self._check_if_cSAXS_shutter_exists_in_config():
return self.device_manager.devices["fsh"].fshstatus_readback()
else:
self.shutter.get()

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()

14
tests/conftest.py Normal file
View File

@@ -0,0 +1,14 @@
"""
Conftest runs for all tests in this directory and subdirectories. Thereby, we know for
certain that the SocketSignal.READBACK_TIMEOUT is set to 0 for all tests, which prevents
hanging tests when a readback is attempted on a non-connected socket.
"""
# conftest.py
import pytest
from ophyd_devices.utils.socket import SocketSignal
@pytest.fixture(autouse=True)
def patch_socket_timeout(monkeypatch):
monkeypatch.setattr(SocketSignal, "READBACK_TIMEOUT", 0.0)

View File

@@ -282,10 +282,11 @@ def test_ddg1_stage(mock_ddg1: DDG1):
mock_ddg1.scan_info.msg.scan_parameters["exp_time"] = exp_time
mock_ddg1.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
mock_ddg1.fast_shutter_control._read_pv.mock_data = 0 # Simulate shutter control
mock_ddg1.stage()
shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3
shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger
assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled
assert np.isclose(mock_ddg1.burst_delay.get(), 0)
@@ -302,6 +303,25 @@ def test_ddg1_stage(mock_ddg1: DDG1):
assert np.isclose(mock_ddg1.ef.width.get(), 1e-6)
assert mock_ddg1.staged == ophyd.Staged.yes
mock_ddg1.unstage()
# Test if shutter is kept open..
mock_ddg1.fast_shutter_control._read_pv.mock_data = 1 # Simulate shutter control is kept open
# Test method
mock_ddg1.keep_shutter_open_during_scan(True)
shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger
assert np.isclose(
shutter_width, exp_time * frames_per_trigger
) # Shutter to open delay is not added as shutter is kept open
# Simulate fly scan, so no extra trigger for MCS card.
mock_ddg1.scan_info.msg.scan_type = "fly"
mock_ddg1.stage()
# Shutter channel cd
assert np.isclose(mock_ddg1.cd.delay.get(), 0)
assert np.isclose(mock_ddg1.cd.width.get(), shutter_width)
# MCS channel ef or gate
assert np.isclose(mock_ddg1.ef.delay.get(), 0)
assert np.isclose(mock_ddg1.ef.width.get(), 0) # No triggering of MCS due to shutter fly scan
def test_ddg1_on_trigger(mock_ddg1: DDG1):
@@ -331,9 +351,28 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1):
#################################
with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs:
mock_prepare_mcs.return_value = ophyd.StatusBase(done=True, success=True)
# MCS card is present and enabled, should call prepare_mcs_on_trigger
# and the status should resolve once acuiring goes from 1 to 0.
status = ddg.trigger()
assert status.done is False
mcs = ddg.device_manager.devices.get("mcs", None)
assert mcs is not None
mcs.acquiring._read_pv.mock_data = 1 # Simulate acquiring started
assert status.done is False
mcs.acquiring._read_pv.mock_data = 0 # Simulate acquiring stopped
status.wait(timeout=1) # Wait for the status to be done
assert status.done is True
assert status.success is True
mock_prepare_mcs.assert_called_once()
# Now we disable the mcs card, and trigger again. This should not call prepare_mcs_on_trigger
# and should fallback to polling the DDG for END_OF_BURST status bit.
# Disable mcs card
mcs.enabled = False
status = ddg.trigger()
# Check that the poll thread run event is set
# Careful in debugger, there is a timeout based on the exp_time + 5s default
assert ddg._poll_thread_run_event.is_set()
assert not ddg._poll_thread_poll_loop_done.is_set()

View File

@@ -0,0 +1,37 @@
"""Module to test epics devices."""
import pytest
from ophyd_devices.tests.utils import patched_device
from csaxs_bec.devices.epics.fast_shutter import cSAXSFastEpicsShutter
@pytest.fixture
def fast_shutter_device():
"""Fixture to create a patched cSAXSFastEpicsShutter device for testing."""
with patched_device(cSAXSFastEpicsShutter, name="fsh", prefix="X12SA-ES1-TTL:") as device:
yield device
def test_fast_shutter_methods(fast_shutter_device):
"""Test the user-facing methods of the cSAXSFastEpicsShutter device."""
assert fast_shutter_device.name == "fsh", "Device name should be 'fsh'"
assert fast_shutter_device.prefix == "X12SA-ES1-TTL:", "Device prefix is 'X12SA-ES1-TTL:'"
# Test fshopen and fshclose
fast_shutter_device.fshopen()
assert fast_shutter_device.shutter.get() == 1, "Shutter should be open (1) after fshopen()"
assert fast_shutter_device.fshstatus() == 1, "fshstatus should return 1 when shutter is open"
fast_shutter_device.fshclose()
assert fast_shutter_device.shutter.get() == 0, "Shutter should be closed (0) after fshclose()"
assert fast_shutter_device.fshstatus() == 0, "fshstatus should return 0 when shutter is closed"
# shutter_readback is connected to separate PV.
fast_shutter_device.shutter_readback._read_pv.mock_data = 1 # Simulate readback showing open
assert (
fast_shutter_device.fshstatus_readback() == 1
), "fshstatus_readback should return 1 when shutter readback shows open"
fast_shutter_device.shutter_readback._read_pv.mock_data = 0 # Simulate readback showing closed
assert (
fast_shutter_device.fshstatus_readback() == 0
), "fshstatus_readback should return 0 when shutter readback shows closed"

View File

@@ -0,0 +1,99 @@
# pylint: skip-file
from __future__ import annotations
import pytest
from ophyd_devices.tests.utils import patched_device
from csaxs_bec.devices.epics.eps import EPS
ALL_PVS = [
# ALARMS
"X12SA-EPS-PLC:AlarmCnt_EPS",
"ARS00-MIS-PLC-01:AlarmCnt_Frontends",
# FRONTEND VALVES
"X12SA-FE-VVPG-0000:PLC_OPEN",
"X12SA-FE-VVPG-1010:PLC_OPEN",
"X12SA-FE-VVFV-2010:PLC_OPEN",
"X12SA-FE-VVPG-2010:PLC_OPEN",
# Optics VALVES
"X12SA-OP-VVPG-1010:PLC_OPEN",
"X12SA-OP-VVPG-2010:PLC_OPEN",
"X12SA-OP-VVPG-3010:PLC_OPEN",
"X12SA-OP-VVPG-3020:PLC_OPEN",
"X12SA-OP-VVPG-4010:PLC_OPEN",
"X12SA-OP-VVPG-5010:PLC_OPEN",
"X12SA-OP-VVPG-6010:PLC_OPEN",
"X12SA-OP-VVPG-7010:PLC_OPEN",
# Endstation VALVES
"X12SA-ES-VVPG-1010:PLC_OPEN",
# Frontend SHUTTERS
"X12SA-FE-PSH1-EMLS-0010:OPEN",
"X12SA-FE-STO1-EMLS-0010:OPEN",
# Optics SHUTTERS
"X12SA-OP-PSH1-EMLS-7010:OPEN",
# DMM Monochromator
"X12SA-OP-DMM-ETTC-3010:TEMP",
"X12SA-OP-DMM-ETTC-3020:TEMP",
"X12SA-OP-DMM-ETTC-3030:TEMP",
"X12SA-OP-DMM-ETTC-3040:TEMP",
"X12SA-OP-DMM-EMLS-3010:THRU",
"X12SA-OP-DMM-EMLS-3020:IN",
"X12SA-OP-DMM-EMLS-3030:THRU",
"X12SA-OP-DMM-EMLS-3040:IN",
"X12SA-OP-DMM-EMSW-3050:SWITCH",
"X12SA-OP-DMM-EMSW-3060:SWITCH",
"X12SA-OP-DMM-EMSW-3070:SWITCH",
"X12SA-OP-DMM1:ENERGY-GET",
"X12SA-OP-DMM1:POSITION",
"X12SA-OP-DMM1:STRIPE",
# CCM Monochromator
"X12SA-OP-CCM-ETTC-4010:TEMP",
"X12SA-OP-CCM-ETTC-4020:TEMP",
"X12SA-OP-CCM-EMSW-4010:SWITCH",
"X12SA-OP-CCM-EMSW-4020:SWITCH",
"X12SA-OP-CCM-EMSW-4030:SWITCH",
"X12SA-OP-CCM1:ENERGY-GET",
"X12SA-OP-CCM1:POSITION",
# Water Cooling
"X12SA-OP-SL1-EFSW-2010:FLOW",
"X12SA-OP-SL2-EFSW-2010:FLOW",
"X12SA-OP-EB1-EFSW-5010:FLOW",
"X12SA-OP-EB1-EFSW-5020:FLOW",
"X12SA-OP-SL3-EFSW-5010:FLOW",
"X12SA-OP-KB-EFSW-6010:FLOW",
"X12SA-OP-PSH1-EFSW-7010:FLOW",
"X12SA-ES-EB2-EFSW-1010:FLOW",
"X12SA-OP-CS-ECVW-0010:PLC_OPEN",
"X12SA-OP-CS-ECVW-0020:PLC_OPEN",
# Request PVs
"X12SA-EPS-PLC:ACKERR-REQUEST",
"X12SA-OP-CS-ECVW:PLC_REQUEST",
]
@pytest.fixture
def eps():
dev_name = "EPS"
with patched_device(EPS, name=dev_name) as eps:
yield eps
def test_eps_has_signals(eps):
"""Test that all expected PVs are present in the eps device."""
found_pvs = [walk.item._read_pv.pvname for walk in eps.walk_signals()]
assert set(found_pvs) == set(
ALL_PVS
), f"Expected PVs {ALL_PVS} but found {set(ALL_PVS) - set(found_pvs)}"
# pylint: disable=line-too-long
expected_show_all_output = "\x1b[1mX12SA EPS status\x1b[0m\n\n\x1b[1mEPS Alarms\x1b[0m\n - X12SA EPS Alarm count 0\n - FrontEnd MIS Alarm count 0\n\n\x1b[1mValves Frontend\x1b[0m\n - FE-VVPG-0000 \x1b[91mCLOSED\x1b[0m\n - FE-VVPG-1010 \x1b[91mCLOSED\x1b[0m\n - FE-VVFV-2010 \x1b[91mCLOSED\x1b[0m\n - FE-VVPG-2010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mValves Optics Hutch\x1b[0m\n - OP-VVPG-1010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-2010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-3010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-3020 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-4010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-5010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-6010 \x1b[91mCLOSED\x1b[0m\n - OP-VVPG-7010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mValves ES Hutch\x1b[0m\n - ES-VVPG-1010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mShutters Frontend\x1b[0m\n - FE-PSH1-EMLS-0010 \x1b[91mCLOSED\x1b[0m\n - FE-STO1-EMLS-0010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mShutters Endstation\x1b[0m\n - OP-PSH1-EMLS-7010 \x1b[91mCLOSED\x1b[0m\n\n\x1b[1mDMM Monochromator\x1b[0m\n - DMM Temp Surface 1 0.0\n - DMM Temp Surface 2 0.0\n - DMM Temp Shield 1 (disaster) 0.0\n - DMM Temp Shield 2 (disaster) 0.0\n - DMM Translation ThruPos \x1b[91mINACTIVE\x1b[0m\n - DMM Translation InPos \x1b[91mINACTIVE\x1b[0m\n - DMM Bragg ThruPos \x1b[91mINACTIVE\x1b[0m\n - DMM Bragg InPos \x1b[91mINACTIVE\x1b[0m\n - DMM Heater Fault XTAL 1 \x1b[92mOK\x1b[0m\n - DMM Heater Fault XTAL 2 \x1b[92mOK\x1b[0m\n - DMM Heater Fault Support 1 \x1b[92mOK\x1b[0m\n - DMM Energy 0.0000\n - DMM Position out of beam\n - DMM Stripe Stripe 1 W/B4C\n\n\x1b[1mCCM Monochromator\x1b[0m\n - CCM Temp Crystal 0.0\n - CCM Temp Shield (disaster) 0.0\n - CCM Heater Fault 1 \x1b[92mOK\x1b[0m\n - CCM Heater Fault 2 \x1b[92mOK\x1b[0m\n - CCM Heater Fault 3 \x1b[92mOK\x1b[0m\n - CCM Energy 0.0000\n - CCM Position out of beam\n\n\x1b[1mCooling Water\x1b[0m\n - OP-SL1-EFSW-2010 \x1b[91mFAIL\x1b[0m\n - OP-SL2-EFSW-2010 \x1b[91mFAIL\x1b[0m\n - OP-EB1-EFSW-5010 \x1b[91mFAIL\x1b[0m\n - OP-EB1-EFSW-5020 \x1b[91mFAIL\x1b[0m\n - OP-SL3-EFSW-5010 \x1b[91mFAIL\x1b[0m\n - OP-KB-EFSW-6010 \x1b[91mFAIL\x1b[0m\n - OP-PSH1-EFSW-7010 \x1b[91mFAIL\x1b[0m\n - ES-EB2-EFSW-1010 \x1b[91mFAIL\x1b[0m\n - OP-CS-ECVW-0010 \x1b[91mCLOSED\x1b[0m\n - OP-CS-ECVW-0020 \x1b[91mCLOSED\x1b[0m\n\n\x1b[96mHint:\x1b[0m Both water cooling valves are CLOSED.\nYou can open them using: \x1b[1mdev.x12saEPS.water_cooling_op()\x1b[0m\n"
def test_eps_show_all(eps, capsys):
"""Test that the show_all method outputs the expected status."""
eps.show_all()
output = capsys.readouterr().out
assert (
output == expected_show_all_output
), f"Expected output does not match actual output.\nExpected:\n{expected_show_all_output}\nActual:\n{output}"