21 Commits

Author SHA1 Message Date
x01da
efd8842540 do not close pilatus curtain after measurements
All checks were successful
CI for debye_bec / test (push) Successful in 1m1s
CI for debye_bec / test (pull_request) Successful in 1m3s
2026-03-02 13:28:24 +01:00
x01da
fd1626fbcd nidaq improvement on_stage 2026-03-02 13:27:51 +01:00
x01da
062df3171b add additional signals for xrd tigger information 2026-03-02 13:27:51 +01:00
x01da
37a268fe7b add additional CI channels for NIDAQ. Add enable PV for dead time correction 2026-03-02 13:27:51 +01:00
x01da
e9e7d84e60 reworked function to get rid of (potentionally infinite) loop. 2026-03-02 13:27:51 +01:00
x01da
1c0c9ad53e add opencv dependency for hutch cameras 2026-03-02 13:27:51 +01:00
x01da
faeb991b75 create hutch camera class 2026-03-02 13:27:51 +01:00
x01da
7377613213 add config signals 2026-03-02 13:27:51 +01:00
x01da
d8383d3b73 add string representation of signals. add Pips class/device 2026-03-02 13:27:51 +01:00
x01da
c3bfab2056 add string representation of signals 2026-03-02 13:27:51 +01:00
x01da
0261c601ff uncomment Pilatus-Sample distance 2026-03-02 13:27:51 +01:00
x01da
d3dc130f11 uncomment ionization chamber and add pips diode 2026-03-02 13:27:51 +01:00
x01da
ed1e5a027f add hutch cameras to config 2026-03-02 13:27:51 +01:00
x01da
df2961ce8e add hutch cameras config 2026-03-02 13:27:51 +01:00
x01da
e179fc1a07 add gas sensors to config 2026-03-02 13:27:51 +01:00
60d1dfc5af Update repo with template version v1.2.8
All checks were successful
CI for debye_bec / test (pull_request) Successful in 1m4s
CI for debye_bec / test (push) Successful in 1m2s
2026-02-27 15:49:26 +01:00
3e2e37908b Update repo with template version v1.2.7
Some checks failed
CI for debye_bec / test (push) Failing after 0s
CI for debye_bec / test (pull_request) Failing after 0s
2026-02-27 12:11:40 +01:00
804a731181 test(pilatus): Fix on_complete callback for pilatus
All checks were successful
CI for debye_bec / test (pull_request) Successful in 1m0s
CI for debye_bec / test (push) Successful in 1m4s
2025-12-05 14:18:46 +01:00
99f6192f37 refactor: deprecate duplicate Status implementation.
Some checks failed
CI for debye_bec / test (push) Failing after 1m2s
CI for debye_bec / test (pull_request) Failing after 1m9s
2025-11-30 22:29:23 +01:00
0a8272685d fix(status): cleanup and remove of old status usage 2025-11-30 22:28:34 +01:00
c6ed27966c fix(status): fix compare and transition status occurences
Some checks failed
CI for debye_bec / test (push) Failing after 1m9s
CI for debye_bec / test (pull_request) Failing after 3m33s
2025-11-26 13:46:53 +01:00
21 changed files with 681 additions and 425 deletions

View File

@@ -2,7 +2,7 @@
# It is needed to track the repo template version, and editing may break things.
# This file will be overwritten by copier on template updates.
_commit: v1.2.2
_commit: v1.2.8
_src_path: https://github.com/bec-project/plugin_copier_template.git
make_commit: false
project_name: debye_bec

View File

@@ -28,7 +28,7 @@ on:
description: "Python version to use"
required: false
type: string
default: "3.11"
default: "3.12"
permissions:
pull-requests: write
@@ -44,7 +44,19 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ inputs.PYTHON_VERSION || '3.11' }}"
python-version: "${{ inputs.PYTHON_VERSION || '3.12' }}"
- name: Checkout BEC Plugin Repository
uses: actions/checkout@v4
with:
repository: bec/debye_bec
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
path: ./debye_bec
- name: Lint for merge conflicts from template updates
shell: bash
# Find all Copier conflicts except this line
run: '! grep -r "<<<<<<< before updating" | grep -v "grep -r \"<<<<<<< before updating"'
- name: Checkout BEC Core
uses: actions/checkout@v4
@@ -67,13 +79,6 @@ jobs:
ref: "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}"
path: ./bec_widgets
- name: Checkout BEC Plugin Repository
uses: actions/checkout@v4
with:
repository: bec/debye_bec
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
path: ./debye_bec
- name: Install dependencies
shell: bash
run: |

View File

@@ -0,0 +1,62 @@
name: Create template upgrade PR for debye_bec
on:
workflow_dispatch:
permissions:
pull-requests: write
jobs:
create_update_branch_and_pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install tools
run: |
pip install copier PySide6
- name: Checkout
uses: actions/checkout@v4
- name: Perform update
run: |
git config --global user.email "bec_ci_staging@psi.ch"
git config --global user.name "BEC automated CI"
branch="chore/update-template-$(python -m uuid)"
echo "switching to branch $branch"
git checkout -b $branch
echo "Running copier update..."
output="$(copier update --trust --defaults --conflict inline 2>&1)"
echo "$output"
msg="$(printf '%s\n' "$output" | head -n 1)"
if ! grep -q "make_commit: true" .copier-answers.yml ; then
echo "Autocommit not made, committing..."
git add -A
git commit -a -m "$msg"
fi
if diff-index --quiet HEAD ; then
echo "No changes detected"
exit 0
fi
git push -u origin $branch
curl -X POST "https://gitea.psi.ch/api/v1/repos/${{ gitea.repository }}/pulls" \
-H "Authorization: token ${{ secrets.CI_REPO_WRITE }}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Template: $(echo $msg)\",
\"body\": \"This PR was created by Gitea Actions\",
\"head\": \"$(echo $branch)\",
\"base\": \"main\"
}"

View File

@@ -1,7 +0,0 @@
include:
- file: /templates/plugin-repo-template.yml
inputs:
name: debye_bec
target: debye_bec
branch: $CHILD_PIPELINE_BRANCH
project: bec/awi_utils

View File

@@ -386,4 +386,64 @@ es_light_toggle:
read_pv: "X01DA-EH-LIGHT:TOGGLE"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_o2:
readoutPriority: baseline
description: ES Gas Sensor O2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-O2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_h2s:
readoutPriority: baseline
description: ES Gas Sensor H2S
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-H2S"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_no2:
readoutPriority: baseline
description: ES Gas Sensor NO2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-NO2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_co:
readoutPriority: baseline
description: ES Gas Sensor CO
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-CO"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_h2:
readoutPriority: baseline
description: ES Gas Sensor H2
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-H2"
onFailure: retry
enabled: true
softwareTrigger: false
es_gas_sensor_nh3:
readoutPriority: baseline
description: ES Gas Sensor NH3
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-KIMESSA2:EH-NH3"
onFailure: retry
enabled: true
softwareTrigger: false

View File

@@ -0,0 +1,34 @@
###################################
## Hutch Cameras ##
###################################
hutch_cam_1:
readoutPriority: baseline
description: Hutch Camera 1
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085420"
onFailure: retry
enabled: true
softwareTrigger: false
hutch_cam_2:
readoutPriority: baseline
description: Hutch Camera 2
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085436"
onFailure: retry
enabled: true
softwareTrigger: false
hutch_cam_3:
readoutPriority: baseline
description: Hutch Camera 3
deviceClass: debye_bec.devices.cameras.hutch_cam.HutchCam
deviceConfig:
prefix: "pcp085435"
onFailure: retry
enabled: true
softwareTrigger: false

View File

@@ -51,7 +51,7 @@ optics_config:
## Experimental Hutch ##
###################################
## NIDAQ
# ## NIDAQ
nidaq:
readoutPriority: monitored
description: NIDAQ backend for data reading for debye scans
@@ -67,8 +67,13 @@ xas_config:
- !include ./x01da_xas.yaml
## XRD (Pilatus, pinhole, beamstop)
xrd_config:
- !include ./x01da_xrd.yaml
#xrd_config:
# - !include ./x01da_xrd.yaml
# Commented out because too slow
## Hutch cameras
# hutch_cams:
# - !include ./x01da_hutch_cameras.yaml
## Remaining experimental hutch
es_config:

View File

@@ -3,35 +3,45 @@
## Ionization Chambers ##
###################################
# ic0:
# readoutPriority: baseline
# description: Ionization chamber 0
# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0
# deviceConfig:
# prefix: "X01DA-"
# onFailure: retry
# enabled: true
# softwareTrigger: false
ic0:
readoutPriority: baseline
description: Ionization chamber 0
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber0
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
# ic1:
# readoutPriority: baseline
# description: Ionization chamber 1
# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1
# deviceConfig:
# prefix: "X01DA-"
# onFailure: retry
# enabled: true
# softwareTrigger: false
ic1:
readoutPriority: baseline
description: Ionization chamber 1
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber1
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
# ic2:
# readoutPriority: baseline
# description: Ionization chamber 2
# deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2
# deviceConfig:
# prefix: "X01DA-"
# onFailure: retry
# enabled: true
# softwareTrigger: false
ic2:
readoutPriority: baseline
description: Ionization chamber 2
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.IonizationChamber2
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
pips:
readoutPriority: baseline
description: Pips diode
deviceClass: debye_bec.devices.ionization_chambers.ionization_chamber.Pips
deviceConfig:
prefix: "X01DA-"
onFailure: retry
enabled: true
softwareTrigger: false
###################################
## Reference Foil Changer ##

View File

@@ -86,7 +86,7 @@ pilatus_curtain:
softwareTrigger: false
pilatus:
readoutPriority: async
readoutPriority: baseline
description: Pilatus
deviceClass: debye_bec.devices.pilatus.pilatus.Pilatus
deviceTags:
@@ -97,12 +97,12 @@ pilatus:
enabled: true
softwareTrigger: true
# sampl_pil:
# readoutPriority: baseline
# description: Sample to pilatus distance
# deviceClass: ophyd.EpicsSignalRO
# deviceConfig:
# read_pv: "X01DA-SAMPL-PIL"
# onFailure: retry
# enabled: true
# softwareTrigger: false
pilatus_smpl:
readoutPriority: baseline
description: Sample to pilatus distance
deviceClass: ophyd.EpicsSignalRO
deviceConfig:
read_pv: "X01DA-ES2-DET:SMPLDIST"
onFailure: retry
enabled: true
softwareTrigger: false

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from ophyd import ADBase
from ophyd import ADBase, EpicsSignalRO
from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt
from ophyd_devices import PreviewSignal
@@ -20,6 +20,16 @@ if TYPE_CHECKING: # pragma: no cover
class BaslerCamBase(ADBase):
"""BaslerCam Base class."""
cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True)
_default_configuration_attrs = [
'cam1.acquire_time',
'cam1.detector_state',
'cam_detector_state_string',
'cam1.gain',
'cam1.model',
]
cam1 = ADCpt(AravisDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:")

View File

@@ -0,0 +1,79 @@
"""EH Hutch Cameras"""
from __future__ import annotations
import cv2
import threading
from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from bec_lib.file_utils import get_full_path
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices import DeviceStatus
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
from bec_lib.messages import ScanStatusMessage
logger = bec_logger.logger
CAM_USERNAME = "camera_user"
CAM_PASSWORD = "camera_user1"
CAM_PORT = 554
class HutchCam(PSIDeviceBase):
"""Class for the Hutch Cameras"""
# image = Cpt(Signal, name='image', kind='config')
def __init__(self, *, name: str, prefix: str = "", scan_info: ScanInfo | None = None, **kwargs):
super().__init__(name=name, scan_info=scan_info, **kwargs)
self.hostname = prefix
self.status = None
# pylint: disable=E1101
def on_connected(self) -> None:
"""
Called after the device is connected and its signals are connected.
Default values for signals should be set here.
"""
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
if not cap.isOpened():
logger.error(self, "Connection Failed", "Could not connect to the camera stream.")
return
cap.release()
def on_stage(self) -> DeviceStatus:
"""Called while staging the device."""
scan_msg: ScanStatusMessage = self.scan_info.msg
file_path = get_full_path(scan_msg, name='hutch_cam_' + self.hostname).removesuffix('h5')
self.status = DeviceStatus(self)
thread = threading.Thread(target=self._save_picture, args=(file_path, self.status), daemon=True)
thread.start()
return self.status
def _save_picture(self, file_path, status):
try:
logger.info(f'Capture from camera {self.hostname}')
rtsp_url = f"rtsp://{CAM_USERNAME}:{CAM_PASSWORD}@{self.hostname}.psi.ch:{CAM_PORT}/rtpstream/config1"
cap = cv2.VideoCapture(f"{rtsp_url}?tcp")
if not cap.isOpened():
logger.error("Connection Failed", "Could not connect to the camera stream.")
return
logger.info(f'Connection to camera {self.hostname} established')
ret, frame = cap.readAsync()
cap.release()
if not ret:
logger.error("Capture Failed", "Failed to capture image from camera.")
return
cv2.imwrite(file_path + 'png', frame)
status.set_finished()
logger.info(f'Capture from camera {self.hostname} done')
except Exception as e:
status.set_exception(e)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from ophyd import ADBase
from ophyd import ADBase, EpicsSignalRO
from ophyd import ADComponent as ADCpt
from ophyd import Component as Cpt
from ophyd_devices import PreviewSignal
@@ -20,6 +20,16 @@ if TYPE_CHECKING: # pragma: no cover
class ProsilicaCamBase(ADBase):
"""Base class for Prosilica cameras."""
cam_detector_state_string = Cpt(EpicsSignalRO, suffix="cam1:DetectorState_RBV", string=True)
_default_configuration_attrs = [
'cam1.acquire_time',
'cam1.detector_state',
'cam_detector_state_string',
'cam1.gain',
'cam1.model',
]
cam1 = ADCpt(ProsilicaDetectorCam, "cam1:")
image1 = ADCpt(ImagePlugin_V35, "image1:")

View File

@@ -9,8 +9,7 @@ from ophyd import Component as Cpt
from ophyd import Device
from ophyd import DynamicDeviceComponent as Dcpt
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd.status import DeviceStatus, SubscriptionStatus
from ophyd_devices import CompareStatus, TransitionStatus
from ophyd_devices import CompareStatus, DeviceStatus, SubscriptionStatus, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from typeguard import typechecked
@@ -34,22 +33,24 @@ class EpicsSignalSplit(EpicsSignal):
class GasMixSetupControl(Device):
"""GasMixSetup Control for Inonization Chamber 0"""
gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="config", doc="Gas 1 requirement")
gas1_req = Cpt(EpicsSignalWithRBV, suffix="Gas1Req", kind="omitted", doc="Gas 1 requirement")
conc1_req = Cpt(
EpicsSignalWithRBV, suffix="Conc1Req", kind="config", doc="Concentration 1 requirement"
EpicsSignalWithRBV, suffix="Conc1Req", kind="omitted", doc="Concentration 1 requirement"
)
gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="config", doc="Gas 2 requirement")
gas2_req = Cpt(EpicsSignalWithRBV, suffix="Gas2Req", kind="omitted", doc="Gas 2 requirement")
conc2_req = Cpt(
EpicsSignalWithRBV, suffix="Conc2Req", kind="config", doc="Concentration 2 requirement"
EpicsSignalWithRBV, suffix="Conc2Req", kind="omitted", doc="Concentration 2 requirement"
)
press_req = Cpt(
EpicsSignalWithRBV, suffix="PressReq", kind="config", doc="Pressure requirement"
EpicsSignalWithRBV, suffix="PressReq", kind="omitted", doc="Pressure requirement"
)
fill = Cpt(EpicsSignal, suffix="Fill", kind="config", doc="Fill the chamber")
status = Cpt(EpicsSignalRO, suffix="Status", kind="config", doc="Status")
gas1 = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1")
gas1_string = Cpt(EpicsSignalRO, suffix="Gas1", kind="config", doc="Gas 1", string=True)
conc1 = Cpt(EpicsSignalRO, suffix="Conc1", kind="config", doc="Concentration 1")
gas2 = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2")
gas2_string = Cpt(EpicsSignalRO, suffix="Gas2", kind="config", doc="Gas 2", string=True)
conc2 = Cpt(EpicsSignalRO, suffix="Conc2", kind="config", doc="Concentration 2")
press = Cpt(EpicsSignalRO, suffix="PressTransm", kind="config", doc="Current Pressure")
@@ -85,10 +86,25 @@ class IonizationChamber0(PSIDeviceBase):
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES1-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
@@ -276,10 +292,25 @@ class IonizationChamber1(IonizationChamber0):
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
@@ -312,10 +343,25 @@ class IonizationChamber2(IonizationChamber0):
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = Cpt(GasMixSetupControl, suffix=f"ES-GMES:IC{num-1}")
gmes_status = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
gmes_status_msg = Cpt(EpicsSignalRO, suffix="ES-GMES:StatusMsg0", kind="config", doc="Status")
hv = Cpt(HighVoltageSuppliesControl, suffix=f"ES2-IC{num-1}:")
hv_en_signals = {
"ext_ena": (
@@ -326,3 +372,63 @@ class IonizationChamber2(IonizationChamber0):
"ena": (EpicsSignal, "ES2-IC12:HV-Ena", {"kind": "config", "doc": "Enable signal of HV"}),
}
hv_en = Dcpt(hv_en_signals)
class Pips(IonizationChamber0):
"""Pips, prefix should be 'X01DA-'."""
USER_ACCESS = ["set_gain", "set_filter"]
num = 4
amp_signals = {
"cOnOff": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}"},
),
"cGain_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}"},
),
"cFilter_ENUM": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}"},
),
"cOnOff_string": (
EpicsSignal,
(f"ES:AMP5004.cOnOff{num}"),
{"kind": "config", "doc": f"Enable ch{num} -> IC{num-1}", "string": True},
),
"cGain_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cGain{num}_ENUM"),
{"kind": "config", "doc": f"Gain of ch{num} -> IC{num-1}", "string": True},
),
"cFilter_ENUM_string": (
EpicsSignalWithRBV,
(f"ES:AMP5004:cFilter{num}_ENUM"),
{"kind": "config", "doc": f"Filter of ch{num} -> IC{num-1}", "string": True},
),
}
amp = Dcpt(amp_signals)
gmes = None
gmes_status_msg = None
hv = None
hv_en_signals = None
hv_en = None
@typechecked
def set_hv(self, *_) -> None:
"""Not available for the PIPS"""
return None
@typechecked
def set_grid(self, *_) -> None:
"""Not available for the PIPS"""
return None
@typechecked
def fill(self, *_) -> None:
"""Not available for the PIPS"""
return None

View File

@@ -9,16 +9,15 @@ used to ensure that the action is executed completely. This is believed
to allow for a more stable execution of the action."""
import time
from typing import Any, Literal
from typing import Literal
from bec_lib.devicemanager import ScanInfo
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import DeviceStatus, Signal, StatusBase
from ophyd.status import SubscriptionStatus, WaitTimeoutError
from ophyd import DeviceStatus, StatusBase
from ophyd.status import WaitTimeoutError
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.utils.errors import DeviceStopError
from pydantic import BaseModel, Field
from typeguard import typechecked
@@ -281,7 +280,7 @@ class Mo1Bragg(PSIDeviceBase, Mo1BraggPositioner):
self.scan_control.scan_status,
transitions=[ScanControlScanStatus.READY, ScanControlScanStatus.RUNNING],
strict=True,
raise_states=[ScanControlScanStatus.PARAMETER_WRONG],
failure_states=[ScanControlScanStatus.PARAMETER_WRONG],
)
self.cancel_on_stop(status)
start_func(1)

View File

@@ -1,12 +1,11 @@
from __future__ import annotations
import time
from typing import TYPE_CHECKING, Literal, cast
from typing import TYPE_CHECKING, Literal
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Kind, StatusBase
from ophyd.status import SubscriptionStatus, WaitTimeoutError
from ophyd.status import WaitTimeoutError
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from ophyd_devices.sim.sim_signals import SetableSignal
@@ -147,6 +146,76 @@ class NidaqControl(Device):
doc="EPICS counter input 7",
auto_monitor=True,
)
ci8 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI8",
kind=Kind.normal,
doc="EPICS counter input 8",
auto_monitor=True,
)
ci9 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI9",
kind=Kind.normal,
doc="EPICS counter input 9",
auto_monitor=True,
)
ci10 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI10",
kind=Kind.normal,
doc="EPICS counter input 0",
auto_monitor=True,
)
ci11 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI11",
kind=Kind.normal,
doc="EPICS counter input 1",
auto_monitor=True,
)
ci12 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI12",
kind=Kind.normal,
doc="EPICS counter input 2",
auto_monitor=True,
)
ci13 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI13",
kind=Kind.normal,
doc="EPICS counter input 3",
auto_monitor=True,
)
ci14 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI14",
kind=Kind.normal,
doc="EPICS counter input 4",
auto_monitor=True,
)
ci15 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI15",
kind=Kind.normal,
doc="EPICS counter input 5",
auto_monitor=True,
)
ci16 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI16",
kind=Kind.normal,
doc="EPICS counter input 6",
auto_monitor=True,
)
ci17 = Cpt(
EpicsSignalRO,
suffix="NIDAQ-CI17",
kind=Kind.normal,
doc="EPICS counter input 7",
auto_monitor=True,
)
di0 = Cpt(
EpicsSignalRO,
@@ -276,6 +345,36 @@ class NidaqControl(Device):
ci7_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7, MEAN"
)
ci8_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8, MEAN"
)
ci9_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9, MEAN"
)
ci10_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10, MEAN"
)
ci11_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11, MEAN"
)
ci12_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12, MEAN"
)
ci13_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13, MEAN"
)
ci14_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14, MEAN"
)
ci15_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15, MEAN"
)
ci16_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16, MEAN"
)
ci17_mean = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17, MEAN"
)
ci0_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 0. STD"
@@ -301,12 +400,44 @@ class NidaqControl(Device):
ci7_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 7. STD"
)
ci8_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 8. STD"
)
ci9_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 9. STD"
)
ci10_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 10. STD"
)
ci11_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 11. STD"
)
ci12_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 12. STD"
)
ci13_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 13. STD"
)
ci14_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 14. STD"
)
ci15_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 15. STD"
)
ci16_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 16. STD"
)
ci17_std_dev = Cpt(
SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream counter input 17. STD"
)
xas_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XAS timestamp")
xrd_timestamp = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD timestamp")
xrd_angle = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD angle")
xrd_energy = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD energy")
xrd_ai0_mean = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 mean")
xrd_ai0_std_dev = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream XRD ai0 std dev")
di0_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 0, MAX")
di1_max = Cpt(SetableSignal, value=0, kind=Kind.normal, doc="NIDAQ stream digital input 1, MAX")
@@ -321,16 +452,21 @@ class NidaqControl(Device):
### Control PVs ###
enable_compression = Cpt(EpicsSignal, suffix="NIDAQ-EnableRLE", kind=Kind.config)
enable_dead_time_correction = Cpt(EpicsSignal, suffix="NIDAQ-EnableDTC", kind=Kind.config)
kickoff_call = Cpt(EpicsSignal, suffix="NIDAQ-Kickoff", kind=Kind.config)
stage_call = Cpt(EpicsSignal, suffix="NIDAQ-Stage", kind=Kind.config)
state = Cpt(EpicsSignal, suffix="NIDAQ-FSMState", kind=Kind.config, auto_monitor=True)
server_status = Cpt(EpicsSignalRO, suffix="NIDAQ-ServerStatus", kind=Kind.config)
compression_ratio = Cpt(EpicsSignalRO, suffix="NIDAQ-CompressionRatio", kind=Kind.config)
scan_type = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config)
scan_type_string = Cpt(EpicsSignal, suffix="NIDAQ-ScanType", kind=Kind.config, string=True)
sampling_rate = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config)
sampling_rate_string = Cpt(EpicsSignal, suffix="NIDAQ-SamplingRateRequested", kind=Kind.config, string=True)
scan_duration = Cpt(EpicsSignal, suffix="NIDAQ-SamplingDuration", kind=Kind.config)
readout_range = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config)
readout_range_string = Cpt(EpicsSignal, suffix="NIDAQ-ReadoutRange", kind=Kind.config, string=True)
encoder_factor = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config)
encoder_factor_string = Cpt(EpicsSignal, suffix="NIDAQ-EncoderFactor", kind=Kind.config, string=True)
stop_call = Cpt(EpicsSignal, suffix="NIDAQ-Stop", kind=Kind.config)
power = Cpt(EpicsSignal, suffix="NIDAQ-Power", kind=Kind.config)
heartbeat = Cpt(EpicsSignal, suffix="NIDAQ-Heartbeat", kind=Kind.config, auto_monitor=True)
@@ -358,7 +494,7 @@ class Nidaq(PSIDeviceBase, NidaqControl):
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
self.scan_info: ScanInfo
self.timeout_wait_for_signal = 5 # put 5s firsts
self._timeout_wait_for_pv = 3 # 3s timeout for pv calls
self._timeout_wait_for_pv = 5 # 5s timeout for pv calls. editted due to timeout issues persisting
self.valid_scan_names = [
"xas_simple_scan",
"xas_simple_scan_with_xrd",
@@ -557,7 +693,11 @@ class Nidaq(PSIDeviceBase, NidaqControl):
# Stage call to IOC
status = CompareStatus(self.state, NidaqState.STAGE)
self.cancel_on_stop(status)
self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv)
# TODO 11.11.25/HS64
# Switched from set to put in the hope to get rid of the rare event where nidaq is stopped at the start of a scan
# Problems consistently persisting, testing changing back to set, unconvinced this is the actual cause 14.11.25/AHC
# self.stage_call.set(1).wait(timeout=self._timeout_wait_for_pv)
self.stage_call.put(1)
status.wait(timeout=self.timeout_wait_for_signal)
if self.scan_info.msg.scan_name != "nidaq_continuous_scan":
status = self.on_kickoff()

View File

@@ -6,23 +6,21 @@ import enum
import threading
import time
import traceback
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Tuple
import numpy as np
from bec_lib.file_utils import get_full_path
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import EpicsSignal, Kind
from ophyd import EpicsSignal, EpicsSignalRO, Kind
from ophyd.areadetector.cam import ADBase, PilatusDetectorCam
from ophyd.areadetector.plugins import HDF5Plugin_V22 as HDF5Plugin
from ophyd.areadetector.plugins import ImagePlugin_V22 as ImagePlugin
from ophyd.status import WaitTimeoutError
from ophyd_devices import CompareStatus, DeviceStatus, FileEventSignal, PreviewSignal
from ophyd_devices import AndStatus, CompareStatus, DeviceStatus, FileEventSignal, PreviewSignal
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from pydantic import BaseModel, Field
from debye_bec.devices.pilatus.utils import AndStatusWithList
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import ScanInfo
from bec_lib.messages import DevicePreviewMessage, ScanStatusMessage
@@ -147,6 +145,19 @@ class Pilatus(PSIDeviceBase, ADBase):
# USER_ACCESS = ["start_live_mode", "stop_live_mode"]
cam_gain_menu_string = Cpt(EpicsSignalRO, suffix='cam1:GainMenu', string=True)
_default_configuration_attrs = [
'cam.threshold_energy',
'cam.threshold_auto_apply',
'cam.gain_menu',
'cam_gain_menu_string',
'cam.pixel_cut_off',
'cam.acquire_time',
'cam.num_exposures',
'cam.model',
]
cam = Cpt(PilatusDetectorCam, "cam1:")
hdf = Cpt(HDF5Plugin, "HDF1:")
image1 = Cpt(ImagePlugin, "image1:")
@@ -205,22 +216,11 @@ class Pilatus(PSIDeviceBase, ADBase):
PreviewSignal,
name="preview",
ndim=2,
num_rotation_90=0, # Check this
num_rotation_90=3,
doc="Preview signal for the Pilatus Detector",
)
file_event = Cpt(FileEventSignal, name="file_event")
@property
def baseline_signals(self):
"""Define baseline signals"""
return [
self.cam.acquire_time,
self.cam.num_exposures,
self.cam.threshold_energy,
self.cam.gain_menu,
self.cam.pixel_cut_off,
]
def __init__(
self,
*,
@@ -360,78 +360,59 @@ class Pilatus(PSIDeviceBase, ADBase):
# f"Live Mode on detector {self.name} did not stop: {content} after 10s."
# )
def check_detector_stop_running_acquisition(self) -> AndStatusWithList:
def check_detector_stop_running_acquisition(self) -> AndStatus:
"""Check if the detector is still running an acquisition."""
status_acquire = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_writing = CompareStatus(self.hdf.capture, ACQUIREMODE.DONE.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.UNARMED.value)
status = AndStatusWithList(
device=self, status_list=[status_acquire, status_writing, status_cam_server]
)
status = status_acquire & status_writing & status_cam_server
return status
def _calculate_trigger(self, scan_msg: ScanStatusMessage):
def _calculate_trigger(self, scan_msg: ScanStatusMessage) -> Tuple[float, float]:
self._update_scan_parameter()
total_osc = 0
calc_duration = 0
total_trig_lo = 0
total_trig_hi = 0
calc_duration = 0
n_trig_lo = 1
n_trig_hi = 1
init_lo = 1
init_hi = 1
lo_done = 0
hi_done = 0
if not self.scan_parameter.break_enable_low:
lo_done = 1
if not self.scan_parameter.break_enable_high:
hi_done = 1
start_time = time.time()
while True:
# TODO, we should not use infinite loops, for now let's add the escape Timeout of 20s, but should eventually be reviewed.
if time.time() - start_time > 20:
raise RuntimeError(
f"Calculating the number of triggers for scan {scan_msg.scan_name} took more than 20 seconds, aborting."
)
# Switching high/low is intended as angle is inverse to energy and settings in BEC are always in energy
loc_break_enable_low = self.scan_parameter.break_enable_high
loc_break_time_low = self.scan_parameter.break_time_high
loc_cycle_low = self.scan_parameter.cycle_high
loc_break_enable_high = self.scan_parameter.break_enable_low
loc_break_time_high = self.scan_parameter.break_time_low
loc_cycle_high = self.scan_parameter.cycle_low
if not loc_break_enable_low:
loc_break_time_low = 0
loc_cycle_low = 1
if not loc_break_enable_high:
loc_break_time_high = 0
loc_cycle_high = 1
total_osc = self.scan_parameter.scan_duration / (
self.scan_parameter.scan_time +
loc_break_time_low / (2 * loc_cycle_low) +
loc_break_time_high / (2 * loc_cycle_high)
)
total_osc = np.ceil(total_osc)
total_osc = total_osc + total_osc % 2 # round up to the next even number
if loc_break_enable_low:
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
if loc_break_enable_high:
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high
if calc_duration < self.scan_parameter.scan_duration:
# Due to inaccuracy in formula, this can happen, we then need to manually add two oscillations and recalculate the triggers
total_osc = total_osc + 2
calc_duration = calc_duration + 2 * self.scan_parameter.scan_time
if loc_break_enable_low:
total_trig_lo = np.floor(total_osc / (2 * loc_cycle_low))
if loc_break_enable_high:
total_trig_hi = np.floor(total_osc / (2 * loc_cycle_high))
calc_duration = total_osc * self.scan_parameter.scan_time + total_trig_lo * loc_break_time_low + total_trig_hi * loc_break_time_high
if self.scan_parameter.break_enable_low and n_trig_lo >= self.scan_parameter.cycle_low:
n_trig_lo = 1
calc_duration = calc_duration + self.scan_parameter.break_time_low
if init_lo:
lo_done = 1
init_lo = 0
else:
n_trig_lo += 1
if (
self.scan_parameter.break_enable_high
and n_trig_hi >= self.scan_parameter.cycle_high
):
n_trig_hi = 1
calc_duration = calc_duration + self.scan_parameter.break_time_high
if init_hi:
hi_done = 1
init_hi = 0
else:
n_trig_hi += 1
if lo_done and hi_done:
n = np.floor(self.scan_parameter.scan_duration / calc_duration)
total_osc = total_osc * n
if self.scan_parameter.break_enable_low:
total_trig_lo = n + 1
if self.scan_parameter.break_enable_high:
total_trig_hi = n + 1
calc_duration = calc_duration * n
lo_done = 0
hi_done = 0
if calc_duration >= self.scan_parameter.scan_duration:
break
return total_trig_lo + total_trig_hi
return total_trig_lo, total_trig_hi
########################################
# Beamline Specific Implementations #
@@ -484,6 +465,14 @@ class Pilatus(PSIDeviceBase, ADBase):
"""
# self.stop_live_mode() # Make sure that live mode is stopped if scan runs
# If user has activated alignment mode on qt panel, switch back to multitrigger and stop acquisition
if self.cam.trigger_mode.get() != TRIGGERMODE.MULT_TRIGGER.value:
self.cam.trigger_mode.set(TRIGGERMODE.MULT_TRIGGER.value).wait(5)
if self.cam.acquire.get() == ACQUIREMODE.ACQUIRING.value:
self.cam.acquire.put(0)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.DONE.value)
status_cam.wait(timeout=5)
scan_msg: ScanStatusMessage = self.scan_info.msg
if scan_msg.scan_name in self.xas_xrd_scan_names:
self._update_scan_parameter()
@@ -562,9 +551,7 @@ class Pilatus(PSIDeviceBase, ADBase):
status_hdf = CompareStatus(self.hdf.capture, ACQUIREMODE.ACQUIRING.value)
status_cam = CompareStatus(self.cam.acquire, ACQUIREMODE.ACQUIRING.value)
status_cam_server = CompareStatus(self.cam.armed, DETECTORSTATE.ARMED.value)
status = AndStatusWithList(
device=self, status_list=[status_hdf, status_cam, status_cam_server]
)
status = status_hdf & status_cam & status_cam_server
self.cam.acquire.put(1)
self.hdf.capture.put(1)
self.cancel_on_stop(status)
@@ -593,15 +580,15 @@ class Pilatus(PSIDeviceBase, ADBase):
scan_msg.scan_name in self.xas_xrd_scan_names or scan_msg.scan_type == "step"
): # TODO how to deal with fly scans?
if status.success:
status.device.file_event.put(
file_path=status.device._full_path, # pylint: disable:protected-access
self.file_event.put(
file_path=self._full_path,
done=True,
successful=True,
hinted_h5_entries={"data": "/entry/data/data"},
)
else:
status.device.file_event.put(
file_path=status.device._full_path, # pylint: disable:protected-access
self.file_event.put(
file_path=self._full_path,
done=True,
successful=False,
hinted_h5_entries={"data": "/entry/data/data"},
@@ -622,15 +609,12 @@ class Pilatus(PSIDeviceBase, ADBase):
# For long scans, it can be that the mono will execute one cycle more,
# meaning a few more XRD triggers will be sent
status_img_written = CompareStatus(
self.hdf.num_captured, self.n_images, operation=">="
self.hdf.num_captured, self.n_images, operation_success=">="
)
else:
status_img_written = CompareStatus(self.hdf.num_captured, self.n_images)
status_img_written = CompareStatus(self.hdf.num_captured, self.n_images)
status = AndStatusWithList(
device=self,
status_list=[status_hdf, status_cam, status_img_written, status_cam_server],
)
status = status_hdf & status_cam & status_img_written & status_cam_server
status.add_callback(self._complete_callback) # Callback that writing was successful
self.cancel_on_stop(status)
return status

View File

@@ -1,243 +0,0 @@
"""Temporary utility module for Status Object implementations."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ophyd import Device, DeviceStatus, StatusBase
class AndStatusWithList(DeviceStatus):
"""
Custom implementation of the AndStatus that combines the
option to add multiple statuses as a list, and in addition
allows for adding the Device as an object to access its
methods.
Args"""
def __init__(
self,
device: Device,
status_list: StatusBase | DeviceStatus | list[StatusBase | DeviceStatus],
**kwargs,
):
self.all_statuses = status_list if isinstance(status_list, list) else [status_list]
super().__init__(device=device, **kwargs)
self._trace_attributes["all"] = [st._trace_attributes for st in self.all_statuses]
def inner(status):
with self._lock:
if self._externally_initiated_completion:
return
if self.done: # Return if status is already done.. It must be resolved already
return
for st in self.all_statuses:
with st._lock:
if st.done and not st.success:
self.set_exception(st.exception()) # st._exception
return
if all(st.done for st in self.all_statuses) and all(
st.success for st in self.all_statuses
):
self.set_finished()
for st in self.all_statuses:
with st._lock:
st.add_callback(inner)
# TODO improve __repr__ and __str__
def __repr__(self):
return "<AndStatusWithList({self.all_statuses!r})>".format(self=self)
def __str__(self):
return "<AndStatusWithList(done={self.done}, success={self.success})>".format(self=self)
def __contains__(self, status: StatusBase | DeviceStatus) -> bool:
for child in self.all_statuses:
if child == status:
return True
if isinstance(child, AndStatusWithList):
if status in child:
return True
return False
# # TODO Check if this actually works....
# def set_exception(self, exc):
# # Propagate the exception to all sub-statuses that are not done yet.
#
# with self._lock:
# if self._externally_initiated_completion:
# return
# if self.done: # Return if status is already done.. It must be resolved already
# return
# super().set_exception(exc)
# for st in self.all_statuses:
# with st._lock:
# if not st.done:
# st.set_exception(exc)
def _run_callbacks(self):
"""
Set the Event and run the callbacks.
"""
if self.timeout is None:
timeout = None
else:
timeout = self.timeout + self.settle_time
if not self._settled_event.wait(timeout):
self.log.warning("%r has timed out", self)
with self._externally_initiated_completion_lock:
if self._exception is None:
exc = TimeoutError(
f"AndStatus from device {self.device.name} failed to complete in specified timeout of {self.timeout + self.settle_time}."
)
self._exception = exc
# Mark this as "settled".
try:
self._settled()
except Exception:
self.log.exception("%r encountered error during _settled()", self)
with self._lock:
self._event.set()
if self._exception is not None:
try:
self._handle_failure()
except Exception:
self.log.exception("%r encountered an error during _handle_failure()", self)
for cb in self._callbacks:
try:
cb(self)
except Exception:
self.log.exception(
"An error was raised on a background thread while "
"running the callback %r(%r).",
cb,
self,
)
self._callbacks.clear()
class AndStatus(StatusBase):
"""Custom AndStatus for TimePix detector."""
def __init__(
self,
left: StatusBase | DeviceStatus | list[StatusBase | DeviceStatus] | None,
name: str | Device | None = None,
right: StatusBase | DeviceStatus | list[StatusBase | DeviceStatus] | None = None,
**kwargs,
):
self.left = left if isinstance(left, list) else [left]
if right is not None:
self.right = right if isinstance(right, list) else [right]
else:
self.right = []
self.all_statuses = self.left + self.right
if name is None:
name = "unname_status"
elif isinstance(name, Device):
name = name.name
else:
name = name
self.name = name
super().__init__(**kwargs)
self._trace_attributes["left"] = [st._trace_attributes for st in self.left]
self._trace_attributes["right"] = [st._trace_attributes for st in self.right]
def inner(status):
with self._lock:
if self._externally_initiated_completion:
return
if self.done: # Return if status is already done.. It must be resolved already
return
for st in self.all_statuses:
with st._lock:
if st.done and not st.success:
self.set_exception(st.exception()) # st._exception
return
if all(st.done for st in self.all_statuses) and all(
st.success for st in self.all_statuses
):
self.set_finished()
for st in self.all_statuses:
with st._lock:
st.add_callback(inner)
def __repr__(self):
return "({self.left!r} & {self.right!r})".format(self=self)
def __str__(self):
return "{0}(done={1.done}, " "success={1.success})" "".format(self.__class__.__name__, self)
def __contains__(self, status: StatusBase) -> bool:
for child in [self.left, self.right]:
if child == status:
return True
if isinstance(child, AndStatus):
if status in child:
return True
return False
def _run_callbacks(self):
"""
Set the Event and run the callbacks.
"""
if self.timeout is None:
timeout = None
else:
timeout = self.timeout + self.settle_time
if not self._settled_event.wait(timeout):
# We have timed out. It's possible that set_finished() has already
# been called but we got here before the settle_time timer expired.
# And it's possible that in this space be between the above
# statement timing out grabbing the lock just below,
# set_exception(exc) has been called. Both of these possibilties
# are accounted for.
self.log.warning("%r has timed out", self)
with self._externally_initiated_completion_lock:
# Set the exception and mark the Status as done, unless
# set_exception(exc) was called externally before we grabbed
# the lock.
if self._exception is None:
exc = TimeoutError(
f"Status with name {self.name} failed to complete in specified timeout of {self.timeout + self.settle_time}."
)
self._exception = exc
# Mark this as "settled".
try:
self._settled()
except Exception:
# No alternative but to log this. We can't supersede set_exception,
# and we have to continue and run the callbacks.
self.log.exception("%r encountered error during _settled()", self)
# Now we know whether or not we have succeed or failed, either by
# timeout above or by set_exception(exc), so we can set the Event that
# will mark this Status as done.
with self._lock:
self._event.set()
if self._exception is not None:
try:
self._handle_failure()
except Exception:
self.log.exception("%r encountered an error during _handle_failure()", self)
# The callbacks have access to self, from which they can distinguish
# success or failure.
for cb in self._callbacks:
try:
cb(self)
except Exception:
self.log.exception(
"An error was raised on a background thread while "
"running the callback %r(%r).",
cb,
self,
)
self._callbacks.clear()

View File

@@ -10,8 +10,6 @@ from ophyd import EpicsSignal, EpicsSignalRO
from ophyd_devices import CompareStatus, DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from debye_bec.devices.pilatus.utils import AndStatusWithList
if TYPE_CHECKING:
from bec_lib.devicemanager import ScanInfo
@@ -71,11 +69,11 @@ class PilatusCurtain(PSIDeviceBase):
def on_unstage(self) -> DeviceStatus | None:
"""Called while unstaging the device."""
return self.close()
# return self.close()
def on_stop(self) -> DeviceStatus | None:
"""Called when the device is stopped."""
return self.close()
# return self.close()
def open(self) -> DeviceStatus | None:
"""Open the cover"""
@@ -83,8 +81,8 @@ class PilatusCurtain(PSIDeviceBase):
self.open_cover.put(1)
# TODO timeout ok?
status_open = CompareStatus(self.cover_is_open, COVER.OPEN, timeout=5)
status_error = CompareStatus(self.cover_error, COVER.ERROR, operation="!=")
status = AndStatusWithList(device=self, status_list=[status_open, status_error])
status_error = CompareStatus(self.cover_error, COVER.ERROR, operation_success="!=")
status = status_open & status_error
return status
else:
return None
@@ -95,8 +93,8 @@ class PilatusCurtain(PSIDeviceBase):
self.close_cover.put(1)
# TODO timeout ok?
status_close = CompareStatus(self.cover_is_closed, COVER.CLOSED, timeout=5)
status_error = CompareStatus(self.cover_error, COVER.ERROR, operation="!=")
status = AndStatusWithList(device=self, status_list=[status_close, status_error])
status_error = CompareStatus(self.cover_error, COVER.ERROR, operation_success="!=")
status = status_close & status_error
return status
else:
return None

View File

@@ -52,9 +52,15 @@ class Reffoilchanger(PSIDeviceBase):
status = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status"
)
status_string = Cpt(
EpicsSignal, suffix="ES2-REF:SELN-FilterState-ENUM_RBV", kind="config", doc="Status", string=True
)
op_mode = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status"
)
op_mode_string = Cpt(
EpicsSignalWithRBV, suffix="ES2-REF:SELN-OpMode-ENUM", kind="config", doc="Status", string=True
)
ref_set = Cpt(EpicsSignal, suffix="ES2-REF:SELN-SET", kind="config", doc="Requested reference")
ref_rb = Cpt(
EpicsSignalRO, suffix="ES2-REF:SELN-RB", kind="config", doc="Currently set reference"

View File

@@ -6,13 +6,13 @@ build-backend = "hatchling.build"
name = "debye_bec"
version = "0.0.0"
description = "A plugin repository for BEC"
requires-python = ">=3.10"
requires-python = ">=3.11"
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
"Topic :: Scientific/Engineering",
]
dependencies = ["numpy", "scipy", "bec_lib", "h5py", "ophyd_devices"]
dependencies = ["numpy", "scipy", "bec_lib", "h5py", "ophyd_devices", "opencv-python==4.11.0.86"]
[project.optional-dependencies]
dev = [

View File

@@ -1,10 +1,8 @@
# pylint: skip-file
import os
import threading
from typing import TYPE_CHECKING, Generator
from unittest import mock
import numpy as np
import ophyd
import pytest
from bec_lib.messages import ScanStatusMessage
@@ -177,7 +175,7 @@ def test_pilatus_on_trigger_cancel_on_stop(pilatus):
status.wait(timeout=5)
def test_pilatus_on_complete(pilatus):
def test_pilatus_on_complete(pilatus: Pilatus):
"""Test the on_complete logic of the Pilatus detector."""
if pilatus.scan_info.msg.scan_name.startswith("xas"):