mirror of
https://github.com/bec-project/ophyd_devices.git
synced 2025-06-06 20:00:41 +02:00
Merge branch 'csaxs_detector_integration' into 'master'
Csaxs detector integration See merge request bec/ophyd_devices!34
This commit is contained in:
commit
49029649aa
@ -1,18 +1,28 @@
|
|||||||
# -*- coding: utf-8 -*-
|
import enum
|
||||||
"""
|
import threading
|
||||||
Created on Tue Nov 9 16:12:47 2021
|
import time
|
||||||
|
from typing import Any, List
|
||||||
@author: mohacsi_i
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
|
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
|
||||||
from ophyd import PVPositioner, Signal
|
from ophyd import PVPositioner, Signal, DeviceStatus
|
||||||
from ophyd.pseudopos import (
|
from ophyd.pseudopos import (
|
||||||
pseudo_position_argument,
|
pseudo_position_argument,
|
||||||
real_position_argument,
|
real_position_argument,
|
||||||
PseudoSingle,
|
PseudoSingle,
|
||||||
PseudoPositioner,
|
PseudoPositioner,
|
||||||
)
|
)
|
||||||
|
from ophyd_devices.utils.socket import data_shape, data_type
|
||||||
|
from ophyd_devices.utils import bec_utils as bec_utils
|
||||||
|
|
||||||
|
from bec_lib.core import bec_logger
|
||||||
|
|
||||||
|
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
||||||
|
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class DDGError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DelayStatic(Device):
|
class DelayStatic(Device):
|
||||||
@ -33,10 +43,18 @@ class DelayStatic(Device):
|
|||||||
kind=Kind.config,
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
amplitude = Component(
|
amplitude = Component(
|
||||||
EpicsSignal, "OutputAmpAI", write_pv="OutputAmpAO", name="amplitude", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"OutputAmpAI",
|
||||||
|
write_pv="OutputAmpAO",
|
||||||
|
name="amplitude",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
offset = Component(
|
offset = Component(
|
||||||
EpicsSignal, "OutputOffsetAI", write_pv="OutputOffsetAO", name="offset", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"OutputOffsetAI",
|
||||||
|
write_pv="OutputOffsetAO",
|
||||||
|
name="offset",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -57,9 +75,6 @@ class DelayPair(PseudoPositioner):
|
|||||||
# The pseudo positioner axes
|
# The pseudo positioner axes
|
||||||
delay = Component(PseudoSingle, limits=(0, 2000.0), name="delay")
|
delay = Component(PseudoSingle, limits=(0, 2000.0), name="delay")
|
||||||
width = Component(PseudoSingle, limits=(0, 2000.0), name="pulsewidth")
|
width = Component(PseudoSingle, limits=(0, 2000.0), name="pulsewidth")
|
||||||
# The real delay axes
|
|
||||||
# ch1 = Component(EpicsSignal, "DelayAI", write_pv="DelayAO", name="ch1", put_complete=True, kind=Kind.config)
|
|
||||||
# ch2 = Component(EpicsSignal, "DelayAI", write_pv="DelayAO", name="ch2", put_complete=True, kind=Kind.config)
|
|
||||||
ch1 = Component(DummyPositioner, name="ch1")
|
ch1 = Component(DummyPositioner, name="ch1")
|
||||||
ch2 = Component(DummyPositioner, name="ch2")
|
ch2 = Component(DummyPositioner, name="ch2")
|
||||||
io = Component(DelayStatic, name="io")
|
io = Component(DelayStatic, name="io")
|
||||||
@ -85,6 +100,16 @@ class DelayPair(PseudoPositioner):
|
|||||||
return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1)
|
return self.PseudoPosition(delay=real_pos.ch1, width=real_pos.ch2 - real_pos.ch1)
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSource(int, enum.Enum):
|
||||||
|
INTERNAL = 0
|
||||||
|
EXT_RISING_EDGE = 1
|
||||||
|
EXT_FALLING_EDGE = 2
|
||||||
|
SS_EXT_RISING_EDGE = 3
|
||||||
|
SS_EXT_FALLING_EDGE = 4
|
||||||
|
SINGLE_SHOT = 5
|
||||||
|
LINE = 6
|
||||||
|
|
||||||
|
|
||||||
class DelayGeneratorDG645(Device):
|
class DelayGeneratorDG645(Device):
|
||||||
"""DG645 delay generator
|
"""DG645 delay generator
|
||||||
|
|
||||||
@ -109,8 +134,25 @@ class DelayGeneratorDG645(Device):
|
|||||||
current device
|
current device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
state = Component(EpicsSignalRO, "EventStatusLI", name="status_register")
|
SUB_PROGRESS = "progress"
|
||||||
|
SUB_VALUE = "value"
|
||||||
|
_default_sub = SUB_VALUE
|
||||||
|
|
||||||
|
USER_ACCESS = [
|
||||||
|
"set_channels",
|
||||||
|
"_set_trigger",
|
||||||
|
"burst_enable",
|
||||||
|
"burst_disable",
|
||||||
|
"reload_config",
|
||||||
|
]
|
||||||
|
|
||||||
|
trigger_burst_readout = Component(
|
||||||
|
EpicsSignal, "EventStatusLI.PROC", name="trigger_burst_readout"
|
||||||
|
)
|
||||||
|
burst_cycle_finished = Component(EpicsSignalRO, "EventStatusMBBID.B3", name="read_burst_state")
|
||||||
|
delay_finished = Component(EpicsSignalRO, "EventStatusMBBID.B2", name="delay_finished")
|
||||||
status = Component(EpicsSignalRO, "StatusSI", name="status")
|
status = Component(EpicsSignalRO, "StatusSI", name="status")
|
||||||
|
clear_error = Component(EpicsSignal, "StatusClearBO", name="clear_error")
|
||||||
|
|
||||||
# Front Panel
|
# Front Panel
|
||||||
channelT0 = Component(DelayStatic, "T0", name="T0")
|
channelT0 = Component(DelayStatic, "T0", name="T0")
|
||||||
@ -155,62 +197,423 @@ class DelayGeneratorDG645(Device):
|
|||||||
name="trigger_rate",
|
name="trigger_rate",
|
||||||
kind=Kind.config,
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
|
trigger_shot = Component(EpicsSignal, "TriggerDelayBO", name="trigger_shot", kind="config")
|
||||||
# Command PVs
|
|
||||||
# arm = Component(EpicsSignal, "TriggerDelayBO", name="arm", kind=Kind.omitted)
|
|
||||||
|
|
||||||
# Burst mode
|
# Burst mode
|
||||||
burstMode = Component(
|
burstMode = Component(
|
||||||
EpicsSignal, "BurstModeBI", write_pv="BurstModeBO", name="burstmode", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"BurstModeBI",
|
||||||
|
write_pv="BurstModeBO",
|
||||||
|
name="burstmode",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
burstConfig = Component(
|
burstConfig = Component(
|
||||||
EpicsSignal, "BurstConfigBI", write_pv="BurstConfigBO", name="burstconfig", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"BurstConfigBI",
|
||||||
|
write_pv="BurstConfigBO",
|
||||||
|
name="burstconfig",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
burstCount = Component(
|
burstCount = Component(
|
||||||
EpicsSignal, "BurstCountLI", write_pv="BurstCountLO", name="burstcount", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"BurstCountLI",
|
||||||
|
write_pv="BurstCountLO",
|
||||||
|
name="burstcount",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
burstDelay = Component(
|
burstDelay = Component(
|
||||||
EpicsSignal, "BurstDelayAI", write_pv="BurstDelayAO", name="burstdelay", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"BurstDelayAI",
|
||||||
|
write_pv="BurstDelayAO",
|
||||||
|
name="burstdelay",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
burstPeriod = Component(
|
burstPeriod = Component(
|
||||||
EpicsSignal, "BurstPeriodAI", write_pv="BurstPeriodAO", name="burstperiod", kind=Kind.config
|
EpicsSignal,
|
||||||
|
"BurstPeriodAI",
|
||||||
|
write_pv="BurstPeriodAO",
|
||||||
|
name="burstperiod",
|
||||||
|
kind=Kind.config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
delay_burst = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="delay_burst",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
delta_width = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="delta_width",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_triggers = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="additional_triggers",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
polarity = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="polarity",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
amplitude = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="amplitude",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
offset = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="offset",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
thres_trig_level = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="thres_trig_level",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
set_high_on_exposure = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="set_high_on_exposure",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
set_high_on_stage = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="set_high_on_stage",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
set_trigger_source = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="set_trigger_source",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
trigger_width = Component(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="trigger_width",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="ddg_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix="",
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
kind=None,
|
||||||
|
read_attrs=None,
|
||||||
|
configuration_attrs=None,
|
||||||
|
parent=None,
|
||||||
|
device_manager=None,
|
||||||
|
sim_mode=False,
|
||||||
|
ddg_config=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""_summary_
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (_type_): _description_
|
||||||
|
prefix (str, optional): _description_. Defaults to "".
|
||||||
|
kind (_type_, optional): _description_. Defaults to None.
|
||||||
|
read_attrs (_type_, optional): _description_. Defaults to None.
|
||||||
|
configuration_attrs (_type_, optional): _description_. Defaults to None.
|
||||||
|
parent (_type_, optional): _description_. Defaults to None.
|
||||||
|
device_manager (_type_, optional): _description_. Defaults to None.
|
||||||
|
Signals:
|
||||||
|
polarity (_type_, optional): _description_. Defaults to None.
|
||||||
|
amplitude (_type_, optional): _description_. Defaults to None.
|
||||||
|
offset (_type_, optional): _description_. Defaults to None.
|
||||||
|
thres_trig_level (_type_, optional): _description_. Defaults to None.
|
||||||
|
delay_burst (_type_, float): Add delay for triggering in software trigger mode to allow fast shutter to open. Defaults to 0.
|
||||||
|
delta_width (_type_, float): Add width to fast shutter signal to make sure its open during acquisition. Defaults to 0.
|
||||||
|
delta_triggers (_type_, int): Add additional triggers to burst mode (mcs card needs +1 triggers per line). Defaults to 0.
|
||||||
|
set_high_on_exposure
|
||||||
|
set_high_on_stage
|
||||||
|
set_trigger_source
|
||||||
|
"""
|
||||||
|
self.ddg_config = {
|
||||||
|
f"{name}_delay_burst": 0,
|
||||||
|
f"{name}_delta_width": 0,
|
||||||
|
f"{name}_additional_triggers": 0,
|
||||||
|
f"{name}_polarity": [1, 1, 1, 1, 1],
|
||||||
|
f"{name}_amplitude": 4.5,
|
||||||
|
f"{name}_offset": 0,
|
||||||
|
f"{name}_thres_trig_level": 2.5,
|
||||||
|
f"{name}_set_high_on_exposure": False,
|
||||||
|
f"{name}_set_high_on_stage": False,
|
||||||
|
f"{name}_set_trigger_source": "SINGLE_SHOT",
|
||||||
|
f"{name}_trigger_width": None,
|
||||||
|
}
|
||||||
|
if ddg_config is not None:
|
||||||
|
[self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()]
|
||||||
|
super().__init__(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
read_attrs=read_attrs,
|
||||||
|
configuration_attrs=configuration_attrs,
|
||||||
|
parent=parent,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
if device_manager is None and not sim_mode:
|
||||||
|
raise DDGError("Add DeviceManager to initialization or init with sim_mode=True")
|
||||||
|
self.device_manager = device_manager
|
||||||
|
if not sim_mode:
|
||||||
|
self._producer = self.device_manager.producer
|
||||||
|
else:
|
||||||
|
self._producer = bec_utils.MockProducer()
|
||||||
|
self.device_manager = bec_utils.MockDeviceManager()
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self._all_channels = [
|
||||||
|
"channelT0",
|
||||||
|
"channelAB",
|
||||||
|
"channelCD",
|
||||||
|
"channelEF",
|
||||||
|
"channelGH",
|
||||||
|
]
|
||||||
|
self._all_delay_pairs = ["AB", "CD", "EF", "GH"]
|
||||||
|
self.wait_for_connection() # Make sure to be connected before talking to PVs
|
||||||
|
logger.info(f"Current polarity values {self.polarity.get()}")
|
||||||
|
self.reload_config()
|
||||||
|
self._ddg_is_okay()
|
||||||
|
self._stopped = False
|
||||||
|
|
||||||
|
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||||
|
"""Set trigger source to value of list below, or string
|
||||||
|
Accepts integer 0-6 or TriggerSource.* with *
|
||||||
|
INTERNAL = 0
|
||||||
|
EXT_RISING_EDGE = 1
|
||||||
|
EXT_FALLING_EDGE = 2
|
||||||
|
SS_EXT_RISING_EDGE = 3
|
||||||
|
SS_EXT_FALLING_EDGE = 4
|
||||||
|
SINGLE_SHOT = 5
|
||||||
|
LINE = 6
|
||||||
|
"""
|
||||||
|
value = int(trigger_source)
|
||||||
|
self.source.put(value)
|
||||||
|
|
||||||
|
def _ddg_is_okay(self, raise_on_error=False) -> None:
|
||||||
|
status = self.status.read()[self.status.name]["value"]
|
||||||
|
if status != "STATUS OK" and not raise_on_error:
|
||||||
|
logger.warning(f"DDG returns {status}, trying to clear ERROR")
|
||||||
|
self.clear_error()
|
||||||
|
time.sleep(1)
|
||||||
|
self._ddg_is_okay(rais_on_error=True)
|
||||||
|
elif status != "STATUS OK":
|
||||||
|
raise DDGError(f"DDG failed to start with status: {status}")
|
||||||
|
|
||||||
|
def set_channels(self, signal: str, value: Any, channels: List = None) -> None:
|
||||||
|
if not channels:
|
||||||
|
channels = self._all_channels
|
||||||
|
for chname in channels:
|
||||||
|
channel = getattr(self, chname, None)
|
||||||
|
if not channel:
|
||||||
|
continue
|
||||||
|
if signal in channel.component_names:
|
||||||
|
getattr(channel, signal).set(value)
|
||||||
|
continue
|
||||||
|
if "io" in channel.component_names and signal in channel.io.component_names:
|
||||||
|
getattr(channel.io, signal).set(value)
|
||||||
|
|
||||||
|
def _cleanup_ddg(self) -> None:
|
||||||
|
self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
|
|
||||||
|
def reload_config(self) -> None:
|
||||||
|
for ii, channel in enumerate(self._all_channels):
|
||||||
|
self.set_channels("polarity", self.polarity.get()[ii], channels=[channel])
|
||||||
|
# Set polarity for eiger inverted!
|
||||||
|
# self.set_channels("polarity", 0, channels=["channelAB"])
|
||||||
|
self.set_channels("amplitude", self.amplitude.get())
|
||||||
|
self.set_channels("offset", self.offset.get())
|
||||||
|
# Setup reference
|
||||||
|
self.set_channels(
|
||||||
|
"reference",
|
||||||
|
0,
|
||||||
|
[f"channel{self._all_delay_pairs[ii]}.ch1" for ii in range(len(self._all_delay_pairs))],
|
||||||
|
)
|
||||||
|
for ii in range(len(self._all_delay_pairs)):
|
||||||
|
self.set_channels(
|
||||||
|
"reference",
|
||||||
|
0,
|
||||||
|
[f"channel{self._all_delay_pairs[ii]}.ch2"],
|
||||||
|
)
|
||||||
|
self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
|
# Set threshold level for ext. pulses
|
||||||
|
self.level.put(self.thres_trig_level.get())
|
||||||
|
|
||||||
|
def _check_burst_cycle(self, status) -> None:
|
||||||
|
"""Checks burst cycle of delay generator
|
||||||
|
Force readout, return value from end of burst cycle
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
self.trigger_burst_readout.put(1)
|
||||||
|
if (
|
||||||
|
self.burst_cycle_finished.read()[self.burst_cycle_finished.name]["value"] == 1
|
||||||
|
and self.delay_finished.read()[self.delay_finished.name]["value"] == 1
|
||||||
|
):
|
||||||
|
self._acquisition_done = True
|
||||||
|
status.set_finished()
|
||||||
|
return
|
||||||
|
if self._stopped == True:
|
||||||
|
status.set_finished()
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def stop(self, success=False):
|
||||||
|
"""Stops the DDG"""
|
||||||
|
self._stopped = True
|
||||||
|
self._acquisition_done = True
|
||||||
|
super().stop(success=success)
|
||||||
|
|
||||||
def stage(self):
|
def stage(self):
|
||||||
"""Trigger the generator by arming to accept triggers"""
|
"""Trigger the generator by arming to accept triggers"""
|
||||||
# TODO check PV TriggerDelayBO, seems to be a bug in the IOC
|
self.scaninfo.load_scan_metadata()
|
||||||
# self.arm.write(1).wait()
|
if self.scaninfo.scan_type == "step":
|
||||||
|
# define parameters
|
||||||
|
if self.set_high_on_exposure.get():
|
||||||
|
self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
|
num_burst_cycle = 1 + self.additional_triggers.get()
|
||||||
|
|
||||||
|
exp_time = self.delta_width.get() + self.scaninfo.frames_per_trigger * (
|
||||||
|
self.scaninfo.exp_time + self.scaninfo.readout_time
|
||||||
|
)
|
||||||
|
total_exposure = exp_time
|
||||||
|
delay_burst = self.delay_burst.get()
|
||||||
|
self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||||
|
self.set_channels("delay", 0)
|
||||||
|
# Set burst length to half of the experimental time!
|
||||||
|
if not self.trigger_width.get():
|
||||||
|
self.set_channels("width", exp_time)
|
||||||
|
else:
|
||||||
|
self.set_channels("width", self.trigger_width.get())
|
||||||
|
else:
|
||||||
|
self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
|
exp_time = self.delta_width.get() + self.scaninfo.exp_time
|
||||||
|
total_exposure = exp_time + self.scaninfo.readout_time
|
||||||
|
delay_burst = self.delay_burst.get()
|
||||||
|
num_burst_cycle = self.scaninfo.frames_per_trigger + self.additional_triggers.get()
|
||||||
|
# set parameters in DDG
|
||||||
|
self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||||
|
self.set_channels("delay", 0)
|
||||||
|
# Set burst length to half of the experimental time!
|
||||||
|
if not self.trigger_width.get():
|
||||||
|
self.set_channels("width", exp_time)
|
||||||
|
else:
|
||||||
|
self.set_channels("width", self.trigger_width.get())
|
||||||
|
elif self.scaninfo.scan_type == "fly":
|
||||||
|
if self.set_high_on_exposure.get():
|
||||||
|
# define parameters
|
||||||
|
self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
|
exp_time = (
|
||||||
|
self.delta_width.get()
|
||||||
|
+ self.scaninfo.exp_time * self.scaninfo.num_points
|
||||||
|
+ self.scaninfo.readout_time * (self.scaninfo.num_points - 1)
|
||||||
|
)
|
||||||
|
total_exposure = exp_time
|
||||||
|
delay_burst = self.delay_burst.get()
|
||||||
|
# self.additional_triggers should be 0 for self.set_high_on_exposure or remove here fully..
|
||||||
|
num_burst_cycle = 1 + self.additional_triggers.get()
|
||||||
|
# set parameters in DDG
|
||||||
|
self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||||
|
self.set_channels("delay", 0.0)
|
||||||
|
# Set burst length to half of the experimental time!
|
||||||
|
if not self.trigger_width.get():
|
||||||
|
self.set_channels("width", exp_time)
|
||||||
|
else:
|
||||||
|
self.set_channels("width", self.trigger_width.get())
|
||||||
|
else:
|
||||||
|
# define parameters
|
||||||
|
self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
|
exp_time = self.delta_width.get() + self.scaninfo.exp_time
|
||||||
|
total_exposure = exp_time + self.scaninfo.readout_time
|
||||||
|
delay_burst = self.delay_burst.get()
|
||||||
|
num_burst_cycle = self.scaninfo.num_points + self.additional_triggers.get()
|
||||||
|
# set parameters in DDG
|
||||||
|
self.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||||
|
self.set_channels("delay", 0.0)
|
||||||
|
# Set burst length to half of the experimental time!
|
||||||
|
if not self.trigger_width.get():
|
||||||
|
self.set_channels("width", exp_time)
|
||||||
|
else:
|
||||||
|
self.set_channels("width", self.trigger_width.get())
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise DDGError(f"Unknown scan type {self.scaninfo.scan_type}")
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
self._ddg_is_okay()
|
||||||
|
logger.info("DDG staged")
|
||||||
super().stage()
|
super().stage()
|
||||||
|
|
||||||
def unstage(self):
|
def unstage(self):
|
||||||
"""Stop the trigger generator from accepting triggers"""
|
"""Stop the trigger generator from accepting triggers"""
|
||||||
# self.arm.write(0).wait()
|
# self._set_trigger(getattr(TriggerSource, self.set_trigger_source.get()))
|
||||||
super().stage()
|
# Check status
|
||||||
|
self._ddg_is_okay()
|
||||||
|
self._stopped = False
|
||||||
|
self._acquisition_done = False
|
||||||
|
super().unstage()
|
||||||
|
|
||||||
def burstEnable(self, count, delay, period, config="all"):
|
def trigger(self) -> DeviceStatus:
|
||||||
|
# if self.scaninfo.scan_type == "step":
|
||||||
|
if self.source.read()[self.source.name]["value"] == int(TriggerSource.SINGLE_SHOT):
|
||||||
|
self.trigger_shot.put(1)
|
||||||
|
# status = super().trigger(status=)
|
||||||
|
status = DeviceStatus(self)
|
||||||
|
burst_state = threading.Thread(target=self._check_burst_cycle, args=(status,), daemon=True)
|
||||||
|
burst_state.start()
|
||||||
|
return status
|
||||||
|
|
||||||
|
def burst_enable(self, count, delay, period, config="all"):
|
||||||
"""Enable the burst mode"""
|
"""Enable the burst mode"""
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
count = int(count)
|
count = int(count)
|
||||||
assert count > 0, "Number of bursts must be positive"
|
assert count > 0, "Number of bursts must be positive"
|
||||||
assert delay >= 0, "Burst delay must be larger than 0"
|
assert delay >= 0, "Burst delay must be larger than 0"
|
||||||
assert period > 0, "Burst period must be positive"
|
assert period > 0, "Burst period must be positive"
|
||||||
assert config in ["all", "first"], "Supported bust configs are 'all' and 'first'"
|
assert config in [
|
||||||
|
"all",
|
||||||
|
"first",
|
||||||
|
], "Supported bust configs are 'all' and 'first'"
|
||||||
|
|
||||||
self.burstMode.set(1).wait()
|
self.burstMode.put(1)
|
||||||
self.burstCount.set(count).wait()
|
self.burstCount.put(count)
|
||||||
self.burstDelay.set(delay).wait()
|
self.burstDelay.put(delay)
|
||||||
self.burstPeriod.set(period).wait()
|
self.burstPeriod.put(period)
|
||||||
|
|
||||||
if config == "all":
|
if config == "all":
|
||||||
self.burstConfig.set(0).wait()
|
self.burstConfig.put(0)
|
||||||
elif config == "first":
|
elif config == "first":
|
||||||
self.burstConfig.set(1).wait()
|
self.burstConfig.put(1)
|
||||||
|
|
||||||
def burstDisable(self):
|
def burst_disable(self):
|
||||||
"""Disable the burst mode"""
|
"""Disable the burst mode"""
|
||||||
self.burstMode.set(0).wait()
|
self.burstMode.put(0)
|
||||||
|
|
||||||
|
|
||||||
# Automatically connect to test environmenr if directly invoked
|
# Automatically connect to test environmenr if directly invoked
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
dgen = DelayGeneratorDG645("X01DA-PC-DGEN:", name="delayer")
|
dgen = DelayGeneratorDG645("delaygen:DG1:", name="dgen", sim_mode=True)
|
||||||
|
|
||||||
|
# start = time.time()
|
||||||
|
# dgen.stage()
|
||||||
|
# dgen.trigger()
|
||||||
|
# print(f"Time passed for stage and trigger {time.time()-start}s")
|
||||||
|
9
ophyd_devices/epics/devices/Test.py
Normal file
9
ophyd_devices/epics/devices/Test.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from ophyd_devices.epics.devices.pilatus_csaxs import PilatusCsaxs
|
||||||
|
|
||||||
|
os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
|
||||||
|
os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5824"
|
||||||
|
# pilatus_2 = PilatusCsaxs(name="pilatus_2", prefix="X12SA-ES-PILATUS300K")
|
||||||
|
|
||||||
|
# pilatus_2.stage()
|
@ -21,3 +21,11 @@ from .specMotors import (
|
|||||||
from ophyd import EpicsSignal, EpicsSignalRO, EpicsMotor
|
from ophyd import EpicsSignal, EpicsSignalRO, EpicsMotor
|
||||||
from ophyd.sim import SynAxis, SynSignal, SynPeriodicSignal
|
from ophyd.sim import SynAxis, SynSignal, SynPeriodicSignal
|
||||||
from ophyd.quadem import QuadEM
|
from ophyd.quadem import QuadEM
|
||||||
|
|
||||||
|
# cSAXS
|
||||||
|
from .epics_motor_ex import EpicsMotorEx
|
||||||
|
from .mcs_csaxs import McsCsaxs
|
||||||
|
from .eiger9m_csaxs import Eiger9mCsaxs
|
||||||
|
from .pilatus_csaxs import PilatusCsaxs
|
||||||
|
from .falcon_csaxs import FalconCsaxs
|
||||||
|
from .DelayGeneratorDG645 import DelayGeneratorDG645
|
||||||
|
69
ophyd_devices/epics/devices/bec_scaninfo_mixin.py
Normal file
69
ophyd_devices/epics/devices/bec_scaninfo_mixin.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from bec_lib.core import DeviceManagerBase, BECMessage, MessageEndpoints
|
||||||
|
from bec_lib.core import bec_logger
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class BecScaninfoMixin:
|
||||||
|
def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None:
|
||||||
|
self.device_manager = device_manager
|
||||||
|
self.sim_mode = sim_mode
|
||||||
|
self.scan_msg = None
|
||||||
|
self.scanID = None
|
||||||
|
self.bec_info_msg = {
|
||||||
|
"RID": "mockrid",
|
||||||
|
"queueID": "mockqueuid",
|
||||||
|
"scan_number": 1,
|
||||||
|
"exp_time": 12e-3,
|
||||||
|
"num_points": 500,
|
||||||
|
"readout_time": 3e-3,
|
||||||
|
"scan_type": "fly",
|
||||||
|
"num_lines": 1,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_bec_info_msg(self) -> None:
|
||||||
|
return self.bec_info_msg
|
||||||
|
|
||||||
|
def change_config(self, bec_info_msg: dict) -> None:
|
||||||
|
self.bec_info_msg = bec_info_msg
|
||||||
|
|
||||||
|
def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage:
|
||||||
|
if not self.sim_mode:
|
||||||
|
# TODO what if no scan info is there yet!
|
||||||
|
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
||||||
|
return BECMessage.ScanStatusMessage.loads(msg)
|
||||||
|
|
||||||
|
return BECMessage.ScanStatusMessage(
|
||||||
|
scanID="1",
|
||||||
|
status={},
|
||||||
|
info=self.bec_info_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_username(self) -> str:
|
||||||
|
if not self.sim_mode:
|
||||||
|
return self.device_manager.producer.get(MessageEndpoints.account()).decode()
|
||||||
|
return os.getlogin()
|
||||||
|
|
||||||
|
def load_scan_metadata(self) -> None:
|
||||||
|
self.scan_msg = scan_msg = self._get_current_scan_msg()
|
||||||
|
logger.info(f"{self.scan_msg}")
|
||||||
|
try:
|
||||||
|
self.metadata = {
|
||||||
|
"scanID": scan_msg.content["scanID"],
|
||||||
|
"RID": scan_msg.content["info"]["RID"],
|
||||||
|
"queueID": scan_msg.content["info"]["queueID"],
|
||||||
|
}
|
||||||
|
self.scanID = scan_msg.content["scanID"]
|
||||||
|
self.scan_number = scan_msg.content["info"]["scan_number"]
|
||||||
|
self.exp_time = scan_msg.content["info"]["exp_time"]
|
||||||
|
self.frames_per_trigger = scan_msg.content["info"]["frames_per_trigger"]
|
||||||
|
self.num_points = scan_msg.content["info"]["num_points"]
|
||||||
|
self.scan_type = scan_msg.content["info"].get("scan_type", "step")
|
||||||
|
self.readout_time = scan_msg.content["info"]["readout_time"]
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(f"Failed to load scan metadata: {exc}.")
|
||||||
|
|
||||||
|
self.username = self.get_username()
|
346
ophyd_devices/epics/devices/eiger9m_csaxs.py
Normal file
346
ophyd_devices/epics/devices/eiger9m_csaxs.py
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
import enum
|
||||||
|
import time
|
||||||
|
from typing import Any, List
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||||
|
from ophyd import DetectorBase, Device
|
||||||
|
from ophyd import ADComponent as ADCpt
|
||||||
|
|
||||||
|
from bec_lib.core import BECMessage, MessageEndpoints
|
||||||
|
from bec_lib.core.file_utils import FileWriterMixin
|
||||||
|
from bec_lib.core import bec_logger
|
||||||
|
from ophyd_devices.utils import bec_utils as bec_utils
|
||||||
|
|
||||||
|
from std_daq_client import StdDaqClient
|
||||||
|
|
||||||
|
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
||||||
|
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class EigerError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SlsDetectorCam(Device):
|
||||||
|
detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV")
|
||||||
|
setting = ADCpt(EpicsSignalWithRBV, "Setting")
|
||||||
|
delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime")
|
||||||
|
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||||
|
beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy")
|
||||||
|
enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits")
|
||||||
|
bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth")
|
||||||
|
num_gates = ADCpt(EpicsSignalWithRBV, "NumGates")
|
||||||
|
num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles")
|
||||||
|
num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames")
|
||||||
|
timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode")
|
||||||
|
trigger_software = ADCpt(EpicsSignal, "TriggerSoftware")
|
||||||
|
high_voltage = ADCpt(EpicsSignalWithRBV, "HighVoltage")
|
||||||
|
# Receiver and data callback
|
||||||
|
receiver_mode = ADCpt(EpicsSignalWithRBV, "ReceiverMode")
|
||||||
|
receiver_stream = ADCpt(EpicsSignalWithRBV, "ReceiverStream")
|
||||||
|
enable_data = ADCpt(EpicsSignalWithRBV, "UseDataCallback")
|
||||||
|
missed_packets = ADCpt(EpicsSignalRO, "ReceiverMissedPackets_RBV")
|
||||||
|
# Direct settings access
|
||||||
|
setup_file = ADCpt(EpicsSignal, "SetupFile")
|
||||||
|
load_setup = ADCpt(EpicsSignal, "LoadSetup")
|
||||||
|
command = ADCpt(EpicsSignal, "Command")
|
||||||
|
# Mythen 3
|
||||||
|
counter_mask = ADCpt(EpicsSignalWithRBV, "CounterMask")
|
||||||
|
counter1_threshold = ADCpt(EpicsSignalWithRBV, "Counter1Threshold")
|
||||||
|
counter2_threshold = ADCpt(EpicsSignalWithRBV, "Counter2Threshold")
|
||||||
|
counter3_threshold = ADCpt(EpicsSignalWithRBV, "Counter3Threshold")
|
||||||
|
gate1_delay = ADCpt(EpicsSignalWithRBV, "Gate1Delay")
|
||||||
|
gate1_width = ADCpt(EpicsSignalWithRBV, "Gate1Width")
|
||||||
|
gate2_delay = ADCpt(EpicsSignalWithRBV, "Gate2Delay")
|
||||||
|
gate2_width = ADCpt(EpicsSignalWithRBV, "Gate2Width")
|
||||||
|
gate3_delay = ADCpt(EpicsSignalWithRBV, "Gate3Delay")
|
||||||
|
gate3_width = ADCpt(EpicsSignalWithRBV, "Gate3Width")
|
||||||
|
# Moench
|
||||||
|
json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode")
|
||||||
|
json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode")
|
||||||
|
|
||||||
|
# fixes due to missing PVs from CamBase
|
||||||
|
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||||
|
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSource(int, enum.Enum):
|
||||||
|
AUTO = 0
|
||||||
|
TRIGGER = 1
|
||||||
|
GATING = 2
|
||||||
|
BURST_TRIGGER = 3
|
||||||
|
|
||||||
|
|
||||||
|
class DetectorState(int, enum.Enum):
|
||||||
|
IDLE = 0
|
||||||
|
ERROR = 1
|
||||||
|
WAITING = 2
|
||||||
|
FINISHED = 3
|
||||||
|
TRANSMITTING = 4
|
||||||
|
RUNNING = 5
|
||||||
|
STOPPED = 6
|
||||||
|
STILL_WAITING = 7
|
||||||
|
INITIALIZING = 8
|
||||||
|
DISCONNECTED = 9
|
||||||
|
ABORTED = 10
|
||||||
|
|
||||||
|
|
||||||
|
class Eiger9mCsaxs(DetectorBase):
|
||||||
|
"""Eiger 9M detector for CSAXS
|
||||||
|
|
||||||
|
Parent class: DetectorBase
|
||||||
|
Device class: SlsDetectorCam
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name str: 'eiger'
|
||||||
|
prefix (str): PV prefix (X12SA-ES-EIGER9M:)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
cam = ADCpt(SlsDetectorCam, "cam1:")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix="",
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
kind=None,
|
||||||
|
read_attrs=None,
|
||||||
|
configuration_attrs=None,
|
||||||
|
parent=None,
|
||||||
|
device_manager=None,
|
||||||
|
sim_mode=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
read_attrs=read_attrs,
|
||||||
|
configuration_attrs=configuration_attrs,
|
||||||
|
parent=parent,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
self._stopped = False
|
||||||
|
if device_manager is None and not sim_mode:
|
||||||
|
raise EigerError("Add DeviceManager to initialization or init with sim_mode=True")
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.wait_for_connection() # Make sure to be connected before talking to PVs
|
||||||
|
if not sim_mode:
|
||||||
|
from bec_lib.core.bec_service import SERVICE_CONFIG
|
||||||
|
|
||||||
|
self.device_manager = device_manager
|
||||||
|
self._producer = self.device_manager.producer
|
||||||
|
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||||
|
else:
|
||||||
|
self._producer = bec_utils.MockProducer()
|
||||||
|
self.device_manager = bec_utils.MockDeviceManager()
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"}
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
# TODO
|
||||||
|
self.filepath = ""
|
||||||
|
|
||||||
|
self.filewriter = FileWriterMixin(self.service_cfg)
|
||||||
|
self.reduce_readout = 1e-3 # 3 ms
|
||||||
|
self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered
|
||||||
|
self._init_eiger9m()
|
||||||
|
self._init_standard_daq()
|
||||||
|
|
||||||
|
# self.mokev = self.device_manager.devices.mokev.read()[
|
||||||
|
# self.device_manager.devices.mokev.name
|
||||||
|
# ]["value"]
|
||||||
|
|
||||||
|
def _init_eiger9m(self) -> None:
|
||||||
|
"""Init parameters for Eiger 9m"""
|
||||||
|
self._set_trigger(TriggerSource.GATING)
|
||||||
|
self.cam.acquire.set(0)
|
||||||
|
|
||||||
|
def _update_std_cfg(self, cfg_key: str, value: Any) -> None:
|
||||||
|
cfg = self.std_client.get_config()
|
||||||
|
old_value = cfg.get(cfg_key)
|
||||||
|
if old_value is None:
|
||||||
|
raise EigerError(
|
||||||
|
f"Tried to change entry for key {cfg_key} in std_config that does not exist"
|
||||||
|
)
|
||||||
|
if not isinstance(value, type(old_value)):
|
||||||
|
raise EigerError(
|
||||||
|
f"Type of new value {type(value)}:{value} does not match old value {type(old_value)}:{old_value}"
|
||||||
|
)
|
||||||
|
cfg.update({cfg_key: value})
|
||||||
|
logger.info(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}")
|
||||||
|
|
||||||
|
def _init_standard_daq(self) -> None:
|
||||||
|
self.std_rest_server_url = "http://xbl-daq-29:5000"
|
||||||
|
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
|
||||||
|
self.std_client.stop_writer()
|
||||||
|
timeout = 0
|
||||||
|
self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e")))
|
||||||
|
time.sleep(1)
|
||||||
|
while not self.std_client.get_status()["state"] == "READY":
|
||||||
|
time.sleep(0.1)
|
||||||
|
timeout = timeout + 0.1
|
||||||
|
logger.info("Waiting for std_daq init.")
|
||||||
|
if timeout > 2:
|
||||||
|
if not self.std_client.get_status()["state"]:
|
||||||
|
raise EigerError(
|
||||||
|
f"Std client not in READY state, returns: {self.std_client.get_status()}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _prep_det(self) -> None:
|
||||||
|
self._set_det_threshold()
|
||||||
|
self._set_acquisition_params()
|
||||||
|
self._set_trigger(TriggerSource.GATING)
|
||||||
|
|
||||||
|
def _set_det_threshold(self) -> None:
|
||||||
|
# threshold_energy PV exists on Eiger 9M?
|
||||||
|
factor = 1
|
||||||
|
if self.cam.threshold_energy._metadata["units"] == "eV":
|
||||||
|
factor = 1000
|
||||||
|
setp_energy = int(self.mokev * factor)
|
||||||
|
energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"]
|
||||||
|
if setp_energy != energy:
|
||||||
|
self.cam.beam_energy.set(setp_energy) # .wait()
|
||||||
|
threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"]
|
||||||
|
if not np.isclose(setp_energy / 2, threshold, rtol=0.05):
|
||||||
|
self.cam.threshold_energy.set(setp_energy / 2) # .wait()
|
||||||
|
|
||||||
|
def _set_acquisition_params(self) -> None:
|
||||||
|
# self.cam.acquire_time.set(self.scaninfo.exp_time)
|
||||||
|
# Set acquisition parameters slightly shorter then cycle
|
||||||
|
# self.cam.acquire_period.set(
|
||||||
|
# self.scaninfo.exp_time + (self.scaninfo.readout_time - self.reduce_readout)
|
||||||
|
# )
|
||||||
|
self.cam.num_cycles.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
||||||
|
self.cam.num_frames.set(1)
|
||||||
|
|
||||||
|
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||||
|
"""Set trigger source for the detector, either directly to value or TriggerSource.* with
|
||||||
|
AUTO = 0
|
||||||
|
TRIGGER = 1
|
||||||
|
GATING = 2
|
||||||
|
BURST_TRIGGER = 3
|
||||||
|
"""
|
||||||
|
value = int(trigger_source)
|
||||||
|
self.cam.timing_mode.set(value)
|
||||||
|
|
||||||
|
def _prep_file_writer(self) -> None:
|
||||||
|
self.filepath = self.filewriter.compile_full_filename(
|
||||||
|
self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True
|
||||||
|
)
|
||||||
|
# self._close_file_writer()
|
||||||
|
logger.info(f" std_daq output filepath {self.filepath}")
|
||||||
|
try:
|
||||||
|
self.std_client.start_writer_async(
|
||||||
|
{
|
||||||
|
"output_file": self.filepath,
|
||||||
|
"n_images": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
time.sleep(5)
|
||||||
|
if self.std_client.get_status()["state"] == "READY":
|
||||||
|
raise EigerError(f"Timeout of start_writer_async with {exc}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.std_client.get_status()["acquisition"]["state"]
|
||||||
|
if det_ctrl == "WAITING_IMAGES":
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
|
||||||
|
def _close_file_writer(self) -> None:
|
||||||
|
self.std_client.stop_writer()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stage(self) -> List[object]:
|
||||||
|
"""stage the detector and file writer"""
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.mokev = self.device_manager.devices.mokev.obj.read()[
|
||||||
|
self.device_manager.devices.mokev.name
|
||||||
|
]["value"]
|
||||||
|
|
||||||
|
self._prep_det()
|
||||||
|
logger.info("Waiting for std daq to be armed")
|
||||||
|
self._prep_file_writer()
|
||||||
|
logger.info("std_daq is ready")
|
||||||
|
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=False)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.public_file(self.scaninfo.scanID, self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=False)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.file_event(self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
self.arm_acquisition()
|
||||||
|
logger.info("Waiting for Eiger9m to be armed")
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"]
|
||||||
|
if det_ctrl == int(DetectorState.RUNNING):
|
||||||
|
break
|
||||||
|
if self._stopped == True:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
logger.info("Eiger9m is armed")
|
||||||
|
self._stopped = False
|
||||||
|
return super().stage()
|
||||||
|
|
||||||
|
def unstage(self) -> List[object]:
|
||||||
|
"""unstage the detector and file writer"""
|
||||||
|
logger.info("Waiting for Eiger9M to return from acquisition")
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"]
|
||||||
|
if det_ctrl == 0:
|
||||||
|
break
|
||||||
|
if self._stopped == True:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
logger.info("Eiger9M finished")
|
||||||
|
|
||||||
|
logger.info("Waiting for std daq to receive images")
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.std_client.get_status()["acquisition"]["state"]
|
||||||
|
# TODO if no writing was performed before
|
||||||
|
if det_ctrl == "FINISHED":
|
||||||
|
break
|
||||||
|
if self._stopped == True:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
logger.info("Std_daq finished")
|
||||||
|
# Message to BEC
|
||||||
|
state = True
|
||||||
|
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=True, successful=state)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.public_file(self.scaninfo.scanID, self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
self._stopped = False
|
||||||
|
return super().unstage()
|
||||||
|
|
||||||
|
def arm_acquisition(self) -> None:
|
||||||
|
"""Start acquisition in software trigger mode,
|
||||||
|
or arm the detector in hardware of the detector
|
||||||
|
"""
|
||||||
|
self.cam.acquire.set(1)
|
||||||
|
|
||||||
|
def stop(self, *, success=False) -> None:
|
||||||
|
"""Stop the scan, with camera and file writer"""
|
||||||
|
self.cam.acquire.set(0)
|
||||||
|
self._close_file_writer()
|
||||||
|
super().stop(success=success)
|
||||||
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)
|
45
ophyd_devices/epics/devices/epics_motor_ex.py
Normal file
45
ophyd_devices/epics/devices/epics_motor_ex.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from ophyd import Component as Cpt, EpicsSignal, EpicsMotor
|
||||||
|
|
||||||
|
|
||||||
|
class EpicsMotorEx(EpicsMotor):
|
||||||
|
"""Extend EpicsMotor with extra configuration fields."""
|
||||||
|
|
||||||
|
# configuration
|
||||||
|
motor_resolution = Cpt(EpicsSignal, ".MRES", kind="config", auto_monitor=True)
|
||||||
|
base_velocity = Cpt(EpicsSignal, ".VBAS", kind="config", auto_monitor=True)
|
||||||
|
backlash_distance = Cpt(EpicsSignal, ".BDST", kind="config", auto_monitor=True)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix="",
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
kind=None,
|
||||||
|
read_attrs=None,
|
||||||
|
configuration_attrs=None,
|
||||||
|
parent=None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
# get configuration attributes from kwargs and then remove them
|
||||||
|
attrs = {}
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if hasattr(EpicsMotorEx, key) and isinstance(getattr(EpicsMotorEx, key), Cpt):
|
||||||
|
attrs[key] = value
|
||||||
|
for key in attrs:
|
||||||
|
kwargs.pop(key)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
prefix,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
read_attrs=read_attrs,
|
||||||
|
configuration_attrs=configuration_attrs,
|
||||||
|
parent=parent,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
# set configuration attributes
|
||||||
|
for key, value in attrs.items():
|
||||||
|
# print out attributes that are being configured
|
||||||
|
print("setting ", key, "=", value)
|
||||||
|
getattr(self, key).put(value)
|
262
ophyd_devices/epics/devices/falcon_csaxs.py
Normal file
262
ophyd_devices/epics/devices/falcon_csaxs.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
import enum
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import List
|
||||||
|
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Component as Cpt, Device
|
||||||
|
|
||||||
|
from ophyd.mca import EpicsMCARecord
|
||||||
|
from ophyd.areadetector.plugins import HDF5Plugin_V21, FilePlugin_V22
|
||||||
|
|
||||||
|
from bec_lib.core.file_utils import FileWriterMixin
|
||||||
|
from bec_lib.core import MessageEndpoints, BECMessage
|
||||||
|
from bec_lib.core import bec_logger
|
||||||
|
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
||||||
|
|
||||||
|
from ophyd_devices.utils import bec_utils
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class FalconError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DetectorState(int, enum.Enum):
|
||||||
|
DONE = 0
|
||||||
|
ACQUIRING = 1
|
||||||
|
|
||||||
|
|
||||||
|
class EpicsDXPFalcon(Device):
|
||||||
|
"""All high-level DXP parameters for each channel"""
|
||||||
|
|
||||||
|
elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
|
||||||
|
elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
|
||||||
|
elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
|
||||||
|
|
||||||
|
# Energy Filter PVs
|
||||||
|
energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold")
|
||||||
|
min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation")
|
||||||
|
detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True)
|
||||||
|
scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor")
|
||||||
|
risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization")
|
||||||
|
|
||||||
|
# Misc PVs
|
||||||
|
detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity")
|
||||||
|
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
|
||||||
|
|
||||||
|
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
|
||||||
|
|
||||||
|
|
||||||
|
class FalconHDF5Plugins(Device): # HDF5Plugin_V21, FilePlugin_V22):
|
||||||
|
capture = Cpt(EpicsSignalWithRBV, "Capture")
|
||||||
|
enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config")
|
||||||
|
xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config")
|
||||||
|
lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'")
|
||||||
|
temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True)
|
||||||
|
# file_path = Cpt(
|
||||||
|
# EpicsSignalWithRBV, "FilePath", string=True, kind="config", path_semantics="posix"
|
||||||
|
# )
|
||||||
|
file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config")
|
||||||
|
file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config")
|
||||||
|
file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config")
|
||||||
|
num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config")
|
||||||
|
file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config")
|
||||||
|
capture = Cpt(EpicsSignalWithRBV, "Capture")
|
||||||
|
|
||||||
|
|
||||||
|
class FalconCsaxs(Device):
|
||||||
|
"""FalxonX1 with HDF5 writer"""
|
||||||
|
|
||||||
|
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
|
||||||
|
mca = Cpt(EpicsMCARecord, "mca1")
|
||||||
|
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
|
||||||
|
|
||||||
|
# Control
|
||||||
|
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||||
|
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||||
|
start_all = Cpt(EpicsSignal, "StartAll")
|
||||||
|
state = Cpt(EpicsSignal, "Acquiring")
|
||||||
|
# Preset options
|
||||||
|
preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers
|
||||||
|
preset_real = Cpt(EpicsSignal, "PresetReal")
|
||||||
|
preset_events = Cpt(EpicsSignal, "PresetEvents")
|
||||||
|
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
|
||||||
|
# read-only diagnostics
|
||||||
|
triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
|
||||||
|
events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
|
||||||
|
input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
|
||||||
|
output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True)
|
||||||
|
|
||||||
|
# Mapping control
|
||||||
|
collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping
|
||||||
|
pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode")
|
||||||
|
ignore_gate = Cpt(EpicsSignal, "IgnoreGate")
|
||||||
|
input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity")
|
||||||
|
auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer")
|
||||||
|
pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer")
|
||||||
|
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
|
||||||
|
|
||||||
|
# HDF5
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix="",
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
kind=None,
|
||||||
|
read_attrs=None,
|
||||||
|
configuration_attrs=None,
|
||||||
|
parent=None,
|
||||||
|
device_manager=None,
|
||||||
|
sim_mode=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
read_attrs=read_attrs,
|
||||||
|
configuration_attrs=configuration_attrs,
|
||||||
|
parent=parent,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
if device_manager is None and not sim_mode:
|
||||||
|
raise FalconError("Add DeviceManager to initialization or init with sim_mode=True")
|
||||||
|
self._stopped = False
|
||||||
|
self.name = name
|
||||||
|
self.wait_for_connection() # Make sure to be connected before talking to PVs
|
||||||
|
if not sim_mode:
|
||||||
|
from bec_lib.core.bec_service import SERVICE_CONFIG
|
||||||
|
|
||||||
|
self.device_manager = device_manager
|
||||||
|
self._producer = self.device_manager.producer
|
||||||
|
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||||
|
else:
|
||||||
|
self._producer = bec_utils.MockProducer()
|
||||||
|
self.device_manager = bec_utils.MockDeviceManager()
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"}
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.filewriter = FileWriterMixin(self.service_cfg)
|
||||||
|
|
||||||
|
self.readout = 0.003 # 3 ms
|
||||||
|
self._value_pixel_per_buffer = 1 # 16
|
||||||
|
self._clean_up()
|
||||||
|
self._init_hdf5_saving()
|
||||||
|
self._init_mapping_mode()
|
||||||
|
|
||||||
|
def _clean_up(self) -> None:
|
||||||
|
"""Clean up"""
|
||||||
|
self.hdf5.capture.put(0)
|
||||||
|
self.stop_all.put(1)
|
||||||
|
self.erase_all.put(1)
|
||||||
|
|
||||||
|
def _init_hdf5_saving(self) -> None:
|
||||||
|
"""Set up hdf5 save parameters"""
|
||||||
|
self.hdf5.enable.put(1) # EnableCallbacks
|
||||||
|
self.hdf5.xml_file_name.put("layout.xml") # Points to hardcopy of HDF5 Layout xml file
|
||||||
|
self.hdf5.lazy_open.put(1) # Yes -> To be checked how to add FilePlugin_V21+
|
||||||
|
self.hdf5.temp_suffix.put("temps") # -> To be checked how to add FilePlugin_V22+
|
||||||
|
|
||||||
|
def _init_mapping_mode(self) -> None:
|
||||||
|
"""Set up mapping mode params"""
|
||||||
|
self.collect_mode.put(1) # 1 MCA Mapping, 0 MCA Spectrum
|
||||||
|
self.preset_mode.put(1) # 1 Realtime
|
||||||
|
self.input_logic_polarity.put(0) # 0 Normal, 1 Inverted
|
||||||
|
self.pixel_advance_mode.put(1) # 0 User, 1 Gate, 2 Sync
|
||||||
|
self.ignore_gate.put(1) # 1 Yes
|
||||||
|
self.auto_pixels_per_buffer.put(0) # 0 Manual 1 Auto
|
||||||
|
self.pixels_per_buffer.put(16) #
|
||||||
|
|
||||||
|
def _prep_det(self) -> None:
|
||||||
|
"""Prepare detector for acquisition"""
|
||||||
|
self.collect_mode.put(1)
|
||||||
|
self.preset_real.put(self.scaninfo.exp_time)
|
||||||
|
self.pixels_per_run.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
||||||
|
self.auto_pixels_per_buffer.put(0)
|
||||||
|
self.pixels_per_buffer.put(self._value_pixel_per_buffer)
|
||||||
|
|
||||||
|
def _prep_file_writer(self) -> None:
|
||||||
|
"""Prep HDF5 weriting"""
|
||||||
|
# TODO creta filename and destination path from filepath
|
||||||
|
self.destination_path = self.filewriter.compile_full_filename(
|
||||||
|
self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True
|
||||||
|
)
|
||||||
|
# self.hdf5.file_path.set(self.destination_path)
|
||||||
|
file_path, file_name = os.path.split(self.destination_path)
|
||||||
|
self.hdf5.file_path.put(file_path)
|
||||||
|
self.hdf5.file_name.put(file_name)
|
||||||
|
self.hdf5.file_template.put(f"%s%s")
|
||||||
|
self.hdf5.num_capture.put(self.scaninfo.num_points // self._value_pixel_per_buffer + 1)
|
||||||
|
self.hdf5.file_write_mode.put(2)
|
||||||
|
self.hdf5.capture.put(1)
|
||||||
|
|
||||||
|
def stage(self) -> List[object]:
|
||||||
|
"""stage the detector and file writer"""
|
||||||
|
# TODO clean up needed?
|
||||||
|
# self._clean_up()
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.mokev = self.device_manager.devices.mokev.obj.read()[
|
||||||
|
self.device_manager.devices.mokev.name
|
||||||
|
]["value"]
|
||||||
|
|
||||||
|
logger.info("Waiting for pilatus2 to be armed")
|
||||||
|
self._prep_det()
|
||||||
|
logger.info("Pilatus2 armed")
|
||||||
|
logger.info("Waiting for pilatus2 zmq stream to be ready")
|
||||||
|
self._prep_file_writer()
|
||||||
|
logger.info("Pilatus2 zmq ready")
|
||||||
|
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.destination_path, done=False)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.public_file(self.scaninfo.scanID, self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
self.arm_acquisition()
|
||||||
|
logger.info("Waiting for Falcon to be armed")
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.state.read()[self.state.name]["value"]
|
||||||
|
if det_ctrl == int(DetectorState.ACQUIRING):
|
||||||
|
break
|
||||||
|
if self._stopped == True:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
logger.info("Falcon is armed")
|
||||||
|
self._stopped = False
|
||||||
|
return super().stage()
|
||||||
|
|
||||||
|
def arm_acquisition(self) -> None:
|
||||||
|
self.start_all.put(1)
|
||||||
|
|
||||||
|
def unstage(self) -> List[object]:
|
||||||
|
logger.info("Waiting for Falcon to return from acquisition")
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.state.read()[self.state.name]["value"]
|
||||||
|
if det_ctrl == int(DetectorState.DONE):
|
||||||
|
break
|
||||||
|
if self._stopped == True:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
logger.info("Falcon done")
|
||||||
|
# TODO needed?
|
||||||
|
self._clean_up()
|
||||||
|
state = True
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.destination_path, done=True, successful=state)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.public_file(self.scaninfo.metadata["scanID"], self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
self._stopped = False
|
||||||
|
return super().unstage()
|
||||||
|
|
||||||
|
def stop(self, *, success=False) -> None:
|
||||||
|
"""Stop the scan, with camera and file writer"""
|
||||||
|
self._clean_up()
|
||||||
|
super().stop(success=success)
|
||||||
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
falcon = FalconCsaxs(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)
|
383
ophyd_devices/epics/devices/mcs_csaxs.py
Normal file
383
ophyd_devices/epics/devices/mcs_csaxs.py
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
import enum
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any, List
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ophyd import EpicsSignal, EpicsSignalRO
|
||||||
|
from ophyd import EpicsSignal, EpicsSignalRO, Component as Cpt, Device
|
||||||
|
from ophyd.mca import EpicsMCARecord
|
||||||
|
from ophyd.scaler import ScalerCH
|
||||||
|
|
||||||
|
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
||||||
|
from ophyd_devices.utils import bec_utils
|
||||||
|
|
||||||
|
from bec_lib.core import BECMessage, MessageEndpoints
|
||||||
|
from bec_lib.core.file_utils import FileWriterMixin
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from bec_lib.core import bec_logger, threadlocked
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class MCSError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSource(int, enum.Enum):
|
||||||
|
MODE0 = 0
|
||||||
|
MODE1 = 1
|
||||||
|
MODE2 = 2
|
||||||
|
MODE3 = 3
|
||||||
|
MODE4 = 4
|
||||||
|
MODE5 = 5
|
||||||
|
MODE6 = 6
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelAdvance(int, enum.Enum):
|
||||||
|
INTERNAL = 0
|
||||||
|
EXTERNAL = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ReadoutMode(int, enum.Enum):
|
||||||
|
PASSIVE = 0
|
||||||
|
EVENT = 1
|
||||||
|
IO_INTR = 2
|
||||||
|
FREQ_0_1HZ = 3
|
||||||
|
FREQ_0_2HZ = 4
|
||||||
|
FREQ_0_5HZ = 5
|
||||||
|
FREQ_1HZ = 6
|
||||||
|
FREQ_2HZ = 7
|
||||||
|
FREQ_5HZ = 8
|
||||||
|
FREQ_10HZ = 9
|
||||||
|
FREQ_100HZ = 10
|
||||||
|
|
||||||
|
|
||||||
|
class SIS38XX(Device):
|
||||||
|
"""SIS38XX control"""
|
||||||
|
|
||||||
|
# Acquisition
|
||||||
|
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||||
|
erase_start = Cpt(EpicsSignal, "EraseStart", trigger_value=1)
|
||||||
|
start_all = Cpt(EpicsSignal, "StartAll")
|
||||||
|
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||||
|
|
||||||
|
acquiring = Cpt(EpicsSignal, "Acquiring")
|
||||||
|
|
||||||
|
preset_real = Cpt(EpicsSignal, "PresetReal")
|
||||||
|
elapsed_real = Cpt(EpicsSignal, "ElapsedReal")
|
||||||
|
|
||||||
|
read_mode = Cpt(EpicsSignal, "ReadAll.SCAN")
|
||||||
|
read_all = Cpt(EpicsSignal, "DoReadAll.VAL", trigger_value=1)
|
||||||
|
num_use_all = Cpt(EpicsSignal, "NuseAll")
|
||||||
|
current_channel = Cpt(EpicsSignal, "CurrentChannel")
|
||||||
|
dwell = Cpt(EpicsSignal, "Dwell")
|
||||||
|
channel_advance = Cpt(EpicsSignal, "ChannelAdvance")
|
||||||
|
count_on_start = Cpt(EpicsSignal, "CountOnStart")
|
||||||
|
software_channel_advance = Cpt(EpicsSignal, "SoftwareChannelAdvance")
|
||||||
|
channel1_source = Cpt(EpicsSignal, "Channel1Source")
|
||||||
|
prescale = Cpt(EpicsSignal, "Prescale")
|
||||||
|
enable_client_wait = Cpt(EpicsSignal, "EnableClientWait")
|
||||||
|
client_wait = Cpt(EpicsSignal, "ClientWait")
|
||||||
|
acquire_mode = Cpt(EpicsSignal, "AcquireMode")
|
||||||
|
mux_output = Cpt(EpicsSignal, "MUXOutput")
|
||||||
|
user_led = Cpt(EpicsSignal, "UserLED")
|
||||||
|
input_mode = Cpt(EpicsSignal, "InputMode")
|
||||||
|
input_polarity = Cpt(EpicsSignal, "InputPolarity")
|
||||||
|
output_mode = Cpt(EpicsSignal, "OutputMode")
|
||||||
|
output_polarity = Cpt(EpicsSignal, "OutputPolarity")
|
||||||
|
model = Cpt(EpicsSignalRO, "Model", string=True)
|
||||||
|
firmware = Cpt(EpicsSignalRO, "Firmware")
|
||||||
|
max_channels = Cpt(EpicsSignalRO, "MaxChannels")
|
||||||
|
|
||||||
|
|
||||||
|
class McsCsaxs(SIS38XX):
|
||||||
|
SUB_PROGRESS = "progress"
|
||||||
|
SUB_VALUE = "value"
|
||||||
|
_default_sub = SUB_VALUE
|
||||||
|
# scaler = Cpt(ScalerCH, "scaler1")
|
||||||
|
|
||||||
|
# mca2 = Cpt(EpicsMCARecord, "mca2")
|
||||||
|
mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True)
|
||||||
|
mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True)
|
||||||
|
mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True)
|
||||||
|
# mca5 = Cpt(EpicsMCARecord, "mca5")
|
||||||
|
# mca6 = Cpt(EpicsMCARecord, "mca6")
|
||||||
|
# mca7 = Cpt(EpicsMCARecord, "mca7")
|
||||||
|
# mca8 = Cpt(EpicsMCARecord, "mca8")
|
||||||
|
# mca9 = Cpt(EpicsMCARecord, "mca9")
|
||||||
|
# mca10 = Cpt(EpicsMCARecord, "mca10")
|
||||||
|
# mca11 = Cpt(EpicsMCARecord, "mca11")
|
||||||
|
# mca12 = Cpt(EpicsMCARecord, "mca12")
|
||||||
|
# mca13 = Cpt(EpicsMCARecord, "mca13")
|
||||||
|
# mca14 = Cpt(EpicsMCARecord, "mca14")
|
||||||
|
# mca15 = Cpt(EpicsMCARecord, "mca15")
|
||||||
|
# mca16 = Cpt(EpicsMCARecord, "mca16")
|
||||||
|
# mca17 = Cpt(EpicsMCARecord, "mca17")
|
||||||
|
# mca18 = Cpt(EpicsMCARecord, "mca18")
|
||||||
|
# mca19 = Cpt(EpicsMCARecord, "mca19")
|
||||||
|
# mca20 = Cpt(EpicsMCARecord, "mca20")
|
||||||
|
# mca21 = Cpt(EpicsMCARecord, "mca21")
|
||||||
|
# mca22 = Cpt(EpicsMCARecord, "mca22")
|
||||||
|
# mca23 = Cpt(EpicsMCARecord, "mca23")
|
||||||
|
# mca24 = Cpt(EpicsMCARecord, "mca24")
|
||||||
|
# mca25 = Cpt(EpicsMCARecord, "mca25")
|
||||||
|
# mca26 = Cpt(EpicsMCARecord, "mca26")
|
||||||
|
# mca27 = Cpt(EpicsMCARecord, "mca27")
|
||||||
|
# mca28 = Cpt(EpicsMCARecord, "mca28")
|
||||||
|
# mca29 = Cpt(EpicsMCARecord, "mca29")
|
||||||
|
# mca30 = Cpt(EpicsMCARecord, "mca30")
|
||||||
|
# mca31 = Cpt(EpicsMCARecord, "mca31")
|
||||||
|
# mca32 = Cpt(EpicsMCARecord, "mca32")
|
||||||
|
current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True)
|
||||||
|
|
||||||
|
num_lines = Cpt(
|
||||||
|
bec_utils.ConfigSignal,
|
||||||
|
name="num_lines",
|
||||||
|
kind="config",
|
||||||
|
config_storage_name="mcs_config",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
prefix="",
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
kind=None,
|
||||||
|
read_attrs=None,
|
||||||
|
configuration_attrs=None,
|
||||||
|
parent=None,
|
||||||
|
device_manager=None,
|
||||||
|
sim_mode=False,
|
||||||
|
mcs_config=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.mcs_config = {
|
||||||
|
f"{name}_num_lines": 1,
|
||||||
|
}
|
||||||
|
if mcs_config is not None:
|
||||||
|
[self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()]
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
read_attrs=read_attrs,
|
||||||
|
configuration_attrs=configuration_attrs,
|
||||||
|
parent=parent,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
if device_manager is None and not sim_mode:
|
||||||
|
raise MCSError("Add DeviceManager to initialization or init with sim_mode=True")
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self._stream_ttl = 1800
|
||||||
|
self.wait_for_connection() # Make sure to be connected before talking to PVs
|
||||||
|
|
||||||
|
if not sim_mode:
|
||||||
|
self.device_manager = device_manager
|
||||||
|
self._producer = self.device_manager.producer
|
||||||
|
else:
|
||||||
|
self._producer = bec_utils.MockProducer()
|
||||||
|
self.device_manager = bec_utils.MockDeviceManager()
|
||||||
|
# TODO mack mock connector class
|
||||||
|
# self._consumer = self.device_manager.connector.consumer
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
# TODO
|
||||||
|
self.scaninfo.username = "e21206"
|
||||||
|
self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"}
|
||||||
|
self.filewriter = FileWriterMixin(self.service_cfg)
|
||||||
|
self._stopped = False
|
||||||
|
self._acquisition_done = False
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self.counter = 0
|
||||||
|
self.n_points = 0
|
||||||
|
self._init_mcs()
|
||||||
|
|
||||||
|
def _init_mcs(self) -> None:
|
||||||
|
"""Init parameters for mcs card 9m
|
||||||
|
channel_advance: 0/1 -> internal / external
|
||||||
|
channel1_source: 0/1 -> int clock / external source
|
||||||
|
user_led: 0/1 -> off/on
|
||||||
|
max_output : num of channels 0...32, uncomment top for more than 5
|
||||||
|
input_mode: operation mode -> Mode 3 for external trigger, check manual for more info
|
||||||
|
input_polarity: triggered between falling and falling edge -> use inverted signal from ddg
|
||||||
|
"""
|
||||||
|
self.channel_advance.set(ChannelAdvance.EXTERNAL)
|
||||||
|
self.channel1_source.set(ChannelAdvance.INTERNAL)
|
||||||
|
self.user_led.set(0)
|
||||||
|
self.mux_output.set(5)
|
||||||
|
self._set_trigger(TriggerSource.MODE3)
|
||||||
|
self.input_polarity.set(0)
|
||||||
|
self.output_polarity.set(1)
|
||||||
|
self.count_on_start.set(0)
|
||||||
|
self.mca_names = [signal for signal in self.component_names if signal.startswith("mca")]
|
||||||
|
self.mca_data = defaultdict(lambda: [])
|
||||||
|
for mca in self.mca_names:
|
||||||
|
signal = getattr(self, mca)
|
||||||
|
signal.subscribe(self._on_mca_data, run=False)
|
||||||
|
self.current_channel.subscribe(self._progress_update, run=False)
|
||||||
|
|
||||||
|
def _progress_update(self, value, **kwargs) -> None:
|
||||||
|
num_lines = self.num_lines.get()
|
||||||
|
max_value = self.scaninfo.num_points
|
||||||
|
self._run_subs(
|
||||||
|
sub_type=self.SUB_PROGRESS,
|
||||||
|
value=self.counter * int(self.scaninfo.num_points / num_lines) + max(value - 1, 0),
|
||||||
|
max_value=max_value,
|
||||||
|
done=bool(max_value == self.counter),
|
||||||
|
)
|
||||||
|
|
||||||
|
@threadlocked
|
||||||
|
def _on_mca_data(self, *args, obj=None, **kwargs) -> None:
|
||||||
|
if not isinstance(kwargs["value"], (list, np.ndarray)):
|
||||||
|
return
|
||||||
|
self.mca_data[obj.attr_name] = kwargs["value"][1:]
|
||||||
|
if len(self.mca_names) != len(self.mca_data):
|
||||||
|
return
|
||||||
|
# ref_entry = self.mca_data[self.mca_names[0]]
|
||||||
|
# if not ref_entry:
|
||||||
|
# self.mca_data = defaultdict(lambda: [])
|
||||||
|
# return
|
||||||
|
# if isinstance(ref_entry, list) and (ref_entry > 0):
|
||||||
|
# return
|
||||||
|
|
||||||
|
self._updated = True
|
||||||
|
self.counter += 1
|
||||||
|
if (self.scaninfo.scan_type == "fly" and self.counter == self.num_lines.get()) or (
|
||||||
|
self.scaninfo.scan_type == "step" and self.counter == self.scaninfo.num_points
|
||||||
|
):
|
||||||
|
self._acquisition_done = True
|
||||||
|
self.stop_all.put(1, use_complete=False)
|
||||||
|
self._send_data_to_bec()
|
||||||
|
self.erase_all.put(1)
|
||||||
|
# Require wait for
|
||||||
|
# time.sleep(0.01)
|
||||||
|
self.mca_data = defaultdict(lambda: [])
|
||||||
|
self.counter = 0
|
||||||
|
return
|
||||||
|
self.erase_start.set(1)
|
||||||
|
self._send_data_to_bec()
|
||||||
|
self.mca_data = defaultdict(lambda: [])
|
||||||
|
|
||||||
|
def _send_data_to_bec(self) -> None:
|
||||||
|
if self.scaninfo.scan_msg is None:
|
||||||
|
return
|
||||||
|
metadata = self.scaninfo.scan_msg.metadata
|
||||||
|
metadata.update(
|
||||||
|
{
|
||||||
|
"async_update": "append",
|
||||||
|
"num_lines": self.num_lines.get(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = BECMessage.DeviceMessage(
|
||||||
|
signals=dict(self.mca_data),
|
||||||
|
metadata=self.scaninfo.scan_msg.metadata,
|
||||||
|
).dumps()
|
||||||
|
self._producer.xadd(
|
||||||
|
topic=MessageEndpoints.device_async_readback(
|
||||||
|
scanID=self.scaninfo.scanID, device=self.name
|
||||||
|
),
|
||||||
|
msg={"data": msg},
|
||||||
|
expire=self._stream_ttl,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prep_det(self) -> None:
|
||||||
|
self._set_acquisition_params()
|
||||||
|
self._set_trigger(TriggerSource.MODE3)
|
||||||
|
|
||||||
|
def _set_acquisition_params(self) -> None:
|
||||||
|
if self.scaninfo.scan_type == "step":
|
||||||
|
self.n_points = int(self.scaninfo.frames_per_trigger + 1)
|
||||||
|
elif self.scaninfo.scan_type == "fly":
|
||||||
|
self.n_points = int(self.scaninfo.num_points / int(self.num_lines.get()) + 1)
|
||||||
|
else:
|
||||||
|
raise MCSError(f"Scantype {self.scaninfo} not implemented for MCS card")
|
||||||
|
if self.n_points > 10000:
|
||||||
|
raise MCSError(
|
||||||
|
f"Requested number of points N={self.n_points} exceeds hardware limit of mcs card 10000 (N-1)"
|
||||||
|
)
|
||||||
|
self.num_use_all.set(self.n_points)
|
||||||
|
self.preset_real.set(0)
|
||||||
|
|
||||||
|
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||||
|
"""7 Modes, see TriggerSource
|
||||||
|
Mode3 for cSAXS"""
|
||||||
|
value = int(trigger_source)
|
||||||
|
self.input_mode.set(value)
|
||||||
|
|
||||||
|
def _prep_readout(self) -> None:
|
||||||
|
"""Set readout mode of mcs card
|
||||||
|
Check ReadoutMode class for more information about options
|
||||||
|
"""
|
||||||
|
# self.read_mode.set(ReadoutMode.EVENT)
|
||||||
|
self.erase_all.put(1)
|
||||||
|
self.read_mode.set(ReadoutMode.EVENT)
|
||||||
|
|
||||||
|
def _force_readout_mcs_card(self) -> None:
|
||||||
|
self.read_all.put(1, use_complete=False)
|
||||||
|
|
||||||
|
def stage(self) -> List[object]:
|
||||||
|
"""stage the detector and file writer"""
|
||||||
|
logger.info("Stage mcs")
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self._prep_det()
|
||||||
|
self._prep_readout()
|
||||||
|
|
||||||
|
# msg = BECMessage.FileMessage(file_path=self.filepath, done=False)
|
||||||
|
# self._producer.set_and_publish(
|
||||||
|
# MessageEndpoints.public_file(self.scaninfo.scanID, "mcs_csaxs"),
|
||||||
|
# msg.dumps(),
|
||||||
|
# )
|
||||||
|
self.arm_acquisition()
|
||||||
|
logger.info("Waiting for mcs to be armed")
|
||||||
|
while True:
|
||||||
|
det_ctrl = self.acquiring.read()[self.acquiring.name]["value"]
|
||||||
|
if det_ctrl == 1:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
logger.info("mcs is ready and running")
|
||||||
|
# time.sleep(5)
|
||||||
|
return super().stage()
|
||||||
|
|
||||||
|
def unstage(self) -> List[object]:
|
||||||
|
"""unstage"""
|
||||||
|
logger.info("Waiting for mcs to finish acquisition")
|
||||||
|
while not self._acquisition_done:
|
||||||
|
# monitor signal instead?
|
||||||
|
if self._stopped:
|
||||||
|
break
|
||||||
|
time.sleep(0.005)
|
||||||
|
self._acquisition_done = False
|
||||||
|
self._stopped = False
|
||||||
|
logger.info("mcs done")
|
||||||
|
return super().unstage()
|
||||||
|
|
||||||
|
def arm_acquisition(self) -> None:
|
||||||
|
"""Arm acquisition
|
||||||
|
Options:
|
||||||
|
Start: start_all
|
||||||
|
Erase/Start: erase_start
|
||||||
|
"""
|
||||||
|
self.counter = 0
|
||||||
|
self.erase_start.set(1)
|
||||||
|
# self.start_all.set(1)
|
||||||
|
|
||||||
|
def stop(self, *, success=False) -> None:
|
||||||
|
"""Stop acquisition
|
||||||
|
Stop or Stop and Erase
|
||||||
|
"""
|
||||||
|
self.stop_all.set(1)
|
||||||
|
# self.erase_all.set(1)
|
||||||
|
self._stopped = True
|
||||||
|
self._acquisition_done = True
|
||||||
|
self.counter = 0
|
||||||
|
super().stop(success=success)
|
||||||
|
|
||||||
|
|
||||||
|
# Automatically connect to test environmenr if directly invoked
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mcs = McsCsaxs(name="mcs", prefix="X12SA-MCS:", sim_mode=True)
|
||||||
|
mcs.stage()
|
||||||
|
mcs.unstage()
|
@ -1,32 +1,117 @@
|
|||||||
|
import enum
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from typing import List
|
||||||
import requests
|
import requests
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from typing import List
|
from ophyd import EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||||
|
from ophyd import DetectorBase, Device
|
||||||
from ophyd.areadetector import ADComponent as ADCpt, PilatusDetectorCam, DetectorBase
|
from ophyd import ADComponent as ADCpt
|
||||||
from ophyd.areadetector.plugins import FileBase
|
from ophyd_devices.utils import bec_utils as bec_utils
|
||||||
|
|
||||||
from bec_lib.core import BECMessage, MessageEndpoints
|
from bec_lib.core import BECMessage, MessageEndpoints
|
||||||
from bec_lib.core.file_utils import FileWriterMixin
|
from bec_lib.core.file_utils import FileWriterMixin
|
||||||
from bec_lib.core import bec_logger
|
from bec_lib.core import bec_logger
|
||||||
|
|
||||||
|
|
||||||
|
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class PilatusDetectorCamEx(PilatusDetectorCam, FileBase):
|
class PilatusError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSource(int, enum.Enum):
|
||||||
|
INTERNAL = 0
|
||||||
|
EXT_ENABLE = 1
|
||||||
|
EXT_TRIGGER = 2
|
||||||
|
MULTI_TRIGGER = 3
|
||||||
|
ALGINMENT = 4
|
||||||
|
|
||||||
|
|
||||||
|
class SlsDetectorCam(Device): # CamBase, FileBase):
|
||||||
|
# detector_type = ADCpt(EpicsSignalRO, "DetectorType_RBV")
|
||||||
|
# setting = ADCpt(EpicsSignalWithRBV, "Setting")
|
||||||
|
# beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy")
|
||||||
|
# enable_trimbits = ADCpt(EpicsSignalWithRBV, "Trimbits")
|
||||||
|
# bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth")
|
||||||
|
# trigger_software = ADCpt(EpicsSignal, "TriggerSoftware")
|
||||||
|
# high_voltage = ADCpt(EpicsSignalWithRBV, "HighVoltage")
|
||||||
|
# Receiver and data callback
|
||||||
|
# receiver_mode = ADCpt(EpicsSignalWithRBV, "ReceiverMode")
|
||||||
|
# receiver_stream = ADCpt(EpicsSignalWithRBV, "ReceiverStream")
|
||||||
|
# enable_data = ADCpt(EpicsSignalWithRBV, "UseDataCallback")
|
||||||
|
# missed_packets = ADCpt(EpicsSignalRO, "ReceiverMissedPackets_RBV")
|
||||||
|
# # Direct settings access
|
||||||
|
# setup_file = ADCpt(EpicsSignal, "SetupFile")
|
||||||
|
# load_setup = ADCpt(EpicsSignal, "LoadSetup")
|
||||||
|
# command = ADCpt(EpicsSignal, "Command")
|
||||||
|
# Mythen 3
|
||||||
|
# counter_mask = ADCpt(EpicsSignalWithRBV, "CounterMask")
|
||||||
|
# counter1_threshold = ADCpt(EpicsSignalWithRBV, "Counter1Threshold")
|
||||||
|
# counter2_threshold = ADCpt(EpicsSignalWithRBV, "Counter2Threshold")
|
||||||
|
# counter3_threshold = ADCpt(EpicsSignalWithRBV, "Counter3Threshold")
|
||||||
|
# gate1_delay = ADCpt(EpicsSignalWithRBV, "Gate1Delay")
|
||||||
|
# gate1_width = ADCpt(EpicsSignalWithRBV, "Gate1Width")
|
||||||
|
# gate2_delay = ADCpt(EpicsSignalWithRBV, "Gate2Delay")
|
||||||
|
# gate2_width = ADCpt(EpicsSignalWithRBV, "Gate2Width")
|
||||||
|
# gate3_delay = ADCpt(EpicsSignalWithRBV, "Gate3Delay")
|
||||||
|
# gate3_width = ADCpt(EpicsSignalWithRBV, "Gate3Width")
|
||||||
|
# Moench
|
||||||
|
# json_frame_mode = ADCpt(EpicsSignalWithRBV, "JsonFrameMode")
|
||||||
|
# json_detector_mode = ADCpt(EpicsSignalWithRBV, "JsonDetectorMode")
|
||||||
|
|
||||||
|
# Eiger9M
|
||||||
|
# delay_time = ADCpt(EpicsSignalWithRBV, "DelayTime")
|
||||||
|
# num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames")
|
||||||
|
# acquire = ADCpt(EpicsSignal, "Acquire")
|
||||||
|
# acquire_time = ADCpt(EpicsSignal, 'AcquireTime')
|
||||||
|
# detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
|
||||||
|
# threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||||
|
# num_gates = ADCpt(EpicsSignalWithRBV, "NumGates")
|
||||||
|
# num_cycles = ADCpt(EpicsSignalWithRBV, "NumCycles")
|
||||||
|
# timing_mode = ADCpt(EpicsSignalWithRBV, "TimingMode")
|
||||||
|
|
||||||
|
# Pilatus_2 300k
|
||||||
|
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
|
||||||
|
num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||||
|
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||||
|
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
|
||||||
|
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||||
|
armed = ADCpt(EpicsSignalRO, "Armed")
|
||||||
|
|
||||||
|
read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot")
|
||||||
|
detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV")
|
||||||
|
status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV")
|
||||||
|
acquire_time = ADCpt(EpicsSignal, "AcquireTime")
|
||||||
|
acquire_period = ADCpt(EpicsSignal, "AcquirePeriod")
|
||||||
|
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||||
|
file_path = ADCpt(EpicsSignalWithRBV, "FilePath")
|
||||||
|
file_name = ADCpt(EpicsSignalWithRBV, "FileName")
|
||||||
|
file_number = ADCpt(EpicsSignalWithRBV, "FileNumber")
|
||||||
|
auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement")
|
||||||
|
file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate")
|
||||||
|
file_format = ADCpt(EpicsSignalWithRBV, "FileNumber")
|
||||||
|
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
|
||||||
|
|
||||||
|
|
||||||
class PilatusCsaxs(DetectorBase):
|
class PilatusCsaxs(DetectorBase):
|
||||||
|
"""Pilatus_2 300k detector for CSAXS
|
||||||
|
|
||||||
|
Parent class: DetectorBase
|
||||||
|
Device class: PilatusDetectorCamEx
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name str: 'pilatus_2'
|
||||||
|
prefix (str): PV prefix (X12SA-ES-PILATUS300K:)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
in device config, device_access needs to be set true to inject the device manager
|
cam = ADCpt(SlsDetectorCam, "cam1:")
|
||||||
"""
|
|
||||||
|
|
||||||
_html_docs = ["PilatusDoc.html"]
|
|
||||||
cam = ADCpt(PilatusDetectorCamEx, "cam1:")
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -38,11 +123,9 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
configuration_attrs=None,
|
configuration_attrs=None,
|
||||||
parent=None,
|
parent=None,
|
||||||
device_manager=None,
|
device_manager=None,
|
||||||
|
sim_mode=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.device_manager = device_manager
|
|
||||||
self.username = "e21206" # TODO get from config
|
|
||||||
# self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode()
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
name=name,
|
name=name,
|
||||||
@ -52,49 +135,92 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
parent=parent,
|
parent=parent,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
# TODO how to get base_path
|
if device_manager is None and not sim_mode:
|
||||||
self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.username}/Data10/pilatus_2/"}
|
raise PilatusError("Add DeviceManager to initialization or init with sim_mode=True")
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.wait_for_connection() # Make sure to be connected before talking to PVs
|
||||||
|
if not sim_mode:
|
||||||
|
from bec_lib.core.bec_service import SERVICE_CONFIG
|
||||||
|
|
||||||
|
self.device_manager = device_manager
|
||||||
|
self._producer = self.device_manager.producer
|
||||||
|
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||||
|
else:
|
||||||
|
self._producer = bec_utils.MockProducer()
|
||||||
|
self.device_manager = bec_utils.MockDeviceManager()
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.service_cfg = {"base_path": f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"}
|
||||||
|
|
||||||
|
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
||||||
|
self.filepath_h5 = ""
|
||||||
|
|
||||||
self.filewriter = FileWriterMixin(self.service_cfg)
|
self.filewriter = FileWriterMixin(self.service_cfg)
|
||||||
self.num_frames = 0
|
self.readout = 1e-3 # 3 ms
|
||||||
self.readout = 0.003 # 3 ms
|
|
||||||
self.triggermode = 0 # 0 : internal, scan must set this if hardware triggered
|
|
||||||
|
|
||||||
def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage:
|
def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage:
|
||||||
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
||||||
return BECMessage.ScanStatusMessage.loads(msg)
|
return BECMessage.ScanStatusMessage.loads(msg)
|
||||||
|
|
||||||
def stage(self) -> List[object]:
|
def _prep_det(self) -> None:
|
||||||
# TODO remove
|
# TODO slow reaction, seemed to have timeout.
|
||||||
# scan_msg = self._get_current_scan_msg()
|
self._set_det_threshold()
|
||||||
# self.metadata = {
|
self._set_acquisition_params()
|
||||||
# "scanID": scan_msg.content["scanID"],
|
|
||||||
# "RID": scan_msg.content["info"]["RID"],
|
|
||||||
# "queueID": scan_msg.content["info"]["queueID"],
|
|
||||||
# }
|
|
||||||
self.scan_number = 10 # scan_msg.content["info"]["scan_number"]
|
|
||||||
self.exp_time = 0.5 # scan_msg.content["info"]["exp_time"]
|
|
||||||
self.num_frames = 3 # scan_msg.content["info"]["num_points"]
|
|
||||||
# TODO remove
|
|
||||||
# self.username = self.device_manager.producer.get(MessageEndpoints.account()).decode()
|
|
||||||
|
|
||||||
# set pilatus threshol
|
def _set_det_threshold(self) -> None:
|
||||||
self._set_threshold()
|
# threshold_energy PV exists on Eiger 9M?
|
||||||
|
factor = 1
|
||||||
|
if self.cam.threshold_energy._metadata["units"] == "eV":
|
||||||
|
factor = 1000
|
||||||
|
setp_energy = int(self.mokev * factor)
|
||||||
|
threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"]
|
||||||
|
if not np.isclose(setp_energy / 2, threshold, rtol=0.05):
|
||||||
|
self.cam.threshold_energy.set(setp_energy / 2)
|
||||||
|
|
||||||
# set Epic PVs for filewriting
|
def _set_acquisition_params(self) -> None:
|
||||||
self.cam.file_path.set(f"/dev/shm/zmq/")
|
"""set acquisition parameters on the detector"""
|
||||||
self.cam.file_name.set(f"{self.username}_2_{self.scan_number:05d}")
|
# self.cam.acquire_time.set(self.exp_time)
|
||||||
self.cam.auto_increment.set(1) # auto increment
|
# self.cam.acquire_period.set(self.exp_time + self.readout)
|
||||||
self.cam.file_number.set(0) # first iter
|
self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
||||||
self.cam.file_format.set(0) # 0: TIFF
|
self.cam.num_exposures.set(1)
|
||||||
self.cam.file_template.set("%s%s_%5.5d.cbf")
|
self._set_trigger(TriggerSource.EXT_ENABLE) # EXT_TRIGGER)
|
||||||
|
|
||||||
# compile zmq stream for data transfer
|
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||||
scan_dir = self.filewriter._get_scan_directory(
|
"""Set trigger source for the detector, either directly to value or TriggerSource.* with
|
||||||
scan_bundle=1000, scan_number=self.scan_number, leading_zeros=5
|
INTERNAL = 0
|
||||||
|
EXT_ENABLE = 1
|
||||||
|
EXT_TRIGGER = 2
|
||||||
|
MULTI_TRIGGER = 3
|
||||||
|
ALGINMENT = 4
|
||||||
|
"""
|
||||||
|
value = int(trigger_source)
|
||||||
|
self.cam.trigger_mode.set(value)
|
||||||
|
|
||||||
|
def _prep_file_writer(self) -> None:
|
||||||
|
"""Prepare the file writer for pilatus_2
|
||||||
|
a zmq service is running on xbl-daq-34 that is waiting
|
||||||
|
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
|
||||||
|
"""
|
||||||
|
self.filepath_h5 = self.filewriter.compile_full_filename(
|
||||||
|
self.scaninfo.scan_number, "pilatus_2.h5", 1000, 5, True
|
||||||
)
|
)
|
||||||
|
self.cam.file_path.put(f"/dev/shm/zmq/")
|
||||||
|
self.cam.file_name.put(f"{self.scaninfo.username}_2_{self.scaninfo.scan_number:05d}")
|
||||||
|
self.cam.auto_increment.put(1) # auto increment
|
||||||
|
self.cam.file_number.put(0) # first iter
|
||||||
|
self.cam.file_format.put(0) # 0: TIFF
|
||||||
|
self.cam.file_template.put("%s%s_%5.5d.cbf")
|
||||||
|
|
||||||
|
# compile filename
|
||||||
|
basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/"
|
||||||
self.destination_path = os.path.join(
|
self.destination_path = os.path.join(
|
||||||
self.service_cfg["base_path"]
|
basepath,
|
||||||
) # os.path.join(self.service_cfg["base_path"], scan_dir)
|
self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5),
|
||||||
|
)
|
||||||
|
# Make directory if needed
|
||||||
|
os.makedirs(os.path.dirname(self.destination_path), exist_ok=True)
|
||||||
|
|
||||||
data_msg = {
|
data_msg = {
|
||||||
"source": [
|
"source": [
|
||||||
{
|
{
|
||||||
@ -104,6 +230,7 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(data_msg)
|
logger.info(data_msg)
|
||||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
|
|
||||||
@ -112,6 +239,7 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
data=json.dumps(data_msg),
|
data=json.dumps(data_msg),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||||
|
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
@ -119,14 +247,14 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
# prepare writer
|
# prepare writer
|
||||||
data_msg = [
|
data_msg = [
|
||||||
"zmqWriter",
|
"zmqWriter",
|
||||||
self.username,
|
self.scaninfo.username,
|
||||||
{
|
{
|
||||||
"addr": "tcp://x12sa-pd-2:8888",
|
"addr": "tcp://x12sa-pd-2:8888",
|
||||||
"dst": ["file"],
|
"dst": ["file"],
|
||||||
"numFrm": self.num_frames,
|
"numFrm": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger),
|
||||||
"timeout": 2000,
|
"timeout": 2000,
|
||||||
"ifType": "PULL",
|
"ifType": "PULL",
|
||||||
"user": self.username,
|
"user": self.scaninfo.username,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -136,82 +264,123 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||||
|
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|
||||||
self._set_acquisition_params(
|
# Wait for server to become available again
|
||||||
exp_time=self.exp_time,
|
time.sleep(0.1)
|
||||||
readout=self.readout,
|
|
||||||
num_frames=self.num_frames,
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
triggermode=self.triggermode,
|
data_msg = [
|
||||||
|
"zmqWriter",
|
||||||
|
self.scaninfo.username,
|
||||||
|
{
|
||||||
|
"frmCnt": int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger),
|
||||||
|
"timeout": 2000,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
logger.info(f"{res.status_code} -{res.text} - {res.content}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = requests.put(
|
||||||
|
url="http://xbl-daq-34:8091/pilatus_2/wait",
|
||||||
|
data=json.dumps(data_msg),
|
||||||
|
# headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"{res}")
|
||||||
|
|
||||||
|
if not res.ok:
|
||||||
|
res.raise_for_status()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info(f"Pilatus2 wait threw Exception: {exc}")
|
||||||
|
|
||||||
|
def _close_file_writer(self) -> None:
|
||||||
|
"""Close the file writer for pilatus_2
|
||||||
|
a zmq service is running on xbl-daq-34 that is waiting
|
||||||
|
for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2")
|
||||||
|
if not res.ok:
|
||||||
|
res.raise_for_status()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info(f"Pilatus2 delete threw Exception: {exc}")
|
||||||
|
|
||||||
|
def _stop_file_writer(self) -> None:
|
||||||
|
res = requests.put(
|
||||||
|
url="http://xbl-daq-34:8091/pilatus_2/stop",
|
||||||
|
# data=json.dumps(data_msg),
|
||||||
|
# headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not res.ok:
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
def stage(self) -> List[object]:
|
||||||
|
"""stage the detector and file writer"""
|
||||||
|
self._close_file_writer()
|
||||||
|
self._stop_file_writer()
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
self.mokev = self.device_manager.devices.mokev.obj.read()[
|
||||||
|
self.device_manager.devices.mokev.name
|
||||||
|
]["value"]
|
||||||
|
|
||||||
|
logger.info("Waiting for pilatus2 to be armed")
|
||||||
|
self._prep_det()
|
||||||
|
logger.info("Pilatus2 armed")
|
||||||
|
logger.info("Waiting for pilatus2 zmq stream to be ready")
|
||||||
|
self._prep_file_writer()
|
||||||
|
logger.info("Pilatus2 zmq ready")
|
||||||
|
msg = BECMessage.FileMessage(
|
||||||
|
file_path=self.filepath_h5, done=False, metadata={"input_path": self.destination_path}
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().stage()
|
return super().stage()
|
||||||
|
|
||||||
|
def pre_scan(self) -> None:
|
||||||
|
self.acquire()
|
||||||
|
|
||||||
def unstage(self) -> List[object]:
|
def unstage(self) -> List[object]:
|
||||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
"""unstage the detector and file writer"""
|
||||||
data_msg = [
|
# Reset to software trigger
|
||||||
"zmqWriter",
|
|
||||||
self.username,
|
|
||||||
{
|
|
||||||
"frmCnt": self.num_frames,
|
|
||||||
"timeout": 2000,
|
|
||||||
"ifType": "PULL",
|
|
||||||
"user": self.username,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
logger.info(data_msg)
|
|
||||||
|
|
||||||
res = requests.put(
|
|
||||||
url="http://xbl-daq-34:8091/pilatus_1/run",
|
|
||||||
data=json.dumps(data_msg),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
# Reset triggermode to internal
|
|
||||||
self.triggermode = 0
|
self.triggermode = 0
|
||||||
|
# TODO if images are missing, consider adding delay
|
||||||
if not res.ok:
|
self._close_file_writer()
|
||||||
res.raise_for_status()
|
self._stop_file_writer()
|
||||||
|
# Only sent this out once data is written to disk since cbf to hdf5 converter will be triggered
|
||||||
|
msg = BECMessage.FileMessage(
|
||||||
|
file_path=self.filepath_h5, done=True, metadata={"input_path": self.destination_path}
|
||||||
|
)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.public_file(self.scaninfo.scanID, self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
self._producer.set_and_publish(
|
||||||
|
MessageEndpoints.file_event(self.name),
|
||||||
|
msg.dumps(),
|
||||||
|
)
|
||||||
|
logger.info("Pilatus2 done")
|
||||||
return super().unstage()
|
return super().unstage()
|
||||||
|
|
||||||
def _set_threshold(self) -> None:
|
|
||||||
# TODO readout mono, monitor threshold and set it if mokev is different
|
|
||||||
# mokev = self.device_manager.devices.mokev.obj.read()["mokev"]["value"]
|
|
||||||
# TODO remove
|
|
||||||
mokev = 16
|
|
||||||
# TODO refactor naming from name, pilatus_2
|
|
||||||
pil_threshold = self.cam.threshold_energy.read()["pilatus_2_cam_threshold_energy"]["value"]
|
|
||||||
if not np.isclose(mokev / 2, pil_threshold, rtol=0.05):
|
|
||||||
self.cam.threshold_energy.set(mokev / 2)
|
|
||||||
|
|
||||||
def _set_acquisition_params(
|
|
||||||
self, exp_time: float, readout: float, num_frames: int, triggermode: int
|
|
||||||
) -> None:
|
|
||||||
"""set acquisition parameters on the detector
|
|
||||||
|
|
||||||
Args:
|
|
||||||
exp_time (float): exposure time
|
|
||||||
readout (float): readout time
|
|
||||||
num_frames (int): images per scan
|
|
||||||
triggermode (int):
|
|
||||||
0 Internal
|
|
||||||
1 Ext. Enable
|
|
||||||
2 Ext. Trigger
|
|
||||||
3 Mult. Trigger
|
|
||||||
4 Alignment
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
self.cam.acquire_time.set(exp_time)
|
|
||||||
self.cam.acquire_period.set(exp_time + readout)
|
|
||||||
self.cam.num_images.set(num_frames)
|
|
||||||
self.cam.num_exposures.set(1)
|
|
||||||
self.cam.trigger_mode.set(triggermode)
|
|
||||||
|
|
||||||
def acquire(self) -> None:
|
def acquire(self) -> None:
|
||||||
|
"""Start acquisition in software trigger mode,
|
||||||
|
or arm the detector in hardware of the detector
|
||||||
|
"""
|
||||||
self.cam.acquire.set(1)
|
self.cam.acquire.set(1)
|
||||||
|
|
||||||
def stop(self, *, success=False) -> None:
|
def stop(self, *, success=False) -> None:
|
||||||
|
"""Stop the scan, with camera and file writer"""
|
||||||
self.cam.acquire.set(0)
|
self.cam.acquire.set(0)
|
||||||
|
self._stop_file_writer()
|
||||||
|
# self.unstage()
|
||||||
super().stop(success=success)
|
super().stop(success=success)
|
||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
|
# Automatically connect to test environmenr if directly invoked
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pilatus_2 = PilatusCsaxs(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)
|
||||||
|
pilatus_2.stage()
|
||||||
|
@ -181,7 +181,7 @@ class MonoTheta2(VirtualEpicsSignalRO):
|
|||||||
|
|
||||||
|
|
||||||
MONO_THETA2_OFFSETS_FILENAME = (
|
MONO_THETA2_OFFSETS_FILENAME = (
|
||||||
"/import/work/sls/spec/local/X12SA/macros/spec_data/mono_th2_offsets.txt"
|
"/sls/X12SA/data/gac-x12saop/spec/macros/spec_data/mono_th2_offsets.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,6 +49,9 @@ class GalilController(Controller):
|
|||||||
"galil_show_all",
|
"galil_show_all",
|
||||||
"socket_put_and_receive",
|
"socket_put_and_receive",
|
||||||
"socket_put_confirmed",
|
"socket_put_confirmed",
|
||||||
|
"sgalil_reference",
|
||||||
|
"fly_grid_scan",
|
||||||
|
"read_encoder_position",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -150,7 +153,10 @@ class GalilController(Controller):
|
|||||||
return var
|
return var
|
||||||
|
|
||||||
def stop_all_axes(self) -> str:
|
def stop_all_axes(self) -> str:
|
||||||
return self.socket_put_and_receive(f"XQ#STOP,1")
|
# return self.socket_put_and_receive(f"XQ#STOP,1")
|
||||||
|
# Command stops all threads and motors!
|
||||||
|
# self.socket_put_and_receive(f"ST")
|
||||||
|
return self.socket_put_and_receive(f"AB")
|
||||||
|
|
||||||
def axis_is_referenced(self) -> bool:
|
def axis_is_referenced(self) -> bool:
|
||||||
return bool(float(self.socket_put_and_receive(f"MG allaxref").strip()))
|
return bool(float(self.socket_put_and_receive(f"MG allaxref").strip()))
|
||||||
@ -244,7 +250,8 @@ class GalilController(Controller):
|
|||||||
end_x: float,
|
end_x: float,
|
||||||
interval_x: int,
|
interval_x: int,
|
||||||
exp_time: float,
|
exp_time: float,
|
||||||
readtime: float,
|
readout_time: float,
|
||||||
|
**kwargs,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""_summary_
|
"""_summary_
|
||||||
|
|
||||||
@ -256,7 +263,7 @@ class GalilController(Controller):
|
|||||||
end_x (float): end position of x axis (slow axis)
|
end_x (float): end position of x axis (slow axis)
|
||||||
interval_x (int): number of points in x axis
|
interval_x (int): number of points in x axis
|
||||||
exp_time (float): exposure time in seconds
|
exp_time (float): exposure time in seconds
|
||||||
readtime (float): readout time in seconds, minimum of .5e-3s (0.5ms)
|
readout_time (float): readout time in seconds, minimum of .5e-3s (0.5ms)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
|
|
||||||
@ -264,16 +271,24 @@ class GalilController(Controller):
|
|||||||
LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s
|
LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
#
|
||||||
# time.sleep(0.2)
|
axes_referenced = self.axis_is_referenced()
|
||||||
|
sign_y = self._axis[ord("c") - 97].sign
|
||||||
|
sign_x = self._axis[ord("e") - 97].sign
|
||||||
# Check limits
|
# Check limits
|
||||||
# TODO check sign of stage, or not necessary
|
# TODO check sign of stage, or not necessary
|
||||||
check_values = [start_y, end_y, start_x, end_x]
|
check_values = [start_y, end_y, start_x, end_x]
|
||||||
for val in check_values:
|
for val in check_values:
|
||||||
self.check_value(val)
|
self.check_value(val)
|
||||||
|
|
||||||
speed = np.abs(end_y - start_y) / ((interval_y) * exp_time + (interval_y - 1) * readtime)
|
start_x *= sign_x
|
||||||
|
end_x *= sign_x
|
||||||
|
start_y *= sign_y
|
||||||
|
end_y *= sign_y
|
||||||
|
|
||||||
|
speed = np.abs(end_y - start_y) / (
|
||||||
|
(interval_y) * exp_time + (interval_y - 1) * readout_time
|
||||||
|
)
|
||||||
if speed > 2.00 or speed < 0.02:
|
if speed > 2.00 or speed < 0.02:
|
||||||
raise LimitError(
|
raise LimitError(
|
||||||
f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s"
|
f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s"
|
||||||
@ -284,7 +299,7 @@ class GalilController(Controller):
|
|||||||
n_samples = int(interval_y * interval_x)
|
n_samples = int(interval_y * interval_x)
|
||||||
|
|
||||||
# Hard coded to maximum offset of 0.1mm to avoid long motions.
|
# Hard coded to maximum offset of 0.1mm to avoid long motions.
|
||||||
self.socket_put_and_receive(f"off={(0*0.1/2*1000):f}")
|
self.socket_put_and_receive(f"off={(0):f}")
|
||||||
self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}")
|
self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}")
|
||||||
self.socket_put_and_receive(
|
self.socket_put_and_receive(
|
||||||
f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}"
|
f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}"
|
||||||
@ -298,6 +313,7 @@ class GalilController(Controller):
|
|||||||
# threading.Thread(target=_while_in_motion(3, n_samples), daemon=True).start()
|
# threading.Thread(target=_while_in_motion(3, n_samples), daemon=True).start()
|
||||||
# self._while_in_motion(3, n_samples)
|
# self._while_in_motion(3, n_samples)
|
||||||
|
|
||||||
|
# TODO this is for reading out positions, readout is limited by stage triggering
|
||||||
def _while_in_motion(self, thread_id: int, n_samples: int) -> tuple:
|
def _while_in_motion(self, thread_id: int, n_samples: int) -> tuple:
|
||||||
last_readout = 0
|
last_readout = 0
|
||||||
val_axis2 = [] # y axis
|
val_axis2 = [] # y axis
|
||||||
@ -390,7 +406,7 @@ class GalilSetpointSignal(GalilSignalBase):
|
|||||||
Returns:
|
Returns:
|
||||||
float: setpoint / target value
|
float: setpoint / target value
|
||||||
"""
|
"""
|
||||||
return self.setpoint
|
return self.setpoint * self.parent.sign
|
||||||
|
|
||||||
@retry_once
|
@retry_once
|
||||||
@threadlocked
|
@threadlocked
|
||||||
@ -664,6 +680,22 @@ class SGalilMotor(Device, PositionerBase):
|
|||||||
self.controller.stop_all_axes()
|
self.controller.stop_all_axes()
|
||||||
return super().stop(success=success)
|
return super().stop(success=success)
|
||||||
|
|
||||||
|
def kickoff(
|
||||||
|
self,
|
||||||
|
metadata: dict,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
self.controller.fly_grid_scan(
|
||||||
|
kwargs.get("start_y"),
|
||||||
|
kwargs.get("end_y"),
|
||||||
|
kwargs.get("interval_y"),
|
||||||
|
kwargs.get("start_x"),
|
||||||
|
kwargs.get("end_x"),
|
||||||
|
kwargs.get("interval_x"),
|
||||||
|
kwargs.get("exp_time"),
|
||||||
|
kwargs.get("readout_time"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mock = False
|
mock = False
|
||||||
|
144
ophyd_devices/utils/bec_utils.py
Normal file
144
ophyd_devices/utils/bec_utils.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from bec_lib.core import bec_logger
|
||||||
|
|
||||||
|
from ophyd import Signal, Kind
|
||||||
|
|
||||||
|
from ophyd_devices.utils.socket import data_shape, data_type
|
||||||
|
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
DEFAULT_EPICSSIGNAL_VALUE = object()
|
||||||
|
|
||||||
|
|
||||||
|
class MockProducer:
|
||||||
|
def set_and_publish(self, endpoint: str, msgdump: str):
|
||||||
|
logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}")
|
||||||
|
|
||||||
|
|
||||||
|
class MockDeviceManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.devices = devices()
|
||||||
|
|
||||||
|
|
||||||
|
class OphydObject:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.name = "mock_mokev"
|
||||||
|
self.obj = mokev()
|
||||||
|
|
||||||
|
|
||||||
|
class devices:
|
||||||
|
def __init__(self):
|
||||||
|
self.mokev = OphydObject()
|
||||||
|
|
||||||
|
|
||||||
|
class mokev:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = "mock_mokev"
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return {self.name: {"value": 16.0, "timestamp": time.time()}}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigSignal(Signal):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
name,
|
||||||
|
value=0,
|
||||||
|
timestamp=None,
|
||||||
|
parent=None,
|
||||||
|
labels=None,
|
||||||
|
kind=Kind.hinted,
|
||||||
|
tolerance=None,
|
||||||
|
rtolerance=None,
|
||||||
|
metadata=None,
|
||||||
|
cl=None,
|
||||||
|
attr_name="",
|
||||||
|
config_storage_name: str = "config_storage",
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name,
|
||||||
|
value=value,
|
||||||
|
timestamp=timestamp,
|
||||||
|
parent=parent,
|
||||||
|
labels=labels,
|
||||||
|
kind=kind,
|
||||||
|
tolerance=tolerance,
|
||||||
|
rtolerance=rtolerance,
|
||||||
|
metadata=metadata,
|
||||||
|
cl=cl,
|
||||||
|
attr_name=attr_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.storage_name = config_storage_name
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
self._readback = getattr(self.parent, self.storage_name)[self.name]
|
||||||
|
return self._readback
|
||||||
|
|
||||||
|
def put(
|
||||||
|
self,
|
||||||
|
value,
|
||||||
|
connection_timeout=1,
|
||||||
|
callback=None,
|
||||||
|
timeout=1,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Using channel access, set the write PV to `value`.
|
||||||
|
|
||||||
|
Keyword arguments are passed on to callbacks
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
value : any
|
||||||
|
The value to set
|
||||||
|
connection_timeout : float, optional
|
||||||
|
If not already connected, allow up to `connection_timeout` seconds
|
||||||
|
for the connection to complete.
|
||||||
|
use_complete : bool, optional
|
||||||
|
Override put completion settings
|
||||||
|
callback : callable
|
||||||
|
Callback for when the put has completed
|
||||||
|
timeout : float, optional
|
||||||
|
Timeout before assuming that put has failed. (Only relevant if
|
||||||
|
put completion is used.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
old_value = self.get()
|
||||||
|
timestamp = time.time()
|
||||||
|
getattr(self.parent, self.storage_name)[self.name] = value
|
||||||
|
super().put(value, timestamp=timestamp, force=True)
|
||||||
|
self._run_subs(
|
||||||
|
sub_type=self.SUB_VALUE,
|
||||||
|
old_value=old_value,
|
||||||
|
value=value,
|
||||||
|
timestamp=timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
"""Provide schema and meta-data for :meth:`~BlueskyInterface.read`
|
||||||
|
|
||||||
|
This keys in the `OrderedDict` this method returns must match the
|
||||||
|
keys in the `OrderedDict` return by :meth:`~BlueskyInterface.read`.
|
||||||
|
|
||||||
|
This provides schema related information, (ex shape, dtype), the
|
||||||
|
source (ex PV name), and if available, units, limits, precision etc.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
data_keys : OrderedDict
|
||||||
|
The keys must be strings and the values must be dict-like
|
||||||
|
with the ``event_model.event_descriptor.data_key`` schema.
|
||||||
|
"""
|
||||||
|
if self._readback is DEFAULT_EPICSSIGNAL_VALUE:
|
||||||
|
val = self.get()
|
||||||
|
else:
|
||||||
|
val = self._readback
|
||||||
|
return {
|
||||||
|
self.name: {
|
||||||
|
"source": f"{self.parent.prefix}:{self.name}",
|
||||||
|
"dtype": data_type(val),
|
||||||
|
"shape": data_shape(val),
|
||||||
|
}
|
||||||
|
}
|
1
setup.py
1
setup.py
@ -11,6 +11,7 @@ if __name__ == "__main__":
|
|||||||
"bec_lib",
|
"bec_lib",
|
||||||
"numpy",
|
"numpy",
|
||||||
"pyyaml",
|
"pyyaml",
|
||||||
|
"std_daq_client",
|
||||||
"pyepics",
|
"pyepics",
|
||||||
],
|
],
|
||||||
extras_require={"dev": ["pytest", "pytest-random-order", "black"]},
|
extras_require={"dev": ["pytest", "pytest-random-order", "black"]},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user