mirror of
https://github.com/bec-project/ophyd_devices.git
synced 2025-06-22 02:47:59 +02:00
feat: move csaxs devices to plugin structure, fix imports and tests
This commit is contained in:
@ -2,14 +2,7 @@ from .ophyd_patch import monkey_patch_ophyd
|
||||
|
||||
monkey_patch_ophyd()
|
||||
|
||||
from .eiger1p5m_csaxs.eiger1p5m import Eiger1p5MDetector
|
||||
from .epics import *
|
||||
from .galil.fgalil_ophyd import FlomniGalilMotor
|
||||
from .galil.fupr_ophyd import FuprGalilMotor
|
||||
from .galil.galil_ophyd import GalilMotor
|
||||
from .galil.sgalil_ophyd import SGalilMotor
|
||||
from .npoint.npoint import NPointAxis
|
||||
from .rt_lamni import RtFlomniMotor, RtLamniMotor
|
||||
from .sim.sim import SimCamera
|
||||
from .sim.sim import SimFlyer
|
||||
from .sim.sim import SimFlyer as SynFlyer
|
||||
@ -23,7 +16,6 @@ from .sim.sim_frameworks import DeviceProxy, H5ImageReplayProxy, SlitProxy
|
||||
from .sim.sim_signals import ReadOnlySignal
|
||||
from .sim.sim_signals import ReadOnlySignal as SynSignalRO
|
||||
from .sls_devices.sls_devices import SLSInfo, SLSOperatorMessages
|
||||
from .smaract.smaract_ophyd import SmaractMotor
|
||||
from .utils.bec_device_base import BECDeviceBase
|
||||
from .utils.dynamic_pseudo import ComputedSignal
|
||||
from .utils.static_device_test import launch
|
||||
|
@ -1,173 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class _SLSDetectorConfigSignal(Signal):
|
||||
def put(self, value, *, timestamp=None, force=False):
|
||||
self._readback = value
|
||||
self.parent.sim_state[self.name] = value
|
||||
|
||||
def get(self):
|
||||
self._readback = self.parent.sim_state[self.name]
|
||||
return self.parent.sim_state[self.name]
|
||||
|
||||
|
||||
# if (_eigerinvac_is_on == 1) {
|
||||
# tic("setup eiger in vac")
|
||||
# epics_put("X12SA-ES1-DOUBLE-02",0)
|
||||
# unix(sprintf("mkdir -p /sls/X12SA/data/%s/Data10/eiger_4/S%05d-%05d/S%05d",_username,int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1))
|
||||
|
||||
# epics_put("XOMNYI-DET-OUTDIR:0.DESC",sprintf("/sls/X12SA/data/%s/Data10/eiger_4/",_username))
|
||||
# epics_put("XOMNYI-DET-OUTDIR:1.DESC",sprintf("S%05d-%05d/S%05d",int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1))
|
||||
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts)
|
||||
# epics_put("XOMNYI-DET-EXPTIME:0", $2)
|
||||
# epics_put("XOMNYI-DET-INDEX:0", SCAN_N+1)
|
||||
|
||||
# epics_put("XOMNYI-DET-CONTROL:0.DESC", "begin")
|
||||
# if(_eigerinvac_burst==0)
|
||||
# {
|
||||
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts)
|
||||
# epics_put("XOMNYI-DET-EXPTIME:0", $2)
|
||||
# metadata_set("eiger_burst", "int", 1, 1)
|
||||
# }
|
||||
# else
|
||||
# {
|
||||
# metadata_set("eiger_burst", "int", 1, (int($2/0.0085)))
|
||||
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts*(int($2/0.0085)))
|
||||
# epics_put("XOMNYI-DET-EXPTIME:0", 0.005)
|
||||
# }
|
||||
# global _DC_acquisition_ID
|
||||
# _DC_acquisition_ID = SCAN_N+1
|
||||
|
||||
|
||||
class Eiger1p5MDetector(Device):
|
||||
USER_ACCESS = []
|
||||
file_path = Cpt(EpicsSignal, name="file_path", read_pv="XOMNYI-DET-OUTDIR:0.DESC")
|
||||
detector_out_scan_dir = Cpt(
|
||||
EpicsSignal, name="detector_out_scan_dir", read_pv="XOMNYI-DET-OUTDIR:1.DESC"
|
||||
)
|
||||
frames = Cpt(EpicsSignal, name="frames", read_pv="XOMNYI-DET-CYCLES:0")
|
||||
exp_time = Cpt(EpicsSignal, name="exp_time", read_pv="XOMNYI-DET-EXPTIME:0")
|
||||
index = Cpt(EpicsSignal, name="index", read_pv="XOMNYI-DET-INDEX:0")
|
||||
detector_control = Cpt(
|
||||
EpicsSignal, name="detector_control", read_pv="XOMNYI-DET-CONTROL:0.DESC"
|
||||
)
|
||||
framescaught = Cpt(EpicsSignalRO, name="framescaught", read_pv="XOMNYI-DET-CONTROL:0.VAL")
|
||||
|
||||
file_pattern = Cpt(_SLSDetectorConfigSignal, name="file_pattern", value="")
|
||||
burst = Cpt(_SLSDetectorConfigSignal, name="burst", value=1)
|
||||
save_file = Cpt(_SLSDetectorConfigSignal, name="save_file", value=False)
|
||||
|
||||
def __init__(self, *, name, kind=None, parent=None, device_manager=None, **kwargs):
|
||||
self.device_manager = device_manager
|
||||
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
|
||||
self.sim_state = {
|
||||
f"{self.name}_file_path": "~/Data10/data/",
|
||||
f"{self.name}_file_pattern": f"{self.name}_{{:05d}}.h5",
|
||||
f"{self.name}_frames": 1,
|
||||
f"{self.name}_burst": 1,
|
||||
f"{self.name}_save_file": False,
|
||||
f"{self.name}_exp_time": 0,
|
||||
}
|
||||
self._stopped = False
|
||||
self.file_name = ""
|
||||
self.metadata = {}
|
||||
self.username = "e20588" # TODO get from config
|
||||
|
||||
def _get_current_scan_msg(self) -> messages.ScanStatusMessage:
|
||||
return self.device_manager.connector.get(MessageEndpoints.scan_status())
|
||||
|
||||
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
|
||||
if leading_zeros is None:
|
||||
leading_zeros = len(str(scan_bundle))
|
||||
floor_dir = scan_number // scan_bundle * scan_bundle
|
||||
return f"S{floor_dir:0{leading_zeros}d}-{floor_dir+scan_bundle-1:0{leading_zeros}d}/S{scan_number:0{leading_zeros}d}"
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
scan_msg = self._get_current_scan_msg()
|
||||
self.metadata = {
|
||||
"scan_id": scan_msg.content["scan_id"],
|
||||
"RID": scan_msg.content["info"]["RID"],
|
||||
"queue_id": scan_msg.content["info"]["queue_id"],
|
||||
}
|
||||
scan_number = scan_msg.content["info"]["scan_number"]
|
||||
exp_time = scan_msg.content["info"]["exp_time"]
|
||||
|
||||
# set base path for detector output
|
||||
self.file_path.set(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/")
|
||||
|
||||
# set scan directory (e.g. S00000-00999/S00020)
|
||||
scan_dir = self._get_scan_dir(scan_bundle=1000, scan_number=scan_number, leading_zeros=5)
|
||||
self.detector_out_scan_dir.set(scan_dir)
|
||||
|
||||
self.file_name = os.path.join(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/", scan_dir)
|
||||
|
||||
# set the scan number
|
||||
self.index.set(scan_number)
|
||||
|
||||
# set the number of frames
|
||||
self.frames.set(scan_msg.content["info"]["num_points"])
|
||||
|
||||
# set the exposure time
|
||||
self.exp_time.set(exp_time)
|
||||
|
||||
# wait for detector control to become "ready"
|
||||
while True:
|
||||
det_ctrl = self.detector_control.get()
|
||||
if det_ctrl == "ready":
|
||||
break
|
||||
time.sleep(0.005)
|
||||
|
||||
# send the "begin" flag to start processing the above commands
|
||||
self.detector_control.set("begin")
|
||||
|
||||
# wait for detector to be "armed"
|
||||
logger.info("Waiting for detector setup")
|
||||
while True:
|
||||
det_ctrl = self.detector_control.get()
|
||||
if det_ctrl == "armed":
|
||||
break
|
||||
time.sleep(0.005)
|
||||
|
||||
self.detector_control.put("acquiring")
|
||||
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
time_waited = 0
|
||||
sleep_time = 0.2
|
||||
framesexpected = self.frames.get()
|
||||
while True:
|
||||
framescaught = self.framescaught.get()
|
||||
if self.framescaught.get() < framesexpected:
|
||||
logger.info(
|
||||
f"Waiting for frames: Transferred {framescaught} out of {framesexpected}"
|
||||
)
|
||||
time_waited += sleep_time
|
||||
time.sleep(sleep_time)
|
||||
if self._stopped:
|
||||
break
|
||||
continue
|
||||
break
|
||||
self.detector_control.put("stop")
|
||||
signals = {"config": self.read(), "data": self.file_name}
|
||||
msg = messages.DeviceMessage(signals=signals, metadata=self.metadata)
|
||||
self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg)
|
||||
self._stopped = False
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.detector_control.put("stop")
|
||||
super().stop(success=success)
|
||||
self._stopped = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
eiger = Eiger1p5MDetector(name="eiger1p5m", label="eiger1p5m")
|
||||
breakpoint()
|
@ -3,9 +3,6 @@ from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO
|
||||
from ophyd.quadem import QuadEM
|
||||
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
|
||||
|
||||
from .devices.delay_generator_csaxs import DelayGeneratorcSAXS
|
||||
from .devices.flomni_sample_storage import FlomniSampleStorage
|
||||
from .devices.InsertionDevice import InsertionDevice
|
||||
from .devices.slits import SlitH, SlitV
|
||||
from .devices.specMotors import (
|
||||
Bpm4i,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind, PVPositioner
|
||||
|
||||
|
||||
class InsertionDevice(PVPositioner):
|
||||
"""Python wrapper for the CSAXS insertion device control
|
||||
|
||||
This wrapper provides a positioner interface for the ID control.
|
||||
is completely custom XBPM with templates directly in the
|
||||
VME repo. Thus it needs a custom ophyd template as well...
|
||||
|
||||
WARN: The x and y are not updated by the IOC
|
||||
"""
|
||||
|
||||
status = Component(EpicsSignalRO, "-USER:STATUS", auto_monitor=True)
|
||||
errorSource = Component(EpicsSignalRO, "-USER:ERROR-SOURCE", auto_monitor=True)
|
||||
isOpen = Component(EpicsSignalRO, "-GAP:ISOPEN", auto_monitor=True)
|
||||
|
||||
# PVPositioner interface
|
||||
setpoint = Component(EpicsSignal, "-GAP:SET", auto_monitor=True)
|
||||
readback = Component(EpicsSignalRO, "-GAP:READ", auto_monitor=True, kind=Kind.hinted)
|
||||
done = Component(EpicsSignalRO, ":DONE", auto_monitor=True)
|
||||
stop_signal = Component(EpicsSignal, "-GAP:STOP", kind=Kind.omitted)
|
||||
|
||||
|
||||
# Automatically start simulation if directly invoked
|
||||
# (NA for important devices)
|
||||
if __name__ == "__main__":
|
||||
pass
|
@ -12,17 +12,10 @@ from .aerotech.AerotechAutomation1 import (
|
||||
aa1GlobalVariables,
|
||||
aa1Tasks,
|
||||
)
|
||||
from .delay_generator_csaxs import DelayGeneratorcSAXS
|
||||
from .eiger9m_csaxs import Eiger9McSAXS
|
||||
|
||||
# cSAXS
|
||||
from .epics_motor_ex import EpicsMotorEx
|
||||
from .falcon_csaxs import FalconcSAXS
|
||||
from .flomni_sample_storage import FlomniSampleStorage
|
||||
from .grashopper_tomcat import GrashopperTOMCAT
|
||||
from .InsertionDevice import InsertionDevice
|
||||
from .mcs_csaxs import MCScSAXS
|
||||
from .pilatus_csaxs import PilatuscSAXS
|
||||
from .psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
from .slits import SlitH, SlitV
|
||||
from .specMotors import (
|
||||
|
@ -1,30 +0,0 @@
|
||||
TABLES_DT_PUSH_DIST_MM = 890
|
||||
|
||||
|
||||
class DetectorTableTheta(PseudoPositioner):
|
||||
"""Detector table tilt motor
|
||||
|
||||
Small wrapper to adjust the detector table tilt as angle.
|
||||
The table is pushed from one side by a single vertical motor.
|
||||
|
||||
Note: Rarely used!
|
||||
"""
|
||||
|
||||
# Real axis (in degrees)
|
||||
pusher = Component(EpicsMotor, "", name="pusher")
|
||||
# Virtual axis
|
||||
theta = Component(PseudoSingle, name="theta")
|
||||
|
||||
_real = ["pusher"]
|
||||
|
||||
@pseudo_position_argument
|
||||
def forward(self, pseudo_pos):
|
||||
return self.RealPosition(
|
||||
pusher=tan(pseudo_pos.theta * 3.141592 / 180.0) * TABLES_DT_PUSH_DIST_MM
|
||||
)
|
||||
|
||||
@real_position_argument
|
||||
def inverse(self, real_pos):
|
||||
return self.PseudoPosition(
|
||||
theta=-180 * atan(real_pos.pusher / TABLES_DT_PUSH_DIST_MM) / 3.141592
|
||||
)
|
@ -1,346 +0,0 @@
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component
|
||||
|
||||
from ophyd_devices.epics.devices.psi_delay_generator_base import (
|
||||
DDGCustomMixin,
|
||||
PSIDelayGeneratorBase,
|
||||
TriggerSource,
|
||||
)
|
||||
from ophyd_devices.utils import bec_utils
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DelayGeneratorError(Exception):
|
||||
"""Exception raised for errors."""
|
||||
|
||||
|
||||
class DDGSetup(DDGCustomMixin):
|
||||
"""
|
||||
Mixin class for DelayGenerator logic at cSAXS.
|
||||
|
||||
At cSAXS, multiple DDGs were operated at the same time. There different behaviour is
|
||||
implemented in the ddg_config signals that are passed via the device config.
|
||||
"""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Method to initialize default parameters."""
|
||||
for ii, channel in enumerate(self.parent.all_channels):
|
||||
self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel])
|
||||
|
||||
self.parent.set_channels("amplitude", self.parent.amplitude.get())
|
||||
self.parent.set_channels("offset", self.parent.offset.get())
|
||||
# Setup reference
|
||||
self.parent.set_channels(
|
||||
"reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs]
|
||||
)
|
||||
self.parent.set_channels(
|
||||
"reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs]
|
||||
)
|
||||
self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
|
||||
# Set threshold level for ext. pulses
|
||||
self.parent.level.put(self.parent.thres_trig_level.get())
|
||||
|
||||
def prepare_ddg(self) -> None:
|
||||
"""
|
||||
Method to prepare scan logic of cSAXS
|
||||
|
||||
Two scantypes are supported: "step" and "fly":
|
||||
- step: Scan is performed by stepping the motor and acquiring data at each step
|
||||
- fly: Scan is performed by moving the motor with a constant velocity and acquiring data
|
||||
|
||||
Custom logic for different DDG behaviour during scans.
|
||||
|
||||
- set_high_on_exposure : If True, then TTL signal is high during
|
||||
the full exposure time of the scan (all frames).
|
||||
E.g. Keep shutter open for the full scan.
|
||||
- fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel.
|
||||
If the value is 0, then the width of the TTL pulse is determined,
|
||||
no matter which parameters are passed from the scaninfo for exposure time
|
||||
- set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones
|
||||
were: SINGLE_SHOT, EXT_RISING_EDGE
|
||||
"""
|
||||
self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
|
||||
# scantype "step"
|
||||
if self.parent.scaninfo.scan_type == "step":
|
||||
# High on exposure means that the signal
|
||||
if self.parent.set_high_on_exposure.get():
|
||||
# caluculate parameters
|
||||
num_burst_cycle = 1 + self.parent.additional_triggers.get()
|
||||
|
||||
exp_time = (
|
||||
self.parent.delta_width.get()
|
||||
+ self.parent.scaninfo.frames_per_trigger
|
||||
* (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time)
|
||||
)
|
||||
total_exposure = exp_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
for value, channel in zip(
|
||||
self.parent.fixed_ttl_width.get(), self.parent.all_channels
|
||||
):
|
||||
logger.debug(f"Trying to set DDG {channel} to {value}")
|
||||
if value != 0:
|
||||
self.parent.set_channels("width", value, channels=[channel])
|
||||
else:
|
||||
# caluculate parameters
|
||||
exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time
|
||||
total_exposure = exp_time + self.parent.scaninfo.readout_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
num_burst_cycle = (
|
||||
self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get()
|
||||
)
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
# scantype "fly"
|
||||
elif self.parent.scaninfo.scan_type == "fly":
|
||||
if self.parent.set_high_on_exposure.get():
|
||||
# caluculate parameters
|
||||
exp_time = (
|
||||
self.parent.delta_width.get()
|
||||
+ self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points
|
||||
+ self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1)
|
||||
)
|
||||
total_exposure = exp_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
num_burst_cycle = 1 + self.parent.additional_triggers.get()
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
for value, channel in zip(
|
||||
self.parent.fixed_ttl_width.get(), self.parent.all_channels
|
||||
):
|
||||
logger.debug(f"Trying to set DDG {channel} to {value}")
|
||||
if value != 0:
|
||||
self.parent.set_channels("width", value, channels=[channel])
|
||||
else:
|
||||
# caluculate parameters
|
||||
exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time
|
||||
total_exposure = exp_time + self.parent.scaninfo.readout_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
num_burst_cycle = (
|
||||
self.parent.scaninfo.num_points + self.parent.additional_triggers.get()
|
||||
)
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
|
||||
else:
|
||||
raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}")
|
||||
# Set common DDG parameters
|
||||
self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||
self.parent.set_channels("delay", 0.0)
|
||||
|
||||
def on_trigger(self) -> None:
|
||||
"""Method to be executed upon trigger"""
|
||||
if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT:
|
||||
self.parent.trigger_shot.put(1)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""
|
||||
Method to check if scan_id has changed.
|
||||
|
||||
If yes, then it changes parent.stopped to True, which will stop further actions.
|
||||
"""
|
||||
old_scan_id = self.parent.scaninfo.scan_id
|
||||
self.parent.scaninfo.load_scan_metadata()
|
||||
if self.parent.scaninfo.scan_id != old_scan_id:
|
||||
self.parent.stopped = True
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Method checks if DDG finished acquisition"""
|
||||
|
||||
def on_pre_scan(self) -> None:
|
||||
"""
|
||||
Method called by pre_scan hook in parent class.
|
||||
|
||||
Executes trigger if premove_trigger is Trus.
|
||||
"""
|
||||
if self.parent.premove_trigger.get() is True:
|
||||
self.parent.trigger_shot.put(1)
|
||||
|
||||
|
||||
class DelayGeneratorcSAXS(PSIDelayGeneratorBase):
|
||||
"""
|
||||
DG645 delay generator at cSAXS (multiple can be in use depending on the setup)
|
||||
|
||||
Default values for setting up DDG.
|
||||
Note: checks of set calues are not (only partially) included, check manual for details on possible settings.
|
||||
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
|
||||
|
||||
- delay_burst : (float >=0) Delay between trigger and first pulse in burst mode
|
||||
- delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition
|
||||
- additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line)
|
||||
- polarity : (list of 0/1) polarity for different channels
|
||||
- amplitude : (float) amplitude voltage of TTLs
|
||||
- offset : (float) offset for ampltitude
|
||||
- thres_trig_level : (float) threshold of trigger amplitude
|
||||
|
||||
Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg):
|
||||
|
||||
- set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan.
|
||||
# TODO trigger_width and fixed_ttl could be combined into single list.
|
||||
- fixed_ttl_width : (list of either 1 or 0), one for each channel.
|
||||
- trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value.
|
||||
- set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG.
|
||||
- premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan).
|
||||
- set_high_on_stage : (bool) if True, then TTL signal should go high already on stage.
|
||||
"""
|
||||
|
||||
custom_prepare_cls = DDGSetup
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
fixed_ttl_width = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="fixed_ttl_width",
|
||||
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",
|
||||
)
|
||||
premove_trigger = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="premove_trigger",
|
||||
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,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
prefix (str, optional): Prefix of the device. Defaults to "".
|
||||
name (str): Name of the device.
|
||||
kind (str, optional): Kind of the device. Defaults to None.
|
||||
read_attrs (list, optional): List of attributes to read. Defaults to None.
|
||||
configuration_attrs (list, optional): List of attributes to configure. Defaults to None.
|
||||
parent (Device, optional): Parent device. Defaults to None.
|
||||
device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None.
|
||||
sim_mode (bool, optional): Simulation mode flag. Defaults to False.
|
||||
ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None.
|
||||
|
||||
"""
|
||||
# Default values for ddg_config signals
|
||||
self.ddg_config = {
|
||||
# Setup default values
|
||||
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,
|
||||
# Values for different behaviour during scans
|
||||
f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0],
|
||||
f"{name}_trigger_width": None,
|
||||
f"{name}_set_high_on_exposure": False,
|
||||
f"{name}_set_high_on_stage": False,
|
||||
f"{name}_set_trigger_source": "SINGLE_SHOT",
|
||||
f"{name}_premove_trigger": False,
|
||||
}
|
||||
if ddg_config is not None:
|
||||
# pylint: disable=expression-not-assigned
|
||||
[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,
|
||||
device_manager=device_manager,
|
||||
sim_mode=sim_mode,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start delay generator in simulation mode.
|
||||
# Note: To run, access to Epics must be available.
|
||||
dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True)
|
@ -1,428 +0,0 @@
|
||||
import enum
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import messages, threadlocked
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import ADComponent as ADCpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||
from std_daq_client import StdDaqClient
|
||||
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class EigerError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class EigerTimeoutError(EigerError):
|
||||
"""Raised when the Eiger does not respond in time."""
|
||||
|
||||
|
||||
class Eiger9MSetup(CustomDetectorMixin):
|
||||
"""Eiger setup class
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self.std_rest_server_url = (
|
||||
kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000"
|
||||
)
|
||||
self.std_client = None
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Set default parameters for Eiger9M detector"""
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# Stops the detector
|
||||
self.stop_detector()
|
||||
# Sets the trigger source to GATING
|
||||
self.parent.set_trigger(TriggerSource.GATING)
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize detector backend"""
|
||||
|
||||
# Std client
|
||||
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
|
||||
|
||||
# Stop writer
|
||||
self.std_client.stop_writer()
|
||||
|
||||
# Change e-account
|
||||
eacc = self.parent.scaninfo.username
|
||||
self.update_std_cfg("writer_user_id", int(eacc.strip(" e")))
|
||||
|
||||
signal_conditions = [(lambda: self.std_client.get_status()["state"], "READY")]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions, timeout=self.parent.timeout, all_signals=True
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Std client not in READY state, returns: {self.std_client.get_status()}"
|
||||
)
|
||||
|
||||
def update_std_cfg(self, cfg_key: str, value: Any) -> None:
|
||||
"""
|
||||
Update std_daq config
|
||||
|
||||
Checks that the new value matches the type of the former entry.
|
||||
|
||||
Args:
|
||||
cfg_key (str) : config key of value to be updated
|
||||
value (Any) : value to be updated for the specified key
|
||||
|
||||
Raises:
|
||||
Raises EigerError if the key was not in the config before and if the new value does not match the type of the old value
|
||||
|
||||
"""
|
||||
|
||||
# Load config from client and check old value
|
||||
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"
|
||||
f" {type(old_value)}:{old_value}"
|
||||
)
|
||||
|
||||
# Update config with new value and send back to client
|
||||
cfg.update({cfg_key: value})
|
||||
logger.debug(cfg)
|
||||
self.std_client.set_config(cfg)
|
||||
logger.debug(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}")
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop the detector"""
|
||||
|
||||
# Stop detector
|
||||
self.parent.cam.acquire.put(0)
|
||||
|
||||
# Check if detector returned in idle state
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
|
||||
"value"
|
||||
],
|
||||
DetectorState.IDLE,
|
||||
)
|
||||
]
|
||||
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout - self.parent.timeout // 2,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
# Retry stop detector and wait for remaining time
|
||||
self.parent.cam.acquire.put(0)
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout - self.parent.timeout // 2,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Failed to stop detector, detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Close file writer"""
|
||||
self.std_client.stop_writer()
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.set_detector_threshold()
|
||||
self.set_acquisition_params()
|
||||
self.parent.set_trigger(TriggerSource.GATING)
|
||||
|
||||
def set_detector_threshold(self) -> None:
|
||||
"""
|
||||
Set the detector threshold
|
||||
|
||||
The function sets the detector threshold automatically to 1/2 of the beam energy.
|
||||
"""
|
||||
|
||||
# get current beam energy from device manageer
|
||||
mokev = self.parent.device_manager.devices.mokev.obj.read()[
|
||||
self.parent.device_manager.devices.mokev.name
|
||||
]["value"]
|
||||
factor = 1
|
||||
|
||||
# Check if energies are eV or keV, assume keV as the default
|
||||
unit = getattr(self.parent.cam.threshold_energy, "units", None)
|
||||
if unit is not None and unit == "eV":
|
||||
factor = 1000
|
||||
|
||||
# set energy on detector
|
||||
setpoint = int(mokev * factor)
|
||||
energy = self.parent.cam.beam_energy.read()[self.parent.cam.beam_energy.name]["value"]
|
||||
if setpoint != energy:
|
||||
self.parent.cam.beam_energy.set(setpoint)
|
||||
|
||||
# set threshold on detector
|
||||
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
|
||||
"value"
|
||||
]
|
||||
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
||||
self.parent.cam.threshold_energy.set(setpoint / 2)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for the detector"""
|
||||
|
||||
# Set number of images and frames (frames is for internal burst of detector)
|
||||
self.parent.cam.num_images.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.cam.num_frames.put(1)
|
||||
|
||||
# Update the readout time of the detector
|
||||
self.update_readout_time()
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""Prepare the data backend for the scan"""
|
||||
self.parent.filepath = self.parent.filewriter.compile_full_filename(
|
||||
f"{self.parent.name}.h5"
|
||||
)
|
||||
self.filepath_exists(self.parent.filepath)
|
||||
self.stop_detector_backend()
|
||||
try:
|
||||
self.std_client.start_writer_async(
|
||||
{
|
||||
"output_file": self.parent.filepath,
|
||||
"n_images": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
time.sleep(5)
|
||||
if self.std_client.get_status()["state"] == "READY":
|
||||
raise EigerTimeoutError(f"Timeout of start_writer_async with {exc}") from exc
|
||||
|
||||
# Check status of std_daq
|
||||
signal_conditions = [
|
||||
(lambda: self.std_client.get_status()["acquisition"]["state"], "WAITING_IMAGES")
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=False,
|
||||
all_signals=True,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
"Timeout of 5s reached for std_daq start_writer_async with std_daq client status"
|
||||
f" {self.std_client.get_status()}"
|
||||
)
|
||||
|
||||
def filepath_exists(self, filepath: str) -> None:
|
||||
"""Check if filepath exists"""
|
||||
signal_conditions = [(lambda: os.path.exists(os.path.dirname(filepath)), True)]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=False,
|
||||
all_signals=True,
|
||||
):
|
||||
raise EigerError(f"Timeout of 3s reached for filepath {filepath}")
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm Eiger detector for acquisition"""
|
||||
self.parent.cam.acquire.put(1)
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
|
||||
"value"
|
||||
],
|
||||
DetectorState.RUNNING,
|
||||
)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""Checks if scan_id has changed and stops the scan if it has"""
|
||||
old_scan_id = self.parent.scaninfo.scan_id
|
||||
self.parent.scaninfo.load_scan_metadata()
|
||||
if self.parent.scaninfo.scan_id != old_scan_id:
|
||||
self.parent.stopped = True
|
||||
|
||||
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS.
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
"""
|
||||
pipe = self.parent.connector.pipeline()
|
||||
if successful is None:
|
||||
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath, done=done, successful=successful
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
@threadlocked
|
||||
def finished(self):
|
||||
"""Check if acquisition is finished."""
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.cam.acquire.read()[self.parent.cam.acquire.name]["value"],
|
||||
DetectorState.IDLE,
|
||||
),
|
||||
(lambda: self.std_client.get_status()["acquisition"]["state"], "FINISHED"),
|
||||
(
|
||||
lambda: self.std_client.get_status()["acquisition"]["stats"]["n_write_completed"],
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger),
|
||||
),
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
|
||||
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
|
||||
" the file writer"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
|
||||
class SLSDetectorCam(Device):
|
||||
"""
|
||||
SLS Detector Camera - Eiger9M
|
||||
|
||||
Base class to map EPICS PVs to ophyd signals.
|
||||
"""
|
||||
|
||||
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||
beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy")
|
||||
bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth")
|
||||
num_images = ADCpt(EpicsSignalWithRBV, "NumCycles")
|
||||
num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames")
|
||||
trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode")
|
||||
trigger_software = ADCpt(EpicsSignal, "TriggerSoftware")
|
||||
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger signals for Eiger9M detector"""
|
||||
|
||||
AUTO = 0
|
||||
TRIGGER = 1
|
||||
GATING = 2
|
||||
BURST_TRIGGER = 3
|
||||
|
||||
|
||||
class DetectorState(enum.IntEnum):
|
||||
"""Detector states for Eiger9M detector"""
|
||||
|
||||
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(PSIDetectorBase):
|
||||
"""
|
||||
Eiger9M detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
|
||||
Various EpicsPVs for controlling the detector
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = Eiger9MSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
# specify class attributes
|
||||
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||
|
||||
def set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||
"""Set trigger source for the detector.
|
||||
Check the TriggerSource enum for possible values
|
||||
|
||||
Args:
|
||||
trigger_source (TriggerSource): Trigger source for the detector
|
||||
|
||||
"""
|
||||
value = trigger_source
|
||||
self.cam.trigger_mode.put(value)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""
|
||||
Add functionality to stage, and arm the detector
|
||||
|
||||
Additional call to:
|
||||
- custom_prepare.arm_acquisition()
|
||||
"""
|
||||
rtr = super().stage()
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return rtr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)
|
@ -1,357 +0,0 @@
|
||||
import enum
|
||||
import os
|
||||
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||
from ophyd.mca import EpicsMCARecord
|
||||
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FalconError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class FalconTimeoutError(FalconError):
|
||||
"""Raised when the Falcon does not respond in time."""
|
||||
|
||||
|
||||
class DetectorState(enum.IntEnum):
|
||||
"""Detector states for Falcon detector"""
|
||||
|
||||
DONE = 0
|
||||
ACQUIRING = 1
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger source for Falcon detector"""
|
||||
|
||||
USER = 0
|
||||
GATE = 1
|
||||
SYNC = 2
|
||||
|
||||
|
||||
class MappingSource(enum.IntEnum):
|
||||
"""Mapping source for Falcon detector"""
|
||||
|
||||
SPECTRUM = 0
|
||||
MAPPING = 1
|
||||
|
||||
|
||||
class EpicsDXPFalcon(Device):
|
||||
"""
|
||||
DXP parameters for Falcon detector
|
||||
|
||||
Base class to map EPICS PVs from DXP parameters to ophyd signals.
|
||||
"""
|
||||
|
||||
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):
|
||||
"""
|
||||
HDF5 parameters for Falcon detector
|
||||
|
||||
Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.
|
||||
"""
|
||||
|
||||
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")
|
||||
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")
|
||||
queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config")
|
||||
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
|
||||
|
||||
|
||||
class FalconSetup(CustomDetectorMixin):
|
||||
"""
|
||||
Falcon setup class for cSAXS
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""
|
||||
Set default parameters for Falcon
|
||||
|
||||
This will set:
|
||||
- readout (float): readout time in seconds
|
||||
- value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
|
||||
|
||||
"""
|
||||
self.parent.value_pixel_per_buffer = 20
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize Falcon detector"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
self.parent.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
# 1 Realtime
|
||||
self.parent.preset_mode.put(1)
|
||||
# 0 Normal, 1 Inverted
|
||||
self.parent.input_logic_polarity.put(0)
|
||||
# 0 Manual 1 Auto
|
||||
self.parent.auto_pixels_per_buffer.put(0)
|
||||
# Sets the number of pixels/spectra in the buffer
|
||||
self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer)
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stops detector"""
|
||||
|
||||
self.parent.stop_all.put(1)
|
||||
self.parent.erase_all.put(1)
|
||||
|
||||
signal_conditions = [
|
||||
(lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE)
|
||||
]
|
||||
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout - self.parent.timeout // 2,
|
||||
all_signals=False,
|
||||
):
|
||||
# Retry stop detector and wait for remaining time
|
||||
raise FalconTimeoutError(
|
||||
f"Failed to stop detector, timeout with state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop the detector backend"""
|
||||
self.parent.hdf5.capture.put(0)
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize the detector backend for Falcon."""
|
||||
self.parent.hdf5.enable.put(1)
|
||||
# file location of h5 layout for cSAXS
|
||||
self.parent.hdf5.xml_file_name.put("layout.xml")
|
||||
# TODO Check if lazy open is needed and wanted!
|
||||
self.parent.hdf5.lazy_open.put(1)
|
||||
self.parent.hdf5.temp_suffix.put("")
|
||||
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
|
||||
self.parent.hdf5.queue_size.put(2000)
|
||||
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
|
||||
self.parent.nd_array_mode.put(1)
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for acquisition"""
|
||||
self.parent.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
|
||||
self.parent.pixels_per_run.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""Prepare data backend for acquisition"""
|
||||
self.parent.filepath = self.parent.filewriter.compile_full_filename(
|
||||
f"{self.parent.name}.h5"
|
||||
)
|
||||
file_path, file_name = os.path.split(self.parent.filepath)
|
||||
self.parent.hdf5.file_path.put(file_path)
|
||||
self.parent.hdf5.file_name.put(file_name)
|
||||
self.parent.hdf5.file_template.put("%s%s")
|
||||
self.parent.hdf5.num_capture.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.hdf5.file_write_mode.put(2)
|
||||
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
|
||||
self.parent.hdf5.array_counter.put(0)
|
||||
# Start file writing
|
||||
self.parent.hdf5.capture.put(1)
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm detector for acquisition"""
|
||||
self.parent.start_all.put(1)
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.state.read()[self.parent.state.name]["value"],
|
||||
DetectorState.ACQUIRING,
|
||||
)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise FalconTimeoutError(
|
||||
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""Checks if scan_id has changed and stops the scan if it has"""
|
||||
old_scan_id = self.parent.scaninfo.scan_id
|
||||
self.parent.scaninfo.load_scan_metadata()
|
||||
if self.parent.scaninfo.scan_id != old_scan_id:
|
||||
self.parent.stopped = True
|
||||
|
||||
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS.
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
"""
|
||||
pipe = self.parent.connector.pipeline()
|
||||
if successful is None:
|
||||
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath, done=done, successful=successful
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Check if scan finished succesfully"""
|
||||
total_frames = int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
)
|
||||
signal_conditions = [
|
||||
(self.parent.dxp.current_pixel.get, total_frames),
|
||||
(self.parent.hdf5.array_counter.get, total_frames),
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
logger.debug(
|
||||
f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()},"
|
||||
f" send data {self.parent.hdf5.array_counter.get()} from total_frames"
|
||||
f" {total_frames}"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
|
||||
class FalconcSAXS(PSIDetectorBase):
|
||||
"""
|
||||
Falcon Sitoro detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
|
||||
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
|
||||
mca (EpicsMCARecord) : MCA parameters for Falcon detector
|
||||
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = FalconSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
|
||||
# specify class attributes
|
||||
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
|
||||
mca = Cpt(EpicsMCARecord, "mca1")
|
||||
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
|
||||
|
||||
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||
start_all = Cpt(EpicsSignal, "StartAll")
|
||||
state = Cpt(EpicsSignal, "Acquiring")
|
||||
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")
|
||||
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)
|
||||
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")
|
||||
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
|
||||
|
||||
def set_trigger(
|
||||
self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0
|
||||
) -> None:
|
||||
"""
|
||||
Set triggering mode for detector
|
||||
|
||||
Args:
|
||||
mapping_mode (MappingSource): Mapping mode for the detector
|
||||
trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal
|
||||
ignore_gate (int): Ignore gate from TTL signal; defaults to 0
|
||||
|
||||
"""
|
||||
mapping = int(mapping_mode)
|
||||
trigger = trigger_source
|
||||
self.collect_mode.put(mapping)
|
||||
self.pixel_advance_mode.put(trigger)
|
||||
self.ignore_gate.put(ignore_gate)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""Stage"""
|
||||
rtr = super().stage()
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return rtr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)
|
@ -1,116 +0,0 @@
|
||||
import time
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
class FlomniSampleStorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniSampleStorage(Device):
|
||||
USER_ACCESS = [
|
||||
"is_sample_slot_used",
|
||||
"is_sample_in_gripper",
|
||||
"set_sample_slot",
|
||||
"unset_sample_slot",
|
||||
"set_sample_in_gripper",
|
||||
"unset_sample_in_gripper",
|
||||
"show_all",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
sample_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {}) for i in range(21)
|
||||
}
|
||||
sample_placed = Dcpt(sample_placed)
|
||||
|
||||
sample_names = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True})
|
||||
for i in range(21)
|
||||
}
|
||||
sample_names = Dcpt(sample_names)
|
||||
|
||||
sample_in_gripper = Cpt(
|
||||
EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET"
|
||||
)
|
||||
sample_in_gripper_name = Cpt(
|
||||
EpicsSignal,
|
||||
name="sample_in_gripper_name",
|
||||
read_pv="XOMNY-SAMPLE_DB_flomni100:GET.DESC",
|
||||
string=True,
|
||||
)
|
||||
|
||||
def __init__(self, prefix="", *, name, **kwargs):
|
||||
super().__init__(prefix, name=name, **kwargs)
|
||||
self.sample_placed.sample1.subscribe(self._emit_value)
|
||||
|
||||
def _emit_value(self, **kwargs):
|
||||
timestamp = kwargs.pop("timestamp", time.time())
|
||||
self.wait_for_connection()
|
||||
self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self)
|
||||
|
||||
def set_sample_slot(self, slot_nr: int, name: str) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
getattr(self.sample_placed, f"sample{slot_nr}").set(1)
|
||||
getattr(self.sample_names, f"sample{slot_nr}").set(name)
|
||||
|
||||
def unset_sample_slot(self, slot_nr: int) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
getattr(self.sample_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_names, f"sample{slot_nr}").set("-")
|
||||
|
||||
def set_sample_in_gripper(self, name: str) -> bool:
|
||||
self.sample_in_gripper.set(1)
|
||||
self.sample_in_gripper_name.set(name)
|
||||
|
||||
def unset_sample_in_gripper(self) -> bool:
|
||||
self.sample_in_gripper.set(0)
|
||||
self.sample_in_gripper_name.set("-")
|
||||
|
||||
def is_sample_slot_used(self, slot_nr: int) -> bool:
|
||||
val = getattr(self.sample_placed, f"sample{slot_nr}").get()
|
||||
return bool(val)
|
||||
|
||||
def is_sample_in_gripper(self) -> bool:
|
||||
val = self.sample_in_gripper.get()
|
||||
return bool(val)
|
||||
|
||||
def get_sample_name(self, slot_nr) -> str:
|
||||
val = getattr(self.sample_names, f"sample{slot_nr}").get()
|
||||
return str(val)
|
||||
|
||||
def show_all(self):
|
||||
t = PrettyTable()
|
||||
t.title = "flOMNI sample storage"
|
||||
field_names = [""]
|
||||
field_names.extend(str(ax) for ax in range(1, 11))
|
||||
for ct in range(0, 2):
|
||||
t.field_names = field_names
|
||||
row = ["Container " + str(ct)]
|
||||
row.extend(
|
||||
"used" if self.is_sample_slot_used(slot_nr) else "free"
|
||||
for slot_nr in range((ct * 10) + 1, (ct * 10) + 11)
|
||||
)
|
||||
t.add_row(row)
|
||||
print(t)
|
||||
print("\n\nFollowing samples are currently loaded:\n")
|
||||
for ct in range(1, 21):
|
||||
if self.is_sample_slot_used(ct):
|
||||
print(f" Position {ct:2.0f}: {self.get_sample_name(ct)}")
|
||||
if self.sample_in_gripper.get():
|
||||
print(f"\n Gripper: {self.sample_in_gripper_name.get()}\n")
|
||||
else:
|
||||
print(f"\n Gripper: no sample\n")
|
||||
|
||||
if self.is_sample_slot_used(0):
|
||||
print(f" flOMNI stage: {self.get_sample_name(0)}\n")
|
||||
else:
|
||||
print(f" flOMNI stage: no sample\n")
|
@ -1,311 +0,0 @@
|
||||
import enum
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages, threadlocked
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO
|
||||
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
from ophyd_devices.utils import bec_utils
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MCSError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class MCSTimeoutError(MCSError):
|
||||
"""Raise when MCS card runs into a timeout"""
|
||||
|
||||
|
||||
class TriggerSource(int, enum.Enum):
|
||||
"""Trigger source for mcs card - see manual for more information"""
|
||||
|
||||
MODE0 = 0
|
||||
MODE1 = 1
|
||||
MODE2 = 2
|
||||
MODE3 = 3
|
||||
MODE4 = 4
|
||||
MODE5 = 5
|
||||
MODE6 = 6
|
||||
|
||||
|
||||
class ChannelAdvance(int, enum.Enum):
|
||||
"""Channel advance pixel mode for mcs card - see manual for more information"""
|
||||
|
||||
INTERNAL = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class ReadoutMode(int, enum.Enum):
|
||||
"""Readout mode for mcs card - see manual for more information"""
|
||||
|
||||
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 MCSSetup(CustomDetectorMixin):
|
||||
"""Setup mixin class for the MCS card"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self._lock = threading.RLock()
|
||||
self._stream_ttl = 1800
|
||||
self.acquisition_done = False
|
||||
self.counter = 0
|
||||
self.n_points = 0
|
||||
self.mca_names = [
|
||||
signal for signal in self.parent.component_names if signal.startswith("mca")
|
||||
]
|
||||
self.mca_data = defaultdict(lambda: [])
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# External trigger for pixel advance
|
||||
self.parent.channel_advance.set(ChannelAdvance.EXTERNAL)
|
||||
# Use internal clock for channel 1
|
||||
self.parent.channel1_source.set(ChannelAdvance.INTERNAL)
|
||||
self.parent.user_led.set(0)
|
||||
# Set number of channels to 5
|
||||
self.parent.mux_output.set(5)
|
||||
# Trigger Mode used for cSAXS
|
||||
self.parent.set_trigger(TriggerSource.MODE3)
|
||||
# specify polarity of trigger signals
|
||||
self.parent.input_polarity.set(0)
|
||||
self.parent.output_polarity.set(1)
|
||||
# do not start counting on start
|
||||
self.parent.count_on_start.set(0)
|
||||
self.stop_detector()
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize detector backend"""
|
||||
for mca in self.mca_names:
|
||||
signal = getattr(self.parent, mca)
|
||||
signal.subscribe(self._on_mca_data, run=False)
|
||||
self.parent.current_channel.subscribe(self._progress_update, run=False)
|
||||
|
||||
def _progress_update(self, value, **kwargs) -> None:
|
||||
"""Progress update on the scan"""
|
||||
num_lines = self.parent.num_lines.get()
|
||||
max_value = self.parent.scaninfo.num_points
|
||||
# self.counter seems to be a deprecated variable from a former implementation of the mcs card
|
||||
# pylint: disable=protected-access
|
||||
self.parent._run_subs(
|
||||
sub_type=self.parent.SUB_PROGRESS,
|
||||
value=self.counter * int(self.parent.scaninfo.num_points / num_lines) + value,
|
||||
max_value=max_value,
|
||||
# TODO check if that is correct with
|
||||
done=bool(max_value == value), # == self.counter),
|
||||
)
|
||||
|
||||
@threadlocked
|
||||
def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None:
|
||||
"""Callback function for scan progress"""
|
||||
if not isinstance(value, (list, np.ndarray)):
|
||||
return
|
||||
self.mca_data[obj.attr_name] = value
|
||||
if len(self.mca_names) != len(self.mca_data):
|
||||
return
|
||||
self.acquisition_done = True
|
||||
self._send_data_to_bec()
|
||||
self.mca_data = defaultdict(lambda: [])
|
||||
|
||||
def _send_data_to_bec(self) -> None:
|
||||
"""Sends bundled data to BEC"""
|
||||
if self.parent.scaninfo.scan_msg is None:
|
||||
return
|
||||
metadata = self.parent.scaninfo.scan_msg.metadata
|
||||
metadata.update({"async_update": "append", "num_lines": self.parent.num_lines.get()})
|
||||
msg = messages.DeviceMessage(
|
||||
signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata
|
||||
)
|
||||
self.parent.connector.xadd(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scan_id=self.parent.scaninfo.scan_id, device=self.parent.name
|
||||
),
|
||||
msg={"data": msg},
|
||||
expire=self._stream_ttl,
|
||||
)
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.set_acquisition_params()
|
||||
self.parent.set_trigger(TriggerSource.MODE3)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for scan"""
|
||||
if self.parent.scaninfo.scan_type == "step":
|
||||
self.n_points = int(self.parent.scaninfo.frames_per_trigger) * int(
|
||||
self.parent.scaninfo.num_points
|
||||
)
|
||||
elif self.parent.scaninfo.scan_type == "fly":
|
||||
self.n_points = int(self.parent.scaninfo.num_points) # / int(self.num_lines.get()))
|
||||
else:
|
||||
raise MCSError(f"Scantype {self.parent.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.parent.num_use_all.set(self.n_points)
|
||||
self.parent.preset_real.set(0)
|
||||
|
||||
def prepare_detector_backend(self) -> None:
|
||||
"""Prepare detector backend for scan"""
|
||||
self.parent.erase_all.set(1)
|
||||
self.parent.read_mode.set(ReadoutMode.EVENT)
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm detector for acquisition"""
|
||||
self.counter = 0
|
||||
self.parent.erase_start.set(1)
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Check if acquisition is finished, if not successful, rais MCSTimeoutError"""
|
||||
signal_conditions = [
|
||||
(lambda: self.acquisition_done, True),
|
||||
(self.parent.acquiring.get, 0), # Considering making a enum.Int class for this state
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
total_frames = self.counter * int(
|
||||
self.parent.scaninfo.num_points / self.parent.num_lines.get()
|
||||
) + max(self.parent.current_channel.get(), 0)
|
||||
raise MCSTimeoutError(
|
||||
f"Reached timeout with mcs in state {self.parent.acquiring.get()} and"
|
||||
f" {total_frames} frames arriving at the mcs card"
|
||||
)
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.stop_all.set(1)
|
||||
|
||||
return super().stop_detector()
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop acquisition of data"""
|
||||
self.acquisition_done = True
|
||||
|
||||
|
||||
class SIS38XX(Device):
|
||||
"""SIS38XX card for access to EPICs PVs at cSAXS beamline"""
|
||||
|
||||
|
||||
class MCScSAXS(PSIDetectorBase):
|
||||
"""MCS card for cSAXS for implementation at cSAXS beamline"""
|
||||
|
||||
USER_ACCESS = ["describe", "_init_mcs"]
|
||||
SUB_PROGRESS = "progress"
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = MCSSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 0
|
||||
|
||||
# PV access to SISS38XX card
|
||||
# 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")
|
||||
|
||||
# PV access to MCA signals
|
||||
mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True)
|
||||
mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True)
|
||||
mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True)
|
||||
current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True)
|
||||
|
||||
# Custom signal readout from device config
|
||||
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:
|
||||
# pylint: disable=expression-not-assigned
|
||||
[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,
|
||||
device_manager=device_manager,
|
||||
sim_mode=sim_mode,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||
"""Set trigger mode from TriggerSource"""
|
||||
value = int(trigger_source)
|
||||
self.input_mode.set(value)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""stage the detector for upcoming acquisition"""
|
||||
rtr = super().stage()
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return rtr
|
||||
|
||||
|
||||
# Automatically connect to test environmenr if directly invoked
|
||||
if __name__ == "__main__":
|
||||
mcs = MCScSAXS(name="mcs", prefix="X12SA-MCS:", sim_mode=True)
|
@ -1,417 +0,0 @@
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import ADComponent as ADCpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged
|
||||
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MIN_READOUT = 3e-3
|
||||
|
||||
|
||||
class PilatusError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class PilatusTimeoutError(PilatusError):
|
||||
"""Raised when the Pilatus does not respond in time during unstage."""
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger source options for the detector"""
|
||||
|
||||
INTERNAL = 0
|
||||
EXT_ENABLE = 1
|
||||
EXT_TRIGGER = 2
|
||||
MULTI_TRIGGER = 3
|
||||
ALGINMENT = 4
|
||||
|
||||
|
||||
class SLSDetectorCam(Device):
|
||||
"""SLS Detector Camera - Pilatus
|
||||
|
||||
Base class to map EPICS PVs to ophyd signals.
|
||||
"""
|
||||
|
||||
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
|
||||
num_frames = 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", string=True)
|
||||
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 PilatusSetup(CustomDetectorMixin):
|
||||
"""Pilatus setup class for cSAXS
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Set default parameters for Eiger9M detector"""
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# Stops the detector
|
||||
self.stop_detector()
|
||||
# Sets the trigger source to GATING
|
||||
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""
|
||||
Prepare detector for scan.
|
||||
|
||||
Includes checking the detector threshold,
|
||||
setting the acquisition parameters and setting the trigger source
|
||||
"""
|
||||
self.set_detector_threshold()
|
||||
self.set_acquisition_params()
|
||||
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
|
||||
|
||||
def set_detector_threshold(self) -> None:
|
||||
"""
|
||||
Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance
|
||||
|
||||
Threshold might be in ev or keV
|
||||
"""
|
||||
|
||||
# get current beam energy from device manageer
|
||||
mokev = self.parent.device_manager.devices.mokev.obj.read()[
|
||||
self.parent.device_manager.devices.mokev.name
|
||||
]["value"]
|
||||
factor = 1
|
||||
|
||||
# Check if energies are eV or keV, assume keV as the default
|
||||
unit = getattr(self.parent.cam.threshold_energy, "units", None)
|
||||
if unit is not None and unit == "eV":
|
||||
factor = 1000
|
||||
|
||||
# set energy on detector
|
||||
setpoint = int(mokev * factor)
|
||||
|
||||
# set threshold on detector
|
||||
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
|
||||
"value"
|
||||
]
|
||||
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
||||
self.parent.cam.threshold_energy.set(setpoint / 2)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for the detector"""
|
||||
|
||||
# Set number of images and frames (frames is for internal burst of detector)
|
||||
self.parent.cam.num_images.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.cam.num_frames.put(1)
|
||||
|
||||
# Update the readout time of the detector
|
||||
self.update_readout_time()
|
||||
|
||||
def create_directory(self, filepath: str) -> None:
|
||||
"""Create directory if it does not exist"""
|
||||
os.makedirs(filepath, exist_ok=True)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop the file writer zmq service for pilatus_2"""
|
||||
self.close_file_writer()
|
||||
time.sleep(0.1)
|
||||
self.stop_file_writer()
|
||||
time.sleep(0.1)
|
||||
|
||||
def close_file_writer(self) -> None:
|
||||
"""
|
||||
Close the file writer for pilatus_2
|
||||
|
||||
Delete the data from x12sa-pd-2
|
||||
|
||||
"""
|
||||
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||
try:
|
||||
res = self.send_requests_delete(url=url)
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
logger.info(f"Pilatus2 close threw Exception: {exc}")
|
||||
|
||||
def stop_file_writer(self) -> None:
|
||||
"""
|
||||
Stop the file writer for pilatus_2
|
||||
|
||||
Runs on xbl-daq-34
|
||||
"""
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/stop"
|
||||
res = self.send_requests_put(url=url)
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""
|
||||
Prepare the detector backend of pilatus for a scan
|
||||
|
||||
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.stop_detector_backend()
|
||||
|
||||
self.parent.filepath = self.parent.filewriter.compile_full_filename("pilatus_2.h5")
|
||||
self.parent.cam.file_path.put("/dev/shm/zmq/")
|
||||
self.parent.cam.file_name.put(
|
||||
f"{self.parent.scaninfo.username}_2_{self.parent.scaninfo.scan_number:05d}"
|
||||
)
|
||||
self.parent.cam.auto_increment.put(1) # auto increment
|
||||
self.parent.cam.file_number.put(0) # first iter
|
||||
self.parent.cam.file_format.put(0) # 0: TIFF
|
||||
self.parent.cam.file_template.put("%s%s_%5.5d.cbf")
|
||||
|
||||
# TODO better to remove hard coded path with link to home directory/pilatus_2
|
||||
basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/"
|
||||
self.parent.filepath_raw = os.path.join(
|
||||
basepath,
|
||||
self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5),
|
||||
)
|
||||
# Make directory if needed
|
||||
self.create_directory(self.parent.filepath_raw)
|
||||
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
# start the stream on x12sa-pd-2
|
||||
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||
data_msg = {
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": self.parent.filepath_raw,
|
||||
}
|
||||
]
|
||||
}
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
# start the data receiver on xbl-daq-34
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/run"
|
||||
data_msg = [
|
||||
"zmqWriter",
|
||||
self.parent.scaninfo.username,
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": self.parent.scaninfo.username,
|
||||
},
|
||||
]
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
# Wait for server to become available again
|
||||
time.sleep(0.1)
|
||||
logger.info(f"{res.status_code} -{res.text} - {res.content}")
|
||||
|
||||
# Send requests.put to xbl-daq-34 to wait for data
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/wait"
|
||||
data_msg = [
|
||||
"zmqWriter",
|
||||
self.parent.scaninfo.username,
|
||||
{
|
||||
"frmCnt": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
"timeout": 2000,
|
||||
},
|
||||
]
|
||||
try:
|
||||
res = self.send_requests_put(url=url, data=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 send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object:
|
||||
"""
|
||||
Send a put request to the given url
|
||||
|
||||
Args:
|
||||
url (str): url to send the request to
|
||||
data (dict): data to be sent with the request (optional)
|
||||
headers (dict): headers to be sent with the request (optional)
|
||||
|
||||
Returns:
|
||||
status code of the request
|
||||
"""
|
||||
return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5)
|
||||
|
||||
def send_requests_delete(self, url: str, headers: dict = None) -> object:
|
||||
"""
|
||||
Send a delete request to the given url
|
||||
|
||||
Args:
|
||||
url (str): url to send the request to
|
||||
headers (dict): headers to be sent with the request (optional)
|
||||
|
||||
Returns:
|
||||
status code of the request
|
||||
"""
|
||||
return requests.delete(url=url, headers=headers, timeout=5)
|
||||
|
||||
def pre_scan(self) -> None:
|
||||
"""
|
||||
Pre_scan function call
|
||||
|
||||
This function is called just before the scan core.
|
||||
Here it is used to arm the detector for the acquisition
|
||||
|
||||
"""
|
||||
self.arm_acquisition()
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arms the detector for the acquisition"""
|
||||
self.parent.cam.acquire.put(1)
|
||||
# TODO is this sleep needed? to be tested with detector and for how long
|
||||
time.sleep(0.5)
|
||||
|
||||
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS and publish the event for the h5_converter
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
"""
|
||||
pipe = self.parent.connector.pipeline()
|
||||
if successful is None:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath,
|
||||
done=done,
|
||||
metadata={"input_path": self.parent.filepath_raw},
|
||||
)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath,
|
||||
done=done,
|
||||
successful=successful,
|
||||
metadata={"input_path": self.parent.filepath_raw},
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Check if acquisition is finished."""
|
||||
# pylint: disable=protected-access
|
||||
# TODO: at the moment this relies on device.mcs.obj._staged attribute
|
||||
signal_conditions = [
|
||||
(lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
raise PilatusTimeoutError(
|
||||
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
|
||||
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
|
||||
" the file writer"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.cam.acquire.put(0)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""Checks if scan_id has changed and stops the scan if it has"""
|
||||
old_scan_id = self.parent.scaninfo.scan_id
|
||||
self.parent.scaninfo.load_scan_metadata()
|
||||
if self.parent.scaninfo.scan_id != old_scan_id:
|
||||
self.parent.stopped = True
|
||||
|
||||
|
||||
class PilatuscSAXS(PSIDetectorBase):
|
||||
"""Pilatus_2 300k detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
cam (SLSDetectorCam) : Detector camera
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = PilatusSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
# specify class attributes
|
||||
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||
|
||||
def set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||
"""Set trigger source for the detector"""
|
||||
value = trigger_source
|
||||
self.cam.trigger_mode.put(value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)
|
@ -1,3 +1,5 @@
|
||||
""" TODO This class was never properly tested, it is solely a first draft and should be tested and extended before use! """
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
from .fgalil_ophyd import FlomniGalilController, FlomniGalilMotor
|
||||
from .fupr_ophyd import FuprGalilController, FuprGalilMotor
|
||||
from .galil_ophyd import GalilController, GalilMotor
|
||||
from .sgalil_ophyd import SGalilMotor
|
@ -1,201 +0,0 @@
|
||||
<mxfile host="app.diagrams.net" modified="2023-07-31T14:55:08.470Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0" etag="hKC99drhJ4x6bY93jFGH" version="21.6.6" type="device">
|
||||
<diagram name="Page-1" id="b520641d-4fe3-3701-9064-5fc419738815">
|
||||
<mxGraphModel dx="2049" dy="1206" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="21ea969265ad0168-6" value="SGalil stages" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="12.5" y="24" width="160" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-7" value="grid fly scan (2D), TTL signal at the beginning of each line" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-6" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="54" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-14" value="DelayGenerator 4 - ddg4" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="40" width="160" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-15" value="ext. trigger" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-14" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-16" value="operated in normal mode, controls fast shutter and triggers second DDG" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-14" vertex="1">
|
||||
<mxGeometry y="52" width="160" height="48" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-19" value="<div>channel AB</div>" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="21ea969265ad0168-14">
|
||||
<mxGeometry y="100" width="160" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-21" value="<div>channel T0<br></div>" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="21ea969265ad0168-14">
|
||||
<mxGeometry y="130" width="160" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-22" value="Logical Card" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="710" y="250" width="160" height="104" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-24" value="<div>both signals low -&gt; low<br>either on signal high -&gt; high<br>both signals high -&gt; low<br><br></div>" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-22" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="54" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-26" value="MCS readout card" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="920" y="360" width="160" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-27" value="falling edge trigger<br>readout is triggered between to falling edges, thus logical board required" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-26" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="74" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-36" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21ea969265ad0168-6" target="21ea969265ad0168-15" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-38" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;align=center;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="O8qpyw_Cq4v1m74naMs6-19" target="O8qpyw_Cq4v1m74naMs6-3" edge="1">
|
||||
<mxGeometry x="-0.0026" relative="1" as="geometry">
|
||||
<mxPoint x="430" y="150" as="sourcePoint" />
|
||||
<mxPoint x="680" y="60" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="155" />
|
||||
<mxPoint x="660" y="43" />
|
||||
</Array>
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-42" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitDx=0;exitDy=0;" parent="1" source="O8qpyw_Cq4v1m74naMs6-19" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="155" />
|
||||
<mxPoint x="660" y="290" />
|
||||
</Array>
|
||||
<mxPoint x="710" y="290" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-43" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.378;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="21ea969265ad0168-24" edge="1" target="21ea969265ad0168-27">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="880" y="331" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-2" value="Fast shutter: fsh" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="750" width="160" height="104" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-3" value="FSH opening time, e.g. 20ms" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-2">
|
||||
<mxGeometry y="26" width="160" height="34" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-11" value="TTL" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="60" width="40" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-16" value="DelayGenerator 4 - ddg4" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="270" y="350" width="230" height="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-17" value="ext. trigger" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="26" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-18" value="operated in burst mode:<br>burstCount: N_points<br>burstPeriod: (exp_time + readout time)<br>burstDelay: fsh opening (20ms)" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="50" width="230" height="68" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-29" value="Channel AB" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="118" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-28" value="Channel CD " style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="142" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-30" value="Channel EF" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="166" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-27" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;align=center;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-21" target="O8qpyw_Cq4v1m74naMs6-17">
|
||||
<mxGeometry x="-0.0026" relative="1" as="geometry">
|
||||
<mxPoint x="220" y="262" as="sourcePoint" />
|
||||
<mxPoint x="240" y="390" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="510" y="185" />
|
||||
<mxPoint x="510" y="270" />
|
||||
<mxPoint x="200" y="270" />
|
||||
<mxPoint x="200" y="390" />
|
||||
</Array>
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-31" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-36">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="640" y="504" />
|
||||
<mxPoint x="640" y="610" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="480" as="sourcePoint" />
|
||||
<mxPoint x="910" y="610" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-32" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-34">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="640" y="504" />
|
||||
<mxPoint x="640" y="610" />
|
||||
<mxPoint x="760" y="610" />
|
||||
<mxPoint x="760" y="528" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="500" as="sourcePoint" />
|
||||
<mxPoint x="880" y="540" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-33" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0.008;entryY=0.154;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-38">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="640" y="504" />
|
||||
<mxPoint x="640" y="610" />
|
||||
<mxPoint x="760" y="610" />
|
||||
<mxPoint x="760" y="701" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="480" as="sourcePoint" />
|
||||
<mxPoint x="880" y="700" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-34" value="eiger" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="920" y="510" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-35" value="rising edge<br>set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-34">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-36" value="falcon" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="920" y="600" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-37" value="rising edge <br>set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-36">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-38" value="Pilatus 300k" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="920" y="690" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-39" value="rising edge <br>set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-38">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-40" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=-0.005;entryY=1.002;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-29" target="21ea969265ad0168-24">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="560" y="480" />
|
||||
<mxPoint x="560" y="330" />
|
||||
</Array>
|
||||
<mxPoint x="510" y="520" as="sourcePoint" />
|
||||
<mxPoint x="700" y="330" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-41" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitX=1;exitY=0.167;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-30">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="530" y="520" />
|
||||
<mxPoint x="530" y="780" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="530" as="sourcePoint" />
|
||||
<mxPoint x="610" y="780" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-42" value="SGalil positions encoder" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="610" y="740" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-43" value="triggered on falling edge" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-42">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-44" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel AB</span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: fsh opening (20ms)<br>+ N_points * exp_time<br>+ (N_points-1) * readout time<br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="44" width="180" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-45" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel AB</span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: exp_time <br>or less<br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="530" y="300" width="110" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-46" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel CD<br>split into 3 signals<br></span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: exp_time <br>or less<br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="575" y="450" width="120" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-47" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel EF<br></span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: exp_time/2 <br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="480" y="610" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
Binary file not shown.
Before Width: | Height: | Size: 157 KiB |
@ -1,370 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
|
||||
from ophyd_devices.galil.galil_ophyd import (
|
||||
BECConfigError,
|
||||
GalilAxesReferenced,
|
||||
GalilController,
|
||||
GalilError,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilSetpointSignal,
|
||||
GalilSignalRO,
|
||||
retry_once,
|
||||
)
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FlomniGalilController(GalilController):
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"drive_axis_to_limit",
|
||||
"find_reference",
|
||||
"get_motor_limit_switch",
|
||||
"fosaz_light_curtain_is_triggered",
|
||||
"is_motor_on",
|
||||
"all_axes_referenced",
|
||||
"lights_off",
|
||||
"lights_on",
|
||||
]
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
if axis_Id is None and axis_Id_numeric is not None:
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
active_thread = self.is_thread_active(0)
|
||||
motor_is_on = self.is_motor_on(axis_Id)
|
||||
return bool(active_thread or motor_is_on)
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
# TODO: check if all axes are referenced in all controllers
|
||||
return super().all_axes_referenced()
|
||||
|
||||
def fosaz_light_curtain_is_triggered(self) -> bool:
|
||||
"""
|
||||
Check the light curtain status for fosaz
|
||||
|
||||
Returns:
|
||||
bool: True if the light curtain is triggered
|
||||
"""
|
||||
|
||||
return int(float(self.socket_put_and_receive("MG @IN[14]").strip())) == 1
|
||||
|
||||
def lights_off(self) -> None:
|
||||
"""
|
||||
Turn off the lights
|
||||
"""
|
||||
self.socket_put_confirmed("CB15")
|
||||
|
||||
def lights_on(self) -> None:
|
||||
"""
|
||||
Turn on the lights
|
||||
"""
|
||||
self.socket_put_confirmed("SB15")
|
||||
|
||||
|
||||
class FlomniGalilReadbackSignal(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
return val
|
||||
|
||||
|
||||
class FlomniGalilSetpointSignal(GalilSetpointSignal):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.all_axes_referenced()
|
||||
if axes_referenced:
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}")
|
||||
self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}")
|
||||
self.controller.socket_put_confirmed("movereq=1")
|
||||
self.controller.socket_put_confirmed("XQ#NEWPAR")
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.005)
|
||||
else:
|
||||
raise GalilError("Not all axes are referenced.")
|
||||
|
||||
|
||||
class FlomniGalilMotorResolution(GalilMotorResolution):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniGalilMotorIsMoving(GalilMotorIsMoving):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniGalilAxesReferenced(GalilAxesReferenced):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(FlomniGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(
|
||||
FlomniGalilAxesReferenced, signal_name="all_axes_referenced", kind="config"
|
||||
)
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = FlomniGalilController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.axis_Id = axis_Id
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.sign = sign
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
self.device_mapping = kwargs.pop("device_mapping", {})
|
||||
self.device_manager = device_manager
|
||||
|
||||
if len(self.device_mapping) > 0 and self.device_manager is None:
|
||||
raise BECConfigError(
|
||||
"device_mapping has been specified but the device_manager cannot be accessed."
|
||||
)
|
||||
self.rt = self.device_mapping.get("rt")
|
||||
self.pid_x_correction = 0
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
logger.info("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.1)
|
||||
val = self.readback.read()
|
||||
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||
|
||||
if not success:
|
||||
print(" stop")
|
||||
self._done_moving(success=success)
|
||||
logger.info("Move finished")
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError("Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError("Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# mock = False
|
||||
# if not mock:
|
||||
# leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
|
||||
# leyey.stage()
|
||||
# status = leyey.move(0, wait=True)
|
||||
# status = leyey.move(10, wait=True)
|
||||
# leyey.read()
|
||||
|
||||
# leyey.get()
|
||||
# leyey.describe()
|
||||
|
||||
# leyey.unstage()
|
||||
# else:
|
||||
# from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
# leyex = GalilMotor(
|
||||
# "G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
# )
|
||||
# leyey = GalilMotor(
|
||||
# "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
# )
|
||||
# leyex.stage()
|
||||
# # leyey.stage()
|
||||
|
||||
# leyex.controller.galil_show_all()
|
@ -1,318 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from ophyd_devices.galil.galil_ophyd import (
|
||||
BECConfigError,
|
||||
GalilAxesReferenced,
|
||||
GalilCommunicationError,
|
||||
GalilController,
|
||||
GalilError,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilReadbackSignal,
|
||||
GalilSetpointSignal,
|
||||
retry_once,
|
||||
)
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FuprGalilController(GalilController):
|
||||
_axes_per_controller = 1
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
if axis_Id is None and axis_Id_numeric is not None:
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
|
||||
return is_moving
|
||||
|
||||
def axis_is_referenced(self, axis_Id) -> bool:
|
||||
return self.all_axes_referenced()
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
return bool(float(self.socket_put_and_receive("MG axisref").strip()))
|
||||
|
||||
def drive_axis_to_limit(self, axis_Id_numeric, direction: str) -> None:
|
||||
raise NotImplementedError("This function is not implemented for the FuprGalilController.")
|
||||
|
||||
|
||||
class FuprGalilReadbackSignal(GalilReadbackSignal):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TP{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
if self.parent.axis_Id_numeric == 0:
|
||||
try:
|
||||
rt = self.parent.device_manager.devices[self.parent.rt]
|
||||
if rt.enabled:
|
||||
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"])
|
||||
except KeyError:
|
||||
logger.warning("Failed to set RT value during readback.")
|
||||
return val
|
||||
|
||||
|
||||
class FuprGalilSetpointSignal(GalilSetpointSignal):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.all_axes_referenced()
|
||||
if axes_referenced:
|
||||
self.controller.socket_put_confirmed(
|
||||
f"PA{self.parent.axis_Id}={int(self.setpoint*self.parent.MOTOR_RESOLUTION)}"
|
||||
)
|
||||
self.controller.socket_put_confirmed(f"BG{self.parent.axis_Id}")
|
||||
else:
|
||||
raise GalilError("Not all axes are referenced.")
|
||||
|
||||
|
||||
class FuprGalilMotorResolution(GalilMotorResolution):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.parent.MOTOR_RESOLUTION
|
||||
|
||||
|
||||
class FuprGalilMotorIsMoving(GalilMotorIsMoving):
|
||||
pass
|
||||
|
||||
|
||||
class FuprGalilAxesReferenced(GalilAxesReferenced):
|
||||
pass
|
||||
|
||||
|
||||
class FuprGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
MOTOR_RESOLUTION = 25600
|
||||
readback = Cpt(FuprGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(
|
||||
FuprGalilAxesReferenced, signal_name="all_axes_referenced", kind="config"
|
||||
)
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = FuprGalilController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.axis_Id = axis_Id
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.sign = sign
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
self.device_mapping = kwargs.pop("device_mapping", {})
|
||||
self.device_manager = device_manager
|
||||
|
||||
if len(self.device_mapping) > 0 and self.device_manager is None:
|
||||
raise BECConfigError(
|
||||
"device_mapping has been specified but the device_manager cannot be accessed."
|
||||
)
|
||||
self.rt = self.device_mapping.get("rt", "rtx")
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
logger.info("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.1)
|
||||
val = self.readback.read()
|
||||
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||
|
||||
if not success:
|
||||
print(" stop")
|
||||
self._done_moving(success=success)
|
||||
logger.info("Move finished")
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
@ -1,605 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class GalilCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GalilError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a Galil communication error was raised. This may happen if the buffer was not empty."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
val = fcn(self, *args, **kwargs)
|
||||
except (GalilCommunicationError, GalilError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class GalilController(Controller):
|
||||
_axes_per_controller = 8
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"lgalil_is_air_off_and_orchestra_enabled",
|
||||
"drive_axis_to_limit",
|
||||
"find_reference",
|
||||
"get_motor_limit_switch",
|
||||
"is_motor_on",
|
||||
"all_axes_referenced",
|
||||
]
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\r".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
@retry_once
|
||||
def socket_put_confirmed(self, val: str) -> None:
|
||||
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
|
||||
|
||||
Args:
|
||||
val (str): Message that should be sent to the socket
|
||||
|
||||
Raises:
|
||||
GalilCommunicationError: Raised if the return value is not a colon.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(val)
|
||||
if return_val != ":":
|
||||
raise GalilCommunicationError(
|
||||
f"Expected return value of ':' but instead received {return_val}"
|
||||
)
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
if axis_Id is None and axis_Id_numeric is not None:
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
|
||||
backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[axis]")) != 0)
|
||||
return bool(
|
||||
is_moving or backlash_is_active or self.is_thread_active(0) or self.is_thread_active(2)
|
||||
)
|
||||
|
||||
def is_thread_active(self, thread_id: int) -> bool:
|
||||
val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
if val == -1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
def stop_all_axes(self) -> str:
|
||||
return self.socket_put_and_receive(f"XQ#STOP,1")
|
||||
|
||||
def lgalil_is_air_off_and_orchestra_enabled(self) -> bool:
|
||||
# TODO: move this to the LamNI-specific controller
|
||||
rt_not_blocked_by_galil = bool(self.socket_put_and_receive(f"MG@OUT[9]"))
|
||||
air_off = bool(self.socket_put_and_receive(f"MG@OUT[13]"))
|
||||
return rt_not_blocked_by_galil and air_off
|
||||
|
||||
def axis_is_referenced(self, axis_Id_numeric) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG axisref[{axis_Id_numeric}]").strip()))
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
"""
|
||||
Check if all axes are referenced.
|
||||
"""
|
||||
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
|
||||
|
||||
def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str) -> None:
|
||||
"""
|
||||
Drive an axis to the limit in a specified direction.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number
|
||||
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
|
||||
"""
|
||||
if direction == "forward":
|
||||
direction_flag = 1
|
||||
elif direction == "reverse":
|
||||
direction_flag = -1
|
||||
else:
|
||||
raise ValueError(f"Invalid direction {direction}")
|
||||
|
||||
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
|
||||
self.socket_put_confirmed(f"ndir={direction_flag}")
|
||||
self.socket_put_confirmed("XQ#NEWPAR")
|
||||
time.sleep(0.005)
|
||||
self.socket_put_confirmed("XQ#FES")
|
||||
time.sleep(0.01)
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.01)
|
||||
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
# check if we actually hit the limit
|
||||
if direction == "forward":
|
||||
limit = self.get_motor_limit_switch(axis_Id)[1]
|
||||
elif direction == "reverse":
|
||||
limit = self.get_motor_limit_switch(axis_Id)[0]
|
||||
|
||||
if not limit:
|
||||
raise GalilError(f"Failed to drive axis {axis_Id}/{axis_Id_numeric} to limit.")
|
||||
|
||||
def find_reference(self, axis_Id_numeric: int) -> None:
|
||||
"""
|
||||
Find the reference of an axis.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number
|
||||
"""
|
||||
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
|
||||
self.socket_put_and_receive("XQ#NEWPAR")
|
||||
self.socket_put_confirmed("XQ#FRM")
|
||||
time.sleep(0.1)
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.1)
|
||||
|
||||
if not self.axis_is_referenced(axis_Id_numeric):
|
||||
raise GalilError(f"Failed to find reference of axis {axis_Id_numeric}.")
|
||||
|
||||
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
|
||||
|
||||
def show_running_threads(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [str(ax) for ax in range(self._axes_per_controller)]
|
||||
t.add_row(
|
||||
[
|
||||
"active" if self.is_thread_active(t) else "inactive"
|
||||
for t in range(self._axes_per_controller)
|
||||
]
|
||||
)
|
||||
print(t)
|
||||
|
||||
def is_motor_on(self, axis_Id) -> bool:
|
||||
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
|
||||
|
||||
def get_motor_limit_switch(self, axis_Id) -> list:
|
||||
"""
|
||||
Get the status of the motor limit switches.
|
||||
|
||||
Args:
|
||||
axis_Id (str): Axis identifier (e.g. 'A', 'B', 'C', ...)
|
||||
|
||||
Returns:
|
||||
list: List of two booleans indicating if the low and high limit switch is active, respectively.
|
||||
"""
|
||||
ret = self.socket_put_and_receive(f"MG _LR{axis_Id}, _LF{axis_Id}")
|
||||
low, high = ret.strip().split(" ")
|
||||
return [not bool(float(low)), not bool(float(high))]
|
||||
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [
|
||||
"Axis",
|
||||
"Name",
|
||||
"Connected",
|
||||
"Referenced",
|
||||
"Motor On",
|
||||
"Limits",
|
||||
"Position",
|
||||
]
|
||||
for ax in range(self._axes_per_controller):
|
||||
axis = self._axis[ax]
|
||||
if axis is not None:
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
axis.name,
|
||||
axis.connected,
|
||||
self.axis_is_referenced(axis.axis_Id_numeric),
|
||||
self.is_motor_on(axis.axis_Id),
|
||||
self.get_motor_limit_switch(axis.axis_Id),
|
||||
axis.readback.read().get(axis.name).get("value"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row([None for t in t.field_names])
|
||||
print(t)
|
||||
|
||||
self.show_running_threads()
|
||||
|
||||
def galil_show_all(self) -> None:
|
||||
for controller in self._controller_instances.values():
|
||||
if isinstance(controller, GalilController):
|
||||
controller.describe()
|
||||
|
||||
@staticmethod
|
||||
def axis_Id_to_numeric(axis_Id: str) -> int:
|
||||
return ord(axis_Id.lower()) - 97
|
||||
|
||||
@staticmethod
|
||||
def axis_Id_numeric_to_alpha(axis_Id_numeric: int) -> str:
|
||||
return (chr(axis_Id_numeric + 97)).capitalize()
|
||||
|
||||
|
||||
class GalilSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class GalilSignalRO(GalilSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class GalilReadbackSignal(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
try:
|
||||
rt = self.parent.device_manager.devices[self.parent.rt]
|
||||
if rt.enabled:
|
||||
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"])
|
||||
except KeyError:
|
||||
logger.warning("Failed to set RT value during readback.")
|
||||
return val
|
||||
|
||||
|
||||
class GalilSetpointSignal(GalilSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint * self.parent.sign
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.all_axes_referenced()
|
||||
if axes_referenced:
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
angle_status = self.parent.device_manager.devices[
|
||||
self.parent.rt
|
||||
].obj.controller.feedback_status_angle_lamni()
|
||||
|
||||
if angle_status:
|
||||
self.controller.socket_put_confirmed("angintf=1")
|
||||
|
||||
self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}")
|
||||
self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}")
|
||||
self.controller.socket_put_confirmed("movereq=1")
|
||||
self.controller.socket_put_confirmed("XQ#NEWPAR")
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.005)
|
||||
else:
|
||||
raise GalilError("Not all axes are referenced.")
|
||||
|
||||
|
||||
class GalilMotorResolution(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return float(
|
||||
self.controller.socket_put_and_receive(f"MG stppermm[{self.parent.axis_Id_numeric}]")
|
||||
)
|
||||
|
||||
|
||||
class GalilMotorIsMoving(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class GalilAxesReferenced(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.all_axes_referenced()
|
||||
|
||||
|
||||
class GalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = GalilController(socket_cls=socket_cls, socket_host=host, socket_port=port)
|
||||
self.axis_Id = axis_Id
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.sign = sign
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
self.device_mapping = kwargs.pop("device_mapping", {})
|
||||
self.device_manager = device_manager
|
||||
|
||||
if len(self.device_mapping) > 0 and self.device_manager is None:
|
||||
raise BECConfigError(
|
||||
"device_mapping has been specified but the device_manager cannot be accessed."
|
||||
)
|
||||
self.rt = self.device_mapping.get("rt")
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
logger.info("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.1)
|
||||
val = self.readback.read()
|
||||
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||
|
||||
if not success:
|
||||
print(" stop")
|
||||
self._done_moving(success=success)
|
||||
logger.info("Move finished")
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = self.controller.axis_Id_to_numeric(val)
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = self.controller.axis_Id_numeric_to_alpha(val)
|
||||
self._axis_Id_numeric = val
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# pytest: skip-file
|
||||
mock = False
|
||||
if not mock:
|
||||
leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
|
||||
leyey.stage()
|
||||
status = leyey.move(0, wait=True)
|
||||
status = leyey.move(10, wait=True)
|
||||
leyey.read()
|
||||
|
||||
leyey.get()
|
||||
leyey.describe()
|
||||
|
||||
leyey.unstage()
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
leyex = GalilMotor(
|
||||
"G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyey = GalilMotor(
|
||||
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyex.stage()
|
||||
# leyey.stage()
|
||||
|
||||
leyex.controller.galil_show_all()
|
@ -1,415 +0,0 @@
|
||||
'******************************************************************************
|
||||
' scanning saxs stages controller code
|
||||
' version 0.1 20160113, holler, based on example code
|
||||
' version 0.2 20160321, holler, small adjustments
|
||||
' version 0.3 20160323, position sampling
|
||||
' version 0.4 grid scan implemented
|
||||
' version 0.5 20160426, shutter control from controller
|
||||
' version 0.6 20160614, code for manual stage tuning added
|
||||
' version 0.7 20170609, prefact added
|
||||
' version 0.8 20190327, stepper motor x axis E, encoder axis A
|
||||
' DC motor y axis C
|
||||
' version 0.9 20190403, Pos sampling averaging and ring buffered
|
||||
' internal grid scan updated
|
||||
' 20190507 various fixes during comm. at beamline
|
||||
' off now in microns for higher resolution
|
||||
' version 1.1 20191021, position samples were off compared to xrays
|
||||
' use AL and RL commands for position latch
|
||||
' to reduce delay for axis C (continuous)
|
||||
' switch to DI3 required, averaging removed
|
||||
' version 1.2 20200500, switch stepper motor to axis F
|
||||
' because motor driver axis E defective
|
||||
' version 2.0 20230816, adjustments in premove with BEC
|
||||
' DO8 controls the shutter
|
||||
' DI1 1 during exposure for pos sampling
|
||||
' Thread overview
|
||||
'******************************************************************************
|
||||
#AUTO
|
||||
DA*,*[];'DEALLOCATE ARRAYS
|
||||
ssaxs_v=1.3
|
||||
IA129,129,122,26
|
||||
'acctim determines pre motion
|
||||
acctim=2.5
|
||||
prvspeed=0
|
||||
posest=0
|
||||
'prefact increases the distance for pre acceleration
|
||||
'if in acctim limits
|
||||
prefact=2.5
|
||||
off=0
|
||||
DM aposavg[2000]
|
||||
DM cposavg[2000]
|
||||
nums=1
|
||||
JS#INIT
|
||||
JS#SETPLAT
|
||||
EN
|
||||
#CALIBC
|
||||
'ACC=1000000
|
||||
'DCC=1000000
|
||||
'SPC=1*mm
|
||||
PAC=2*mm
|
||||
BGC;AMC
|
||||
WT 1000
|
||||
PAC=5*mm
|
||||
BGC;AMC
|
||||
WT 1000
|
||||
JP#CALIBC
|
||||
EN
|
||||
'default settings
|
||||
#SETTOMO
|
||||
IF(allaxref=1)
|
||||
EN
|
||||
ENDIF
|
||||
KPC=100
|
||||
PLC=0.3
|
||||
KIC=10
|
||||
KDC=30
|
||||
ILC=9
|
||||
FAC=10
|
||||
FVC=240
|
||||
EN
|
||||
'set tuning parameters for scanning saxs plate
|
||||
#SETPLAT
|
||||
IF(allaxref=1)
|
||||
EN
|
||||
ENDIF
|
||||
KPC=100
|
||||
PLC=0.3
|
||||
KIC=10
|
||||
KDC=30
|
||||
ILC=9
|
||||
FAC=10
|
||||
FVC=240
|
||||
EN
|
||||
'called with AB before execution
|
||||
#SAMPLE
|
||||
posct=0
|
||||
sposct=0
|
||||
IF(_XQ2=-1)
|
||||
'arm position latch rising axis C
|
||||
'set latch direction rising
|
||||
'we do this prior the loop, to do the switching later asap
|
||||
CN ,,1
|
||||
ALC
|
||||
XQ#SAMPLEL,2
|
||||
ELSE
|
||||
runerr=1
|
||||
ENDIF
|
||||
EN
|
||||
#SAMPLEL
|
||||
'wait for latch
|
||||
AI3
|
||||
'WT1
|
||||
''#WAITLT1
|
||||
''JP#WAITLT1,(_ALC=1);'WAIT UNTIL CAPTURED'
|
||||
'write encoder position to a array
|
||||
aposstrt=_TPA
|
||||
'write latched position to c array
|
||||
cposstrt=_RLC
|
||||
'change latch direction falling axis C
|
||||
CN ,,-1
|
||||
ALC
|
||||
'wait for latch
|
||||
AI-3
|
||||
'WT1
|
||||
''#WAITLT2
|
||||
''JP#WAITLT2,(_ALC=1);'WAIT UNTIL CAPTURED
|
||||
'write encoder position to a array
|
||||
aposend=_TPA
|
||||
'write latched position to c array
|
||||
cposend=_RLC
|
||||
'arm position latch rising axis C for next cycle
|
||||
'set latch direction rising
|
||||
CN ,,1
|
||||
ALC
|
||||
aposavg[posct]=((aposstrt+aposend)/2)
|
||||
cposavg[posct]=((cposstrt+cposend)/2)
|
||||
posct=posct+1
|
||||
sposct=sposct+1
|
||||
IF(posct>1999)
|
||||
posct=0
|
||||
ENDIF
|
||||
JP #SAMPLEL,(posct<nums)
|
||||
EN
|
||||
#SCANG
|
||||
'a_start, a_end, speed is line axis
|
||||
'b_start, gridmax, b_step is grid axis
|
||||
gridct=0
|
||||
'nums=1000
|
||||
'wait for detector
|
||||
WT350
|
||||
IF(_XQ3=(-1))
|
||||
XQ#SCANGL,3
|
||||
ENDIF
|
||||
EN
|
||||
#SCANGL
|
||||
gridpos=b_start+(gridct*b_step)
|
||||
targE=gridpos
|
||||
JS#POSE
|
||||
WT10
|
||||
IF(@INT[(gridct/2)]<>(gridct/2))
|
||||
start=a_start
|
||||
end=a_end
|
||||
ELSE
|
||||
start=a_end
|
||||
end=a_start
|
||||
ENDIF
|
||||
'XQ#SCANL,5
|
||||
'scanstat=-1
|
||||
'#lineact
|
||||
'WT2
|
||||
'JP#lineact,(scanstat<>0)
|
||||
JS#SCANL
|
||||
gridct=gridct+1
|
||||
JP #SCANGL,(gridct<=gridmax)
|
||||
'close shutter
|
||||
CB8
|
||||
EN
|
||||
#TEMP1
|
||||
EN
|
||||
'
|
||||
#SCANL
|
||||
'based on acceleration of 500 mm/s^2
|
||||
'and a max speed of 2 mm/s
|
||||
'the max distance needed for acceleration is 4 microns
|
||||
'so we pre-move 10 microns
|
||||
'variables to set in mm, mm/s
|
||||
'start = start position
|
||||
'end = end position
|
||||
'speed = velocity
|
||||
'the scan axis is defined in the init section
|
||||
IF(allaxref=0)
|
||||
EN
|
||||
ENDIF
|
||||
'
|
||||
IF(end>start)
|
||||
dir=1
|
||||
ELSE
|
||||
dir=-1
|
||||
ENDIF
|
||||
'measure required premove
|
||||
IF((@ABS[(prvspeed-speed)])>0.001)
|
||||
premv=speed*mm*5
|
||||
IF(premv>(3*mm))
|
||||
premv=3*mm
|
||||
ENDIF
|
||||
measpre=1
|
||||
ELSE
|
||||
measpre=0
|
||||
ENDIF
|
||||
'for internal grid scans reduce overshoot
|
||||
'IF(_XQ3<>-1)
|
||||
'redpremv=0.1 ;'case for int grid scan
|
||||
'ELSE
|
||||
'redpremv=1 ;'case for line based scans
|
||||
'ENDIF
|
||||
'we are doing an internal grid
|
||||
'reduce overshoot
|
||||
prepos=(start*mm)-(dir*redpremv*(premv+(off/1000*mm)))
|
||||
'move to pre-start position if needed
|
||||
'prepos=(start*mm)-(dir*speed*acctim*mm)
|
||||
'IF(@ABS[(speed*acctim)]<0.01)
|
||||
'prepos=(start*mm)-(dir*0.01*mm*prefact)
|
||||
'ENDIF
|
||||
'IF(@ABS[(speed*acctim)]>0.1)
|
||||
'ENDIF
|
||||
IF((@ABS[(_TDC-prepos)])>(0.002*mm))
|
||||
scanstat=1
|
||||
SPC=2*mm
|
||||
PAC=prepos
|
||||
BGC
|
||||
AMC
|
||||
'open the shutter
|
||||
SB8
|
||||
WT10
|
||||
ENDIF
|
||||
IF((_LFC<>0)&(_LRC=<>0))
|
||||
scanstat=2
|
||||
SPC=@RND[speed*mm]
|
||||
'arm trigger
|
||||
trigpos=(start*mm)+(dir*off/1000*mm)
|
||||
IF(dir=1)
|
||||
OCC=trigpos,0
|
||||
ENDIF
|
||||
IF(dir=-1)
|
||||
OCC=trigpos,-65536;
|
||||
ENDIF
|
||||
'PAC=((end*mm)+(dir*premv))
|
||||
PAC=((end*mm)+(dir*redpremv*(premv+(off/1000*mm))))
|
||||
BGC
|
||||
IF(measpre=1)
|
||||
calstart=_TDC
|
||||
WT25
|
||||
#CALIBV
|
||||
prevvel=_TVC
|
||||
WT10
|
||||
JP#CALIBV,((@ABS[(prevvel-(_TVC/mm))])<15)
|
||||
calend=_TDC
|
||||
premv=((@ABS[(calend-calstart)])*3)
|
||||
'case of grid scan
|
||||
prvspeed=speed
|
||||
ENDIF
|
||||
'
|
||||
AMC
|
||||
ENDIF
|
||||
scanstat=0
|
||||
'close the shutter
|
||||
'#SHUTWT
|
||||
'JP#SHUTWT,(@IN[1]=1)
|
||||
'WT10
|
||||
'JP#SHUTWT,(@IN[1]=1)
|
||||
'
|
||||
'we are doing an internal grid
|
||||
IF(_XQ3=-1)
|
||||
CB8
|
||||
ENDIF
|
||||
EN
|
||||
'
|
||||
#POSE
|
||||
posctr=0
|
||||
posest=1
|
||||
sttime=TIME
|
||||
PTF=1;' Position Tracking aktiv
|
||||
errE=(targE-(_TPA/mm));' Fehler in mm
|
||||
IF((@ABS[errE])>0.2)
|
||||
SPF=12*stpmm
|
||||
ENDIF
|
||||
#CORRE
|
||||
posest=2
|
||||
errE=(targE-(_TPA/mm));' Fehler in mm
|
||||
PAF=_TDF+(errE*stpmm)
|
||||
IF((@ABS[errE])<0.1)
|
||||
SPF=5*stpmm
|
||||
ENDIF
|
||||
IF((@ABS[errE])<0.0001)
|
||||
posctr=posctr+1
|
||||
IF(posctr>5)
|
||||
STF
|
||||
'MG TIME-sttime, (targE-(_TPA/mm))*1000
|
||||
EN
|
||||
ENDIF
|
||||
ELSE
|
||||
posctr=0
|
||||
ENDIF
|
||||
WT5
|
||||
JP#CORRE
|
||||
posest=3
|
||||
MCF
|
||||
EN
|
||||
'
|
||||
#ZZ
|
||||
targE=120;XQ#POSE
|
||||
WT12000
|
||||
targE=20;XQ#POSE
|
||||
WT12000
|
||||
JP#ZZ
|
||||
EN
|
||||
#FINDREF
|
||||
SB1;' Bremse C-Achse loesen (nur in Verbindung mit SHC)
|
||||
SHC
|
||||
SHF
|
||||
JS#LIMSWI
|
||||
JS#REFE
|
||||
JS#REFC
|
||||
allaxref=1
|
||||
targE=0
|
||||
EN
|
||||
'
|
||||
#REFE
|
||||
SHF
|
||||
JGF=-2*stpmm
|
||||
BGF
|
||||
'MG "suche negativen Endschalter E"
|
||||
AMF
|
||||
WT100
|
||||
'step counter zero
|
||||
DPF=0
|
||||
'encoder zero
|
||||
DPA=0
|
||||
EN
|
||||
'
|
||||
#REFC
|
||||
SB1;' Bremse loesen (nur in Verbindung mit SHC)
|
||||
SHC
|
||||
JGC=-2*mm
|
||||
BGC
|
||||
'MG "suche negativen Endschalter C"
|
||||
AMC
|
||||
WT100
|
||||
DPC=0
|
||||
EN
|
||||
'
|
||||
#LIMSWI
|
||||
scanstat=0
|
||||
JS#LFF,_LFF=0;' +LIMIT E
|
||||
JS#LRF,_LRF=0;' -LIMIT E
|
||||
JS#LFC,_LFC=0;' +LIMIT C
|
||||
JS#LRC,_LRC=0;' -LIMIT C
|
||||
RE;' RETURN FROM ERROR INTERUPT
|
||||
'
|
||||
#LRF
|
||||
'MG "- LIMIT E-ACHSE "
|
||||
STF
|
||||
AMF
|
||||
JGF=stpmm
|
||||
BGF
|
||||
#LOOPA1
|
||||
JP#LOOPA1,_LRF=0
|
||||
STF
|
||||
MCF
|
||||
EN
|
||||
'
|
||||
#LFF
|
||||
'MG "+ LIMIT E-ACHSE "
|
||||
STF
|
||||
AMF
|
||||
JGF=-stpmm
|
||||
BGF
|
||||
#LOOPA2
|
||||
JP#LOOPA2,_LFF=0
|
||||
STF
|
||||
MCF
|
||||
EN
|
||||
'
|
||||
#LRC
|
||||
'MG "- LIMIT C-ACHSE "
|
||||
STC
|
||||
AMC
|
||||
JGC=mm
|
||||
BGC
|
||||
#LOOPC1
|
||||
JP#LOOPC1,_LRC=0
|
||||
STC
|
||||
AMC
|
||||
EN
|
||||
'
|
||||
#LFC
|
||||
'MG "- LIMIT C-ACHSE "
|
||||
STC
|
||||
AMC
|
||||
JGC=-mm
|
||||
BGC
|
||||
#LOOPC2
|
||||
JP#LOOPC2,_LFC=0
|
||||
STC
|
||||
AMC
|
||||
EN
|
||||
'
|
||||
#INIT
|
||||
'define fast scan axis redefined by spec
|
||||
scanstat=0
|
||||
mm=10000;' 100nm Aufloesung im encoder
|
||||
stpmm=50000;' microsteps axis E/mm
|
||||
ratio=5;'
|
||||
allaxref=0
|
||||
'acceleration rates
|
||||
ACF=3000000
|
||||
DCF=3000000
|
||||
SPF=2*mm
|
||||
OEF=0;' OF ON ERROR axis E ausgeschaltet
|
||||
MTF=2
|
||||
KSF=0.5;' Smoothing ausgeschaltet
|
||||
ACC=10*mm
|
||||
DCC=10*mm
|
||||
DVC=1;' Dual loop deaktiviert
|
||||
EN
|
||||
'
|
@ -1,714 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class GalilCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GalilError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a Galil communication error was raised. This may happen if the buffer was not empty."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
val = fcn(self, *args, **kwargs)
|
||||
except (GalilCommunicationError, GalilError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class GalilController(Controller):
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"sgalil_reference",
|
||||
"fly_grid_scan",
|
||||
"read_encoder_position",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name="GalilController",
|
||||
kind=None,
|
||||
parent=None,
|
||||
socket=None,
|
||||
attr_name="",
|
||||
labels=None,
|
||||
):
|
||||
if not hasattr(self, "_initialized") or not self._initialized:
|
||||
self._galil_axis_per_controller = 8
|
||||
self._axis = [None for axis_num in range(self._galil_axis_per_controller)]
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket=socket,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
def on(self, controller_num=0) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
if not self.connected:
|
||||
self.sock.open()
|
||||
self.connected = True
|
||||
else:
|
||||
logger.info("The connection has already been established.")
|
||||
# warnings.warn(f"The connection has already been established.", stacklevel=2)
|
||||
|
||||
def off(self) -> None:
|
||||
"""Close the socket connection to the controller"""
|
||||
if self.connected:
|
||||
self.sock.close()
|
||||
self.connected = False
|
||||
else:
|
||||
logger.info("The connection is already closed.")
|
||||
|
||||
def set_axis(self, axis: Device, axis_nr: int) -> None:
|
||||
"""Assign an axis to a device instance.
|
||||
|
||||
Args:
|
||||
axis (Device): Device instance (e.g. GalilMotor)
|
||||
axis_nr (int): Controller axis number
|
||||
|
||||
"""
|
||||
self._axis[axis_nr] = axis
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
time.sleep(0.01)
|
||||
self.sock.put(f"{val}\r".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
time.sleep(0.01)
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
@retry_once
|
||||
def socket_put_confirmed(self, val: str) -> None:
|
||||
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
|
||||
|
||||
Args:
|
||||
val (str): Message that should be sent to the socket
|
||||
|
||||
Raises:
|
||||
GalilCommunicationError: Raised if the return value is not a colon.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(val)
|
||||
if return_val != ":":
|
||||
raise GalilCommunicationError(
|
||||
f"Expected return value of ':' but instead received {return_val}"
|
||||
)
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
|
||||
# backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[axis]")) != 0)
|
||||
return bool(is_moving) # bool(is_moving or backlash_is_active)
|
||||
|
||||
def is_thread_active(self, thread_id: int) -> bool:
|
||||
val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
if val == -1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
def stop_all_axes(self) -> str:
|
||||
# return self.socket_put_and_receive(f"XQ#STOP,1")
|
||||
# Command stops all threads and motors!
|
||||
self.socket_put_and_receive(f"CB8")
|
||||
return self.socket_put_and_receive(f"ST")
|
||||
|
||||
def axis_is_referenced(self) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG allaxref").strip()))
|
||||
|
||||
def show_running_threads(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [str(ax) for ax in range(self._galil_axis_per_controller)]
|
||||
t.add_row(
|
||||
[
|
||||
"active" if self.is_thread_active(t) else "inactive"
|
||||
for t in range(self._galil_axis_per_controller)
|
||||
]
|
||||
)
|
||||
print(t)
|
||||
|
||||
def is_motor_on(self, axis_Id) -> bool:
|
||||
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
|
||||
|
||||
def get_motor_limit_switch(self, axis_Id) -> list:
|
||||
# SGalil specific
|
||||
if axis_Id == "C":
|
||||
ret = self.socket_put_and_receive(f"MG _LF{axis_Id}, _LR{axis_Id}")
|
||||
high, low = ret.strip().split(" ")
|
||||
elif axis_Id == "E":
|
||||
ret = self.socket_put_and_receive(f"MG _LF{'F'}, _LR{'F'}")
|
||||
high, low = ret.strip().split(" ")
|
||||
return [not bool(float(low)), not bool(float(high))]
|
||||
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [
|
||||
"Axis",
|
||||
"Name",
|
||||
"Connected",
|
||||
"Referenced",
|
||||
"Motor On",
|
||||
"Limits",
|
||||
"Position",
|
||||
]
|
||||
for ax in range(self._galil_axis_per_controller):
|
||||
axis = self._axis[ax]
|
||||
if axis is not None:
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
axis.name,
|
||||
axis.connected,
|
||||
self.axis_is_referenced(),
|
||||
self.is_motor_on(axis.axis_Id),
|
||||
self.get_motor_limit_switch(axis.axis_Id),
|
||||
axis.readback.read().get(axis.name).get("value"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row([None for t in t.field_names])
|
||||
print(t)
|
||||
|
||||
self.show_running_threads()
|
||||
|
||||
def galil_show_all(self) -> None:
|
||||
for controller in self._controller_instances.values():
|
||||
if isinstance(controller, GalilController):
|
||||
controller.describe()
|
||||
|
||||
def sgalil_reference(self) -> None:
|
||||
"""Reference all axes of the controller"""
|
||||
if self.axis_is_referenced():
|
||||
print("All axes are already referenced.\n")
|
||||
return
|
||||
# Make sure no axes are moving, is this necessary?
|
||||
self.stop_all_axes()
|
||||
self.socket_put_and_receive(f"XQ#FINDREF")
|
||||
print("Referencing. Please wait, timeout after 100s...\n")
|
||||
|
||||
timeout = time.time() + 100
|
||||
while not self.axis_is_referenced():
|
||||
if time.time() > timeout:
|
||||
print("Abort reference sequence, timeout reached\n")
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
# @threadlocked
|
||||
def fly_grid_scan(
|
||||
self,
|
||||
status: DeviceStatus,
|
||||
start_y: float,
|
||||
end_y: float,
|
||||
interval_y: int,
|
||||
start_x: float,
|
||||
end_x: float,
|
||||
interval_x: int,
|
||||
exp_time: float,
|
||||
readout_time: float,
|
||||
**kwargs,
|
||||
) -> tuple:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
start_y (float): start position of y axis (fast axis)
|
||||
end_y (float): end position of y axis (fast axis)
|
||||
interval_y (int): number of points in y axis
|
||||
start_x (float): start 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
|
||||
exp_time (float): exposure time in seconds
|
||||
readout_time (float): readout time in seconds, minimum of .5e-3s (0.5ms)
|
||||
|
||||
Raises:
|
||||
|
||||
LimitError: Raised if any position of motion is outside of the limits
|
||||
LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s
|
||||
|
||||
"""
|
||||
#
|
||||
if not self.axis_is_referenced():
|
||||
raise GalilError("Axis are not referenced")
|
||||
sign_y = self._axis[ord("c") - 97].sign
|
||||
sign_x = self._axis[ord("e") - 97].sign
|
||||
# Check limits
|
||||
# TODO check sign of stage, or not necessary
|
||||
check_values = [start_y, end_y, start_x, end_x]
|
||||
for val in check_values:
|
||||
self.check_value(val)
|
||||
|
||||
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:
|
||||
raise LimitError(
|
||||
f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s"
|
||||
)
|
||||
|
||||
gridmax = int(interval_x - 1)
|
||||
step_grid = (end_x - start_x) / interval_x
|
||||
n_samples = int(interval_y * interval_x)
|
||||
|
||||
# Hard coded to maximum offset of 0.1mm to avoid long motions.
|
||||
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"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}"
|
||||
)
|
||||
self.socket_put_and_receive(f"nums={n_samples}")
|
||||
self.socket_put_and_receive("XQ#SAMPLE")
|
||||
# sleep 50ms to avoid controller running into
|
||||
time.sleep(0.1)
|
||||
self.socket_put_and_receive("XQ#SCANG")
|
||||
# self._block_while_active(3)
|
||||
# time.sleep(0.1)
|
||||
threading.Thread(target=self._block_while_active, args=(3, status), daemon=True).start()
|
||||
# self._while_in_motion(3, n_samples)
|
||||
|
||||
def _block_while_active(self, thread_id: int, status) -> None:
|
||||
while self.is_thread_active(thread_id):
|
||||
time.sleep(1)
|
||||
time.sleep(1)
|
||||
while self.is_thread_active(thread_id):
|
||||
time.sleep(1)
|
||||
status.set_finished()
|
||||
|
||||
# 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:
|
||||
last_readout = 0
|
||||
val_axis2 = [] # y axis
|
||||
val_axis4 = [] # x axis
|
||||
while self.is_thread_active(thread_id):
|
||||
posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0])
|
||||
logger.info(f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}")
|
||||
time.sleep(1)
|
||||
if posct > last_readout:
|
||||
positions = self.read_encoder_position(last_readout, posct)
|
||||
val_axis4.extend(positions[0])
|
||||
val_axis2.extend(positions[1])
|
||||
last_readout = posct + 1
|
||||
logger.info(len(val_axis2))
|
||||
time.sleep(1)
|
||||
# Readout of last positions after scan finished
|
||||
posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0])
|
||||
logger.info(f"SGalil is scanning - latest enconder position {posct} from {n_samples}")
|
||||
if posct > last_readout:
|
||||
positions = self.read_encoder_position(last_readout, posct)
|
||||
val_axis4.extend(positions[0])
|
||||
val_axis2.extend(positions[1])
|
||||
|
||||
return val_axis4, val_axis2
|
||||
|
||||
def read_encoder_position(self, fromval: int, toval: int) -> tuple:
|
||||
val_axis2 = [] # y axis
|
||||
val_axis4 = [] # x axis
|
||||
for ii in range(fromval, toval + 1):
|
||||
rts = self.socket_put_and_receive(f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10")
|
||||
if rts == ":":
|
||||
val_axis4.append(rts)
|
||||
val_axis2.append(rts)
|
||||
continue
|
||||
|
||||
val_axis4.append(float(rts.strip().split(" ")[0]) / 100000)
|
||||
val_axis2.append(float(rts.strip().split(" ")[1]) / 100000)
|
||||
return val_axis4, val_axis2
|
||||
|
||||
|
||||
class GalilSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class GalilSignalRO(GalilSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class GalilReadbackSignal(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
current_pos = float(
|
||||
self.controller.socket_put_and_receive(f"MG _TP{self.parent.axis_Id}/mm")
|
||||
)
|
||||
elif self.parent.axis_Id_numeric == 4:
|
||||
# hardware controller readback from axis 4 is on axis 0, A instead of E
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm"))
|
||||
current_pos *= self.parent.sign
|
||||
return current_pos
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
return val
|
||||
|
||||
|
||||
class GalilSetpointSignal(GalilSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint * self.parent.sign
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.axis_is_referenced()
|
||||
if not axes_referenced:
|
||||
raise GalilError(
|
||||
"Not all axes are referenced. Please use controller.sgalil_reference(). BE AWARE that axes start moving, potentially beyond limits, make sure full range of motion is safe"
|
||||
)
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
self.controller.socket_put_confirmed(f"PA{self.parent.axis_Id}={target_val:.4f}*mm")
|
||||
self.controller.socket_put_and_receive(f"BG{self.parent.axis_Id}")
|
||||
elif self.parent.axis_Id_numeric == 4:
|
||||
self.controller.socket_put_confirmed(f"targ{self.parent.axis_Id}={target_val:.4f}")
|
||||
self.controller.socket_put_and_receive(f"XQ#POSE,{self.parent.axis_Id_numeric}")
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
class GalilMotorIsMoving(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
ret = self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric)
|
||||
return ret
|
||||
if self.parent.axis_Id_numeric == 4:
|
||||
# Motion signal from axis 4 is mapped to axis 5
|
||||
ret = self.controller.is_axis_moving("F", 5)
|
||||
return ret or self.controller.is_thread_active(4)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class GalilAxesReferenced(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.socket_put_and_receive("MG allaxref")
|
||||
|
||||
|
||||
class SGalilMotor(Device, PositionerBase):
|
||||
""" "SGalil Motors at cSAXS have a
|
||||
DC motor (y axis - vertical) - implemented as C
|
||||
and a step motor (x-axis horizontal) - implemented as E
|
||||
that require different communication for control
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
||||
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="129.129.122.26",
|
||||
port=23,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = GalilController(socket=socket_cls(host=host, port=port))
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
self.device_mapping = kwargs.pop("device_mapping", {})
|
||||
self.device_manager = device_manager
|
||||
|
||||
if len(self.device_mapping) > 0 and self.device_manager is None:
|
||||
raise BECConfigError(
|
||||
"device_mapping has been specified but the device_manager cannot be accessed."
|
||||
)
|
||||
self.rt = self.device_mapping.get("rt")
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
logger.info("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(1.5)
|
||||
val = self.readback.read()
|
||||
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||
|
||||
if not success:
|
||||
print(" stop")
|
||||
self._done_moving(success=success)
|
||||
logger.info("Move finished")
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
if val not in ["C", "E"]:
|
||||
raise ValueError(
|
||||
f"axis_id {val} is currently not supported, please use either 'C' or 'E'."
|
||||
)
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val not in [2, 4]:
|
||||
raise ValueError(f"Numeric value {val} is not supported, it must be either 2 or 4.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}.")
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
# last_speed = self.controller.socket_put_and_receive("MG")
|
||||
rtr = self.controller.socket_put_and_receive(f"SPC={2*10000}")
|
||||
logger.info(f"{rtr}")
|
||||
# logger.info(f'Motor stopped, restored speed for samy from {last_speed}mm/s to 2mm/s')
|
||||
return super().stop(success=success)
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
status = DeviceStatus(self)
|
||||
self.controller.fly_grid_scan(
|
||||
status,
|
||||
self._kickoff_params.get("start_y"),
|
||||
self._kickoff_params.get("end_y"),
|
||||
self._kickoff_params.get("interval_y"),
|
||||
self._kickoff_params.get("start_x"),
|
||||
self._kickoff_params.get("end_x"),
|
||||
self._kickoff_params.get("interval_x"),
|
||||
self._kickoff_params.get("exp_time"),
|
||||
self._kickoff_params.get("readout_time"),
|
||||
)
|
||||
return status
|
||||
|
||||
def configure(self, parameter: dict, **kwargs) -> None:
|
||||
self._kickoff_params = parameter
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mock = False
|
||||
if not mock:
|
||||
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1)
|
||||
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1)
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock)
|
||||
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock)
|
||||
|
||||
samx.controller.galil_show_all()
|
@ -1,79 +0,0 @@
|
||||
# Documentation SGalil ophyd wrapper
|
||||
Ophyd wrapper for the SGalil controller and stages.
|
||||
## TODO tests and evaluate whether its good to combine common functionaltiy with galil lamni/omny/flomni controller
|
||||
## Integration of the device in IPython kernel
|
||||
BEC needs to be able to reach the host TCP to initiate a connection to the device.
|
||||
```Python
|
||||
from ophyd_devices.galil.sgalil_ophyd import SGalilMotor
|
||||
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1)
|
||||
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1)
|
||||
# connect to the controller
|
||||
samx.controller.on()
|
||||
samx.read()
|
||||
samx.move(5)
|
||||
dir(samx)# for full printout of commands
|
||||
# useful for development, check below socket communication with sgalil controller
|
||||
samx.controller.socket_put_and_receive('#string: message_to_controller')
|
||||
```
|
||||
## TODO Integration of device in BEC device config!
|
||||
to be tested too
|
||||
|
||||
## Fly scans
|
||||
2D grid fly scan as implemented on the controller.
|
||||
TTL triggers are sent for the start of each line.
|
||||
The scan on the controller needs to be matched with an appropriate triggering scheme, as for instance shown in the attached scheme together with the Stanford Research DG645 device at cSAXS.
|
||||

|
||||
```Python
|
||||
samx.controller.(start_y, end_y, interval_y, start_x, end_x, interval_x, exp_time, readtime)
|
||||
# for example
|
||||
samx.controller.fly_grid_scan(start_y= 16, end_y= 24, interval_y= 100, start_x= 18, end_x= 17.6, interval_x= 2, exp_time= 0.08, readtime= 0.005)
|
||||
```
|
||||
|
||||
## TODO implement line scan
|
||||
Check SPEC implementation for line scans with sgalil controller, and complement it with a suitable triggering scheme of the DG645.
|
||||
|
||||
## TODO readout of positions in encoder
|
||||
Should this be integrated in the flyscan or not.
|
||||
To be explored where this is most suitable.
|
||||
|
||||
## Socket communication with sgalil controller
|
||||
### vertical axis (samy)
|
||||
- initiate with axis 2, C
|
||||
- in motion: "MG _BG{axis_char}", e.g. "MG _BGC" , 0 or 1
|
||||
- limit switch not pressed: "MG _LR{axis_char}, _LF{axis_char}" , 0 or 1
|
||||
- position: "MG _TP{axis_char}/mm" , position in mm
|
||||
- Axis referenced: "MG allaxref", 0 or 1
|
||||
- stop all axis: "XQ#STOP,1"
|
||||
- is motor on: "MG _MO{axis_char}", 0 or 1
|
||||
- is thread active: "MG _XQ{thread_id}", 0 or 1
|
||||
**Specific for sgalil_y**
|
||||
- set_motion_speed: "SP{axis_char}=2*mm", 2mm/s is max speed
|
||||
- set_final_pos: "PA{axis_char}={val:04f}*mm", target pos in mm
|
||||
- start motion: "BG{axis_char}", start motion
|
||||
### horizontal axis (samx)
|
||||
note: some hardware modifications were done that require access to different channels in the encoder. Encoder, motor and limit switches are not controlled by the same endpoint/axis of the controller... see below
|
||||
- initiate with axis 4, E
|
||||
**Specific for sgalil_x**
|
||||
- set_final_pos: "targ{axis_char}={val:04f}", e.g. "targE=2.0000"
|
||||
- start motion: "XQ#POSE,{axis_char}"
|
||||
- For *in motion* and *limit switch not pressed* commands,
|
||||
the key changes to AXIS 5 || F, e.g. "MG _BGF"
|
||||
- For *position* switch to Axis 0 || A, e.g. "MG _TPA/mm"
|
||||
|
||||
### flyscan 2D grid commanes:
|
||||
Last command ('XQ#SCANG') has to come with sufficient delay, important for setting up dedicated scans
|
||||
f***ast axis***
|
||||
- self.socket_put_and_receive(f'a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}')
|
||||
***slow axis***
|
||||
- self.socket_put_and_receive(f'b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}')
|
||||
- self.socket_put_and_receive(f'nums={n_samples}') # Declare number of triggers for encoder
|
||||
- self.socket_put_and_receive('XQ#SAMPLE') # Reset encoder counting --> sampling starts with 0
|
||||
Start scan (be aware, needs some waiting from before)
|
||||
- self.socket_put_and_receive('XQ#SCANG')
|
||||
|
||||
### Encoder readings!
|
||||
The encoder readout is triggered by an TTL pulse.
|
||||
Unfortunately, TTL triggers to the encoder can only be accepted with at least 12.5ms time between rising/falling edges. Therefore, maximum readout has to be ~25Hz, rather 30Hz (experimentally determined).
|
||||
Socket commands for the readout:
|
||||
- self.socket_put_and_receive('MGsposct') # get current position counter
|
||||
- self.socket_put_and_receive('MGaposavg[{ii%2000}]*10, cposavg[{ii%2000}]*10,') # loop over ii
|
@ -1 +0,0 @@
|
||||
from .npoint import NPointAxis, NPointController
|
@ -1,546 +0,0 @@
|
||||
import functools
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from prettytable import PrettyTable
|
||||
from typeguard import typechecked
|
||||
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import raise_if_disconnected
|
||||
|
||||
|
||||
def channel_checked(fcn):
|
||||
"""Decorator to catch attempted access to channels that are not available."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
self._check_channel(args[0])
|
||||
return fcn(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SocketIO:
|
||||
"""SocketIO helper class for TCP IP connections"""
|
||||
|
||||
def __init__(self, sock=None):
|
||||
self.is_open = False
|
||||
if sock is None:
|
||||
self.open()
|
||||
else:
|
||||
self.sock = sock
|
||||
|
||||
def connect(self, host, port):
|
||||
print(f"connecting to {host} port {port}")
|
||||
# self.sock.create_connection((host, port))
|
||||
self.sock.connect((host, port))
|
||||
|
||||
def _put(self, msg_bytes):
|
||||
return self.sock.send(msg_bytes)
|
||||
|
||||
def _recv(self, buffer_length=1024):
|
||||
return self.sock.recv(buffer_length)
|
||||
|
||||
def _initialize_socket(self):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(5)
|
||||
|
||||
def put(self, msg):
|
||||
return self._put(msg)
|
||||
|
||||
def receive(self, buffer_length=1024):
|
||||
return self._recv(buffer_length=buffer_length)
|
||||
|
||||
def open(self):
|
||||
self._initialize_socket()
|
||||
self.is_open = True
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
self.is_open = False
|
||||
|
||||
|
||||
class NPointController:
|
||||
_controller_instance = None
|
||||
|
||||
NUM_CHANNELS = 3
|
||||
_read_single_loc_bit = "A0"
|
||||
_write_single_loc_bit = "A2"
|
||||
_trailing_bit = "55"
|
||||
_range_offset = "78"
|
||||
_channel_base = ["11", "83"]
|
||||
|
||||
def __init__(
|
||||
self, comm_socket: SocketIO, server_ip: str = "129.129.99.87", server_port: int = 23
|
||||
) -> None:
|
||||
self._lock = threading.RLock()
|
||||
super().__init__()
|
||||
self._server_and_port_name = (server_ip, server_port)
|
||||
self.socket = comm_socket
|
||||
self.connected = False
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not NPointController._controller_instance:
|
||||
NPointController._controller_instance = object.__new__(cls)
|
||||
return NPointController._controller_instance
|
||||
|
||||
@classmethod
|
||||
def create(cls):
|
||||
return cls(SocketIO())
|
||||
|
||||
def show_all(self) -> None:
|
||||
"""Display current status of all channels
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not self.connected:
|
||||
print("npoint controller is currently disabled.")
|
||||
return
|
||||
print(f"Connected to controller at {self._server_and_port_name}")
|
||||
t = PrettyTable()
|
||||
t.field_names = ["Channel", "Range", "Position", "Target"]
|
||||
for ii in range(self.NUM_CHANNELS):
|
||||
t.add_row(
|
||||
[ii, self._get_range(ii), self._get_current_pos(ii), self._get_target_pos(ii)]
|
||||
)
|
||||
print(t)
|
||||
|
||||
@threadlocked
|
||||
def on(self) -> None:
|
||||
"""Enable the NPoint controller and open a new socket.
|
||||
|
||||
Raises:
|
||||
TimeoutError: Raised if the socket connection raises a timeout.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if self.connected:
|
||||
print("You are already connected to the NPoint controller.")
|
||||
return
|
||||
if not self.socket.is_open:
|
||||
self.socket.open()
|
||||
try:
|
||||
self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1])
|
||||
except socket.timeout:
|
||||
raise TimeoutError(
|
||||
f"Failed to connect to the specified server and port {self._server_and_port_name}."
|
||||
)
|
||||
except OSError:
|
||||
print("ERROR while connecting. Let's try again")
|
||||
self.socket.close()
|
||||
time.sleep(0.5)
|
||||
self.socket.open()
|
||||
self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1])
|
||||
self.connected = True
|
||||
|
||||
@threadlocked
|
||||
def off(self) -> None:
|
||||
"""Disable the controller and close the socket.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.socket.close()
|
||||
self.connected = False
|
||||
|
||||
@channel_checked
|
||||
def _get_range(self, channel: int) -> int:
|
||||
"""Get the range of the specified channel axis.
|
||||
|
||||
Args:
|
||||
channel (int): Channel for which the range should be requested.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if the received message doesn't have the expected number of bytes (10).
|
||||
|
||||
Returns:
|
||||
int: Range
|
||||
"""
|
||||
|
||||
# for first channel: 0x11 83 10 78
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{16 + 16 * channel:x}", self._range_offset])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
if len(recvd) != 10:
|
||||
raise RuntimeError(
|
||||
f"Received buffer is corrupted. Expected 10 bytes and instead got {len(recvd)}"
|
||||
)
|
||||
device_range = self._hex_list_to_int(recvd[5:-1], signed=False)
|
||||
return device_range
|
||||
|
||||
@channel_checked
|
||||
def _get_current_pos(self, channel: int) -> float:
|
||||
# for first channel: 0x11 83 13 34
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{19 + 16 * channel:x}", "34"])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
|
||||
pos_buffer = recvd[5:-1]
|
||||
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
|
||||
return pos
|
||||
|
||||
@channel_checked
|
||||
def _set_target_pos(self, channel: int, pos: float) -> None:
|
||||
# for first channel: 0x11 83 12 18 00 00 00 00
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{18 + channel * 16:x}", "18"])
|
||||
|
||||
target = int(round(1048574 / 100 * pos))
|
||||
data = [f"{m:02x}" for m in target.to_bytes(4, byteorder="big", signed=True)]
|
||||
|
||||
send_buffer = self.__write_single_location_buffer(addr, data)
|
||||
self._put(send_buffer)
|
||||
|
||||
@channel_checked
|
||||
def _get_target_pos(self, channel: int) -> float:
|
||||
# for first channel: 0x11 83 12 18
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{18 + channel * 16:x}", "18"])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
pos_buffer = recvd[5:-1]
|
||||
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
|
||||
return pos
|
||||
|
||||
@channel_checked
|
||||
def _set_servo(self, channel: int, enable: bool) -> None:
|
||||
print("Not tested")
|
||||
return
|
||||
# for first channel: 0x11 83 10 84 00 00 00 00
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{16 + channel * 16:x}", "84"])
|
||||
|
||||
if enable:
|
||||
data = ["00"] * 3 + ["01"]
|
||||
else:
|
||||
data = ["00"] * 4
|
||||
send_buffer = self.__write_single_location_buffer(addr, data)
|
||||
|
||||
self._put(send_buffer)
|
||||
|
||||
@channel_checked
|
||||
def _get_servo(self, channel: int) -> int:
|
||||
# for first channel: 0x11 83 10 84 00 00 00 00
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{16 + channel * 16:x}", "84"])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
buffer = recvd[5:-1]
|
||||
status = self._hex_list_to_int(buffer)
|
||||
return status
|
||||
|
||||
@threadlocked
|
||||
def _put(self, buffer: list) -> None:
|
||||
"""Translates a list of hex values to bytes and sends them to the socket.
|
||||
|
||||
Args:
|
||||
buffer (list): List of hex values without leading 0x
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
buffer = b"".join([bytes.fromhex(m) for m in buffer])
|
||||
self.socket.put(buffer)
|
||||
|
||||
@threadlocked
|
||||
def _put_and_receive(self, msg_hex_list: list) -> list:
|
||||
"""Send msg to socket and wait for a reply.
|
||||
|
||||
Args:
|
||||
msg_hex_list (list): List of hex values without leading 0x.
|
||||
|
||||
Returns:
|
||||
list: Received message as a list of hex values
|
||||
"""
|
||||
|
||||
buffer = b"".join([bytes.fromhex(m) for m in msg_hex_list])
|
||||
self.socket.put(buffer)
|
||||
recv_msg = self.socket.receive()
|
||||
recv_hex_list = [hex(m) for m in recv_msg]
|
||||
self._verify_received_msg(msg_hex_list, recv_hex_list)
|
||||
return recv_hex_list
|
||||
|
||||
def _verify_received_msg(self, in_list: list, out_list: list) -> None:
|
||||
"""Ensure that the first address bits of sent and received messages are the same.
|
||||
|
||||
Args:
|
||||
in_list (list): list containing the sent message
|
||||
out_list (list): list containing the received message
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if first two address bits of 'in' and 'out' are not identical
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
# first, translate hex (str) values to int
|
||||
in_list_int = [int(val, 16) for val in in_list]
|
||||
out_list_int = [int(val, 16) for val in out_list]
|
||||
|
||||
# first ints of the reply should be the same. Otherwise something went wrong
|
||||
if not in_list_int[:2] == out_list_int[:2]:
|
||||
raise RuntimeError("Connection failure. Please restart the controller.")
|
||||
|
||||
def _check_channel(self, channel: int) -> None:
|
||||
if channel >= self.NUM_CHANNELS:
|
||||
raise ValueError(
|
||||
f"Channel {channel+1} exceeds the available number of channels ({self.NUM_CHANNELS})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _hex_list_to_int(in_buffer: list, byteorder="little", signed=True) -> int:
|
||||
"""Translate hex list to int.
|
||||
|
||||
Args:
|
||||
in_buffer (list): Input buffer; received as list of hex values
|
||||
byteorder (str, optional): Byteorder of in_buffer. Defaults to "little".
|
||||
signed (bool, optional): Whether the hex list represents a signed int. Defaults to True.
|
||||
|
||||
Returns:
|
||||
int: Translated integer.
|
||||
"""
|
||||
if byteorder == "little":
|
||||
in_buffer.reverse()
|
||||
|
||||
# make sure that all hex strings have the same format ("FF")
|
||||
val_hex = [f"{int(m, 16):02x}" for m in in_buffer]
|
||||
|
||||
val_bytes = [bytes.fromhex(m) for m in val_hex]
|
||||
val = int.from_bytes(b"".join(val_bytes), byteorder="big", signed=signed)
|
||||
return val
|
||||
|
||||
@staticmethod
|
||||
def __read_single_location_buffer(addr) -> list:
|
||||
"""Prepare buffer for reading from a single memory location (hex address).
|
||||
Number of bytes: 6
|
||||
Format: 0xA0 [addr] 0x55
|
||||
Return Value: 0xA0 [addr] [data] 0x55
|
||||
Sample Hex Transmission from PC to LC.400: A0 18 12 83 11 55
|
||||
Sample Hex Return Transmission from LC.400 to PC: A0 18 12 83 11 64 00 00 00 55
|
||||
|
||||
Args:
|
||||
addr (list): Hex address to read from
|
||||
|
||||
Returns:
|
||||
list: List of hex values representing the read instruction.
|
||||
"""
|
||||
buffer = []
|
||||
buffer.append(NPointController._read_single_loc_bit)
|
||||
if isinstance(addr, list):
|
||||
addr.reverse()
|
||||
buffer.extend(addr)
|
||||
else:
|
||||
buffer.append(addr)
|
||||
buffer.append(NPointController._trailing_bit)
|
||||
|
||||
return buffer
|
||||
|
||||
@staticmethod
|
||||
def __write_single_location_buffer(addr: list, data: list) -> list:
|
||||
"""Prepare buffer for writing to a single memory location (hex address).
|
||||
Number of bytes: 10
|
||||
Format: 0xA2 [addr] [data] 0x55
|
||||
Return Value: none
|
||||
Sample Hex Transmission from PC to C.400: A2 18 12 83 11 E8 03 00 00 55
|
||||
|
||||
Args:
|
||||
addr (list): List of hex values representing the address to write to.
|
||||
data (list): List of hex values representing the data that should be written.
|
||||
|
||||
Returns:
|
||||
list: List of hex values representing the write instruction.
|
||||
"""
|
||||
buffer = []
|
||||
buffer.append(NPointController._write_single_loc_bit)
|
||||
if isinstance(addr, list):
|
||||
addr.reverse()
|
||||
buffer.extend(addr)
|
||||
else:
|
||||
buffer.append(addr)
|
||||
|
||||
if isinstance(data, list):
|
||||
data.reverse()
|
||||
buffer.extend(data)
|
||||
else:
|
||||
buffer.append(data)
|
||||
buffer.append(NPointController._trailing_bit)
|
||||
return buffer
|
||||
|
||||
@staticmethod
|
||||
def __read_array():
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def __write_next_command():
|
||||
raise NotImplementedError
|
||||
|
||||
def __del__(self):
|
||||
if self.connected:
|
||||
print("Closing npoint socket")
|
||||
self.off()
|
||||
|
||||
|
||||
class NPointAxis:
|
||||
def __init__(self, controller: NPointController, channel: int, name: str) -> None:
|
||||
super().__init__()
|
||||
self._axis_range = 100
|
||||
self.controller = controller
|
||||
self.channel = channel
|
||||
self.name = name
|
||||
self.controller._check_channel(channel)
|
||||
self._settling_time = 0.1
|
||||
|
||||
if self.settling_time == 0:
|
||||
self.settling_time = 0.1
|
||||
print(f"Setting the npoint settling time to {self.settling_time:.2f} s.")
|
||||
print(
|
||||
"You can set the settling time depending on the stage tuning\nusing the settling_time property."
|
||||
)
|
||||
print("This is the waiting time before the counting is done.")
|
||||
|
||||
def show_all(self) -> None:
|
||||
self.controller.show_all()
|
||||
|
||||
@raise_if_disconnected
|
||||
def get(self) -> float:
|
||||
"""Get current position for this channel.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
float: position
|
||||
"""
|
||||
return self.controller._get_current_pos(self.channel)
|
||||
|
||||
@raise_if_disconnected
|
||||
def get_target_pos(self) -> float:
|
||||
"""Get target position for this channel.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
float: position
|
||||
"""
|
||||
return self.controller._get_target_pos(self.channel)
|
||||
|
||||
@raise_if_disconnected
|
||||
@typechecked
|
||||
def set(self, pos: float) -> None:
|
||||
"""Set a new target position and wait until settled (settling_time).
|
||||
|
||||
Args:
|
||||
pos (float): New target position
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.controller._set_target_pos(self.channel, pos)
|
||||
time.sleep(self.settling_time)
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return self.controller.connected
|
||||
|
||||
@property
|
||||
@raise_if_disconnected
|
||||
def servo(self) -> int:
|
||||
"""Get servo status
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
int: Servo status
|
||||
"""
|
||||
return self.controller._get_servo(self.channel)
|
||||
|
||||
@servo.setter
|
||||
@raise_if_disconnected
|
||||
@typechecked
|
||||
def servo(self, val: bool) -> None:
|
||||
"""Set servo status
|
||||
|
||||
Args:
|
||||
val (bool): Servo status
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.controller._set_servo(self.channel, val)
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self._settling_time
|
||||
|
||||
@settling_time.setter
|
||||
@typechecked
|
||||
def settling_time(self, val: float) -> None:
|
||||
self._settling_time = val
|
||||
print(f"Setting the npoint settling time to {val:.2f} s.")
|
||||
|
||||
|
||||
class NPointEpics(NPointAxis):
|
||||
def __init__(self, controller: NPointController, channel: int, name: str) -> None:
|
||||
super().__init__(controller, channel, name)
|
||||
self.low_limit = -50
|
||||
self.high_limit = 50
|
||||
self._prefix = name
|
||||
|
||||
def get_pv(self) -> str:
|
||||
return self.name
|
||||
|
||||
def get_position(self, readback=True) -> float:
|
||||
if readback:
|
||||
return self.get()
|
||||
else:
|
||||
return self.get_target_pos()
|
||||
|
||||
def within_limits(self, pos: float) -> bool:
|
||||
return pos > self.low_limit and pos < self.high_limit
|
||||
|
||||
def move(self, position: float, wait=True) -> None:
|
||||
self.set(position)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
## EXAMPLES ##
|
||||
#
|
||||
# Create controller and socket instance explicitly:
|
||||
# controller = NPointController(SocketIO())
|
||||
# npointx = NPointAxis(controller, 0, "nx")
|
||||
# npointy = NPointAxis(controller, 1, "ny")
|
||||
|
||||
# Create controller instance explicitly
|
||||
# controller = NPointController.create()
|
||||
# npointx = NPointAxis(controller, 0, "nx")
|
||||
# npointy = NPointAxis(controller, 1, "ny")
|
||||
|
||||
# Single-line axis:
|
||||
# npointx = NPointAxis(NPointController.create(), 0, "nx")
|
||||
#
|
||||
# EPICS wrapper:
|
||||
# nx = NPointEpics(NPointController.create(), 0, "nx")
|
||||
|
||||
controller = NPointController.create()
|
||||
npointx = NPointAxis(NPointController.create(), 0, "nx")
|
||||
npointy = NPointAxis(NPointController.create(), 0, "ny")
|
@ -1,2 +0,0 @@
|
||||
from .rt_flomni_ophyd import RtFlomniController, RtFlomniMotor
|
||||
from .rt_lamni_ophyd import RtLamniController, RtLamniMotor
|
@ -1,807 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from ophyd_devices.rt_lamni.rt_ophyd import (
|
||||
BECConfigError,
|
||||
RtCommunicationError,
|
||||
RtController,
|
||||
RtError,
|
||||
RtReadbackSignal,
|
||||
RtSetpointSignal,
|
||||
RtSignalRO,
|
||||
retry_once,
|
||||
)
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RtFlomniController(RtController):
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"set_rotation_angle",
|
||||
"feedback_disable",
|
||||
"feedback_enable_without_reset",
|
||||
"feedback_enable_with_reset",
|
||||
"feedback_is_running",
|
||||
"add_pos_to_scan",
|
||||
"get_pid_x",
|
||||
"move_samx_to_scan_region",
|
||||
"clear_trajectory_generator",
|
||||
"show_cyclic_error_compensation",
|
||||
"laser_tracker_on",
|
||||
"laser_tracker_off",
|
||||
"laser_tracker_show_all",
|
||||
"show_signal_strength_interferometer",
|
||||
"read_ssi_interferometer",
|
||||
"laser_tracker_check_signalstrength",
|
||||
"laser_tracker_check_enabled",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name=None,
|
||||
socket_cls=None,
|
||||
socket_host=None,
|
||||
socket_port=None,
|
||||
attr_name="",
|
||||
parent=None,
|
||||
labels=None,
|
||||
kind=None,
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket_cls=socket_cls,
|
||||
socket_host=socket_host,
|
||||
socket_port=socket_port,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
self.tracker_info = {}
|
||||
self._min_scan_buffer_reached = False
|
||||
self.rt_pid_voltage = None
|
||||
|
||||
def add_pos_to_scan(self, positions) -> None:
|
||||
def send_positions(parent, positions):
|
||||
parent._min_scan_buffer_reached = False
|
||||
start_time = time.time()
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}")
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
logger.info(
|
||||
f"Sending {len(positions)} positions took {time.time()-start_time} seconds."
|
||||
)
|
||||
|
||||
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
|
||||
|
||||
def move_to_zero(self):
|
||||
self.socket_put("pa0,0")
|
||||
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
|
||||
self.socket_put("pa1,0")
|
||||
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
|
||||
self.socket_put("pa2,0")
|
||||
self.get_axis_by_name("rtz").user_setpoint.setpoint = 0
|
||||
time.sleep(0.05)
|
||||
|
||||
def feedback_is_running(self) -> bool:
|
||||
status = int(float(self.socket_put_and_receive("l2").strip()))
|
||||
if status == 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
self.socket_put("l0") # disable feedback
|
||||
|
||||
self.move_to_zero()
|
||||
|
||||
if not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1:
|
||||
print("Please wait, slew rate limiters not on target.")
|
||||
logger.info("Please wait, slew rate limiters not on target.")
|
||||
while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1:
|
||||
time.sleep(0.05)
|
||||
|
||||
self.get_device_manager().devices.rty.update_user_parameter({"tomo_additional_offsety": 0})
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.laser_tracker_on()
|
||||
|
||||
# move to 0. FUPR will set the rotation angle during readout
|
||||
self.get_device_manager().devices.fsamroy.obj.move(0, wait=True)
|
||||
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
|
||||
fsamx.obj.pid_x_correction = 0
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
fsamx_in = fsamx.user_parameter.get("in")
|
||||
if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3):
|
||||
print(
|
||||
"Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically."
|
||||
)
|
||||
raise RtError(
|
||||
"Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically."
|
||||
)
|
||||
|
||||
if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01):
|
||||
fsamx.read_only = False
|
||||
fsamx.obj.move(fsamx_in, wait=True)
|
||||
fsamx.read_only = True
|
||||
time.sleep(1)
|
||||
|
||||
self.socket_put("l1")
|
||||
time.sleep(0.4)
|
||||
|
||||
if not self.feedback_is_running():
|
||||
print("Feedback is not running; likely an error in the interferometer.")
|
||||
raise RtError("Feedback is not running; likely an error in the interferometer.")
|
||||
|
||||
time.sleep(1.5)
|
||||
self.show_cyclic_error_compensation()
|
||||
|
||||
self.rt_pid_voltage = self.get_pid_x()
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
rtx.update_user_parameter({"rt_pid_voltage": self.rt_pid_voltage})
|
||||
|
||||
self.set_device_enabled("fsamx", False)
|
||||
self.set_device_enabled("fsamy", False)
|
||||
self.set_device_enabled("foptx", False)
|
||||
self.set_device_enabled("fopty", False)
|
||||
|
||||
def move_samx_to_scan_region(self, fovx: float, cenx: float):
|
||||
time.sleep(0.05)
|
||||
if self.rt_pid_voltage is None:
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
|
||||
if self.rt_pid_voltage is None:
|
||||
raise RtError(
|
||||
"rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first."
|
||||
)
|
||||
logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}")
|
||||
expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100
|
||||
logger.info(f"Expected PID voltage: {expected_voltage}")
|
||||
logger.info(f"Current PID voltage: {self.get_pid_x()}")
|
||||
|
||||
wait_on_exit = False
|
||||
while True:
|
||||
if np.abs(self.get_pid_x() - expected_voltage) < 1:
|
||||
break
|
||||
wait_on_exit = True
|
||||
self.socket_put("v0")
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx.read_only = False
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007
|
||||
logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}")
|
||||
fsamx_in = fsamx.user_parameter.get("in")
|
||||
fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True)
|
||||
fsamx.read_only = True
|
||||
time.sleep(0.1)
|
||||
self.laser_tracker_on()
|
||||
time.sleep(0.01)
|
||||
|
||||
if wait_on_exit:
|
||||
time.sleep(1)
|
||||
|
||||
self.socket_put("v1")
|
||||
|
||||
@threadlocked
|
||||
def clear_trajectory_generator(self):
|
||||
self.socket_put("sc")
|
||||
logger.info("flomni scan stopped and deleted, moving to start position")
|
||||
|
||||
def feedback_enable_without_reset(self):
|
||||
self.laser_tracker_on()
|
||||
self.socket_put("l3")
|
||||
time.sleep(0.01)
|
||||
|
||||
if not self.feedback_is_running():
|
||||
print("Feedback is not running; likely an error in the interferometer.")
|
||||
raise RtError("Feedback is not running; likely an error in the interferometer.")
|
||||
|
||||
self.set_device_enabled("fsamx", False)
|
||||
self.set_device_enabled("fsamy", False)
|
||||
self.set_device_enabled("foptx", False)
|
||||
self.set_device_enabled("fopty", False)
|
||||
|
||||
def feedback_disable(self):
|
||||
self.clear_trajectory_generator()
|
||||
self.move_to_zero()
|
||||
self.socket_put("l0")
|
||||
|
||||
self.set_device_enabled("fsamx", True)
|
||||
self.set_device_enabled("fsamy", True)
|
||||
self.set_device_enabled("foptx", True)
|
||||
self.set_device_enabled("fopty", True)
|
||||
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]")
|
||||
print("rt feedback is now disalbed.")
|
||||
|
||||
def get_pid_x(self) -> float:
|
||||
voltage = float(self.socket_put_and_receive("g").strip())
|
||||
return voltage
|
||||
|
||||
def show_cyclic_error_compensation(self):
|
||||
cec0 = int(float(self.socket_put_and_receive("w0").strip()))
|
||||
cec1 = int(float(self.socket_put_and_receive("w1").strip()))
|
||||
|
||||
if cec0 == 32:
|
||||
logger.info("Cyclic Error Compensation: y-axis is initialized")
|
||||
else:
|
||||
logger.info("Cyclic Error Compensation: y-axis is NOT initialized")
|
||||
print("Cyclic Error Compensation: y-axis is NOT initialized")
|
||||
if cec1 == 32:
|
||||
logger.info("Cyclic Error Compensation: x-axis is initialized")
|
||||
else:
|
||||
logger.info("Cyclic Error Compensation: x-axis is NOT initialized")
|
||||
print("Cyclic Error Compensation: x-axis is NOT initialized")
|
||||
|
||||
def set_rotation_angle(self, val: float) -> None:
|
||||
self.socket_put(f"a{val/180*np.pi}")
|
||||
|
||||
def laser_tracker_check_enabled(self) -> bool:
|
||||
self.laser_update_tracker_info()
|
||||
if self.tracker_info["enabled_z"] and self.tracker_info["enabled_y"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def laser_tracker_on(self):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
logger.info("Enabling the laser tracker. Please wait...")
|
||||
print("Enabling the laser tracker. Please wait...")
|
||||
|
||||
tracker_intensity = self.tracker_info["tracker_intensity"]
|
||||
if (
|
||||
tracker_intensity < self.tracker_info["threshold_intensity_y"]
|
||||
or tracker_intensity < self.tracker_info["threshold_intensity_z"]
|
||||
):
|
||||
logger.info(self.tracker_info)
|
||||
print("The tracker cannot be enabled because the beam intensity it low.")
|
||||
raise RtError("The tracker cannot be enabled because the beam intensity it low.")
|
||||
|
||||
self.move_to_zero()
|
||||
self.socket_put("T1")
|
||||
time.sleep(0.5)
|
||||
|
||||
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
|
||||
"trackyct=0"
|
||||
)
|
||||
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
|
||||
"trackzct=0"
|
||||
)
|
||||
|
||||
self.laser_tracker_wait_on_target()
|
||||
logger.info("Laser tracker running!")
|
||||
print("Laser tracker running!")
|
||||
|
||||
def laser_tracker_off(self):
|
||||
self.socket_put("T0")
|
||||
logger.info("Disabled the laser tracker")
|
||||
print("Disabled the laser tracker")
|
||||
|
||||
def laser_tracker_show_all(self):
|
||||
self.laser_update_tracker_info()
|
||||
t = PrettyTable()
|
||||
t.title = f"Laser Tracker Info"
|
||||
t.field_names = ["Name", "Value"]
|
||||
for key, val in self.tracker_info.items():
|
||||
t.add_row([key, val])
|
||||
print(t)
|
||||
|
||||
def laser_update_tracker_info(self):
|
||||
ret = self.socket_put_and_receive("Ts")
|
||||
|
||||
# remove trailing \n
|
||||
ret = ret.split("\n")[0]
|
||||
|
||||
tracker_values = [float(val) for val in ret.split(",")]
|
||||
self.tracker_info = {
|
||||
"tracker_intensity": tracker_values[2],
|
||||
"threshold_intensity_y": tracker_values[8],
|
||||
"enabled_y": bool(tracker_values[10]),
|
||||
"beampos_y": tracker_values[5],
|
||||
"target_y": tracker_values[6],
|
||||
"piezo_voltage_y": tracker_values[9],
|
||||
"threshold_intensity_z": tracker_values[3],
|
||||
"enabled_z": bool(tracker_values[10]),
|
||||
"beampos_z": tracker_values[0],
|
||||
"target_z": tracker_values[1],
|
||||
"piezo_voltage_z": tracker_values[4],
|
||||
}
|
||||
|
||||
def laser_tracker_galil_enable(self):
|
||||
ftrackz_con = self.get_device_manager().devices.ftrackz.obj.controller
|
||||
ftrackz_con.socket_put_confirmed("tracken=1")
|
||||
ftrackz_con.socket_put_confirmed("trackyct=0")
|
||||
ftrackz_con.socket_put_confirmed("trackzct=0")
|
||||
ftrackz_con.socket_put_confirmed("XQ#Tracker")
|
||||
|
||||
def laser_tracker_on_target(self) -> bool:
|
||||
self.laser_update_tracker_info()
|
||||
if np.isclose(
|
||||
self.tracker_info["beampos_y"], self.tracker_info["target_y"], atol=0.02
|
||||
) and np.isclose(self.tracker_info["beampos_z"], self.tracker_info["target_z"], atol=0.02):
|
||||
return True
|
||||
return False
|
||||
|
||||
def laser_tracker_wait_on_target(self):
|
||||
max_repeat = 25
|
||||
count = 0
|
||||
while not self.laser_tracker_on_target():
|
||||
self.laser_tracker_galil_enable()
|
||||
logger.info("Waiting for laser tracker to reach target.")
|
||||
time.sleep(0.5)
|
||||
count += 1
|
||||
if count > max_repeat:
|
||||
print("Failed to reach laser target position.")
|
||||
raise RtError("Failed to reach laser target position.")
|
||||
|
||||
def slew_rate_limiters_on_target(self) -> bool:
|
||||
ret = int(float(self.socket_put_and_receive("y").strip()))
|
||||
if ret == 3:
|
||||
return True
|
||||
return False
|
||||
|
||||
def pid_y(self) -> float:
|
||||
ret = float(self.socket_put_and_receive("G").strip())
|
||||
return ret
|
||||
|
||||
def read_ssi_interferometer(self, axis_number):
|
||||
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
|
||||
return val
|
||||
|
||||
def laser_tracker_check_signalstrength(self):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
returnval = "disabled"
|
||||
else:
|
||||
returnval = "ok"
|
||||
self.laser_tracker_wait_on_target()
|
||||
|
||||
signal = self.read_ssi_interferometer(1)
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
min_signal = rtx.user_parameter.get("min_signal")
|
||||
low_signal = rtx.user_parameter.get("low_signal")
|
||||
if signal < min_signal:
|
||||
time.sleep(1)
|
||||
if signal < min_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the minimum required signal of {min_signal}. Readjustment requred!\x1b[0m"
|
||||
)
|
||||
returnval = "toolow"
|
||||
# raise RtError("The interferometer signal of tracker is too low.")
|
||||
elif signal < low_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the warning limit of {low_signal}. Readjustment recommended!\x1b[0m"
|
||||
)
|
||||
returnval = "low"
|
||||
return returnval
|
||||
|
||||
def show_signal_strength_interferometer(self):
|
||||
t = PrettyTable()
|
||||
t.title = f"Interferometer signal strength"
|
||||
t.field_names = ["Axis", "Value"]
|
||||
for i in range(4):
|
||||
t.add_row([i, self.read_ssi_interferometer(i)])
|
||||
print(t)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
if not self.feedback_is_running():
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an"
|
||||
" interferometer error."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot start scan because feedback loop is not running or there is an"
|
||||
" interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status()
|
||||
|
||||
if number_of_positions_planned == 0:
|
||||
logger.error("Cannot start scan because no target positions are planned.")
|
||||
raise RtError("Cannot start scan because no target positions are planned.")
|
||||
# hier exception
|
||||
# start a point-by-point scan (for cont scan in flomni it would be "sa")
|
||||
self.socket_put_and_receive("sd")
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive("sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtCommunicationError(
|
||||
f"Expected to receive 3 return values. Instead received {return_table}"
|
||||
)
|
||||
mode = int(float(return_table[0]))
|
||||
# mode 0: direct positioning
|
||||
# mode 1: running internal timer (not tested/used anymore)
|
||||
# mode 2: rt point scan running
|
||||
# mode 3: rt point scan starting
|
||||
# mode 5/6: rt continuous scanning (not available in LamNI)
|
||||
number_of_positions_planned = int(float(return_table[1]))
|
||||
current_position_in_scan = int(float(return_table[2]))
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
def get_device_manager(self):
|
||||
for axis in self._axis:
|
||||
if hasattr(axis, "device_manager") and axis.device_manager:
|
||||
return axis.device_manager
|
||||
raise BECConfigError("Could not access the device_manager")
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}."
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_flomni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
|
||||
class RtFlomniReadbackSignal(RtReadbackSignal):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
time.sleep(0.1)
|
||||
return_table = (self.controller.socket_put_and_receive(f"pr")).split(",")
|
||||
|
||||
current_pos = float(return_table[self.parent.axis_Id_numeric])
|
||||
|
||||
current_pos *= self.parent.sign
|
||||
self.parent.user_setpoint.setpoint = current_pos
|
||||
return current_pos
|
||||
|
||||
|
||||
class RtFlomniSetpointSignal(RtSetpointSignal):
|
||||
setpoint = 0
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
RtError: Raised if interferometer feedback is disabled.
|
||||
|
||||
"""
|
||||
if not self.parent.controller.feedback_is_running():
|
||||
print(
|
||||
"The interferometer feedback is not running. Either it is turned off or and"
|
||||
" interferometer error occured."
|
||||
)
|
||||
raise RtError(
|
||||
"The interferometer feedback is not running. Either it is turned off or and"
|
||||
" interferometer error occured."
|
||||
)
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
|
||||
|
||||
|
||||
class RtFlomniFeedbackRunning(RtSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return int(self.parent.controller.feedback_is_running())
|
||||
|
||||
|
||||
class RtFlomniMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(RtFlomniReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(RtFlomniSetpointSignal, signal_name="setpoint")
|
||||
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2844.psi.ch",
|
||||
port=2222,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
limits=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = RtFlomniController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.device_manager = device_manager
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
self._stopped = False
|
||||
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while not self.controller.slew_rate_limiters_on_target() and not self._stopped:
|
||||
print("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.01)
|
||||
print("Move finished")
|
||||
self._done_moving(success=(not self._stopped))
|
||||
|
||||
self._stopped = False
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "um"
|
||||
|
||||
# how is this used later?
|
||||
|
||||
def stage(self) -> List[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> List[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
self._stopped = True
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rtcontroller = RtFlomniController(
|
||||
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222
|
||||
)
|
||||
rtcontroller.on()
|
||||
rtcontroller.laser_tracker_on()
|
@ -1,848 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RtLamniCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RtLamniError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a CommunicationError was raised. This may happen if the buffer was not empty."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
val = fcn(self, *args, **kwargs)
|
||||
except (RtLamniCommunicationError, RtLamniError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class RtLamniController(Controller):
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"set_rotation_angle",
|
||||
"feedback_disable",
|
||||
"feedback_enable_without_reset",
|
||||
"feedback_disable_and_even_reset_lamni_angle_interferometer",
|
||||
"feedback_enable_with_reset",
|
||||
"add_pos_to_scan",
|
||||
"clear_trajectory_generator",
|
||||
"_set_axis_velocity",
|
||||
"_set_axis_velocity_maximum_speed",
|
||||
"_position_sampling_single_read",
|
||||
"_position_sampling_single_reset_and_start_sampling",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name="RtLamniController",
|
||||
kind=None,
|
||||
parent=None,
|
||||
socket=None,
|
||||
attr_name="",
|
||||
labels=None,
|
||||
):
|
||||
if not hasattr(self, "_initialized") or not self._initialized:
|
||||
self._rtlamni_axis_per_controller = 3
|
||||
self._axis = [None for axis_num in range(self._rtlamni_axis_per_controller)]
|
||||
self._min_scan_buffer_reached = False
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket=socket,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
self.readout_metadata = {}
|
||||
|
||||
def on(self, controller_num=0) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
if not self.connected:
|
||||
try:
|
||||
self.sock.open()
|
||||
# discuss - after disconnect takes a while for the server to be ready again
|
||||
max_retries = 10
|
||||
tries = 0
|
||||
while not self.connected:
|
||||
try:
|
||||
welcome_message = self.sock.receive()
|
||||
self.connected = True
|
||||
except ConnectionResetError as conn_reset:
|
||||
if tries > max_retries:
|
||||
raise conn_reset
|
||||
tries += 1
|
||||
time.sleep(2)
|
||||
except ConnectionRefusedError as conn_error:
|
||||
logger.error("Failed to open a connection to RTLamNI.")
|
||||
raise RtLamniCommunicationError from conn_error
|
||||
|
||||
else:
|
||||
logger.info("The connection has already been established.")
|
||||
# warnings.warn(f"The connection has already been established.", stacklevel=2)
|
||||
|
||||
self._update_flyer_device_info()
|
||||
|
||||
def off(self) -> None:
|
||||
"""Close the socket connection to the controller"""
|
||||
if self.connected:
|
||||
self.sock.close()
|
||||
self.connected = False
|
||||
else:
|
||||
logger.info("The connection is already closed.")
|
||||
|
||||
def set_axis(self, axis: Device, axis_nr: int) -> None:
|
||||
"""Assign an axis to a device instance.
|
||||
|
||||
Args:
|
||||
axis (Device): Device instance (e.g. GalilMotor)
|
||||
axis_nr (int): Controller axis number
|
||||
|
||||
"""
|
||||
self._axis[axis_nr] = axis
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\n".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
def is_axis_moving(self, axis_Id) -> bool:
|
||||
# this checks that axis is on target
|
||||
axis_is_on_target = bool(float(self.socket_put_and_receive(f"o")))
|
||||
return not axis_is_on_target
|
||||
|
||||
# def is_thread_active(self, thread_id: int) -> bool:
|
||||
# val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
# if val == -1:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
@threadlocked
|
||||
def set_rotation_angle(self, val: float):
|
||||
self.socket_put(f"a{(val-300+30.538)/180*np.pi}")
|
||||
|
||||
@threadlocked
|
||||
def stop_all_axes(self):
|
||||
self.socket_put("sc")
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable(self):
|
||||
self.socket_put("J0")
|
||||
logger.info("LamNI Feedback disabled.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity(self, um_per_s):
|
||||
self.socket_put(f"V{um_per_s}")
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity_maximum_speed(self):
|
||||
self.socket_put(f"V0")
|
||||
|
||||
# for developement of soft continuous scanning
|
||||
@threadlocked
|
||||
def _position_sampling_single_reset_and_start_sampling(self):
|
||||
self.socket_put(f"Ss")
|
||||
|
||||
@threadlocked
|
||||
def _position_sampling_single_read(self):
|
||||
(number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive(
|
||||
f"Sr"
|
||||
).split(",")
|
||||
avg_x = float(sum1) / int(number_of_samples)
|
||||
avg_y = float(sum0) / int(number_of_samples)
|
||||
stdev_x = np.sqrt(
|
||||
float(sum1_2) / int(number_of_samples)
|
||||
- np.power(float(sum1) / int(number_of_samples), 2)
|
||||
)
|
||||
stdev_y = np.sqrt(
|
||||
float(sum0_2) / int(number_of_samples)
|
||||
- np.power(float(sum0) / int(number_of_samples), 2)
|
||||
)
|
||||
return (avg_x, avg_y, stdev_x, stdev_y)
|
||||
|
||||
@threadlocked
|
||||
def feedback_enable_without_reset(self):
|
||||
# read current interferometer position
|
||||
return_table = (self.socket_put_and_receive(f"J4")).split(",")
|
||||
x_curr = float(return_table[2])
|
||||
y_curr = float(return_table[1])
|
||||
# set these as closed loop target position
|
||||
self.socket_put(f"pa0,{x_curr:.4f}")
|
||||
self.socket_put(f"pa1,{y_curr:.4f}")
|
||||
self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
|
||||
self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
|
||||
self.socket_put("J5")
|
||||
logger.info("LamNI Feedback enabled (without reset).")
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable_and_even_reset_lamni_angle_interferometer(self):
|
||||
self.socket_put("J6")
|
||||
logger.info("LamNI Feedback disabled including the angular interferometer.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
def get_device_manager(self):
|
||||
for axis in self._axis:
|
||||
if hasattr(axis, "device_manager") and axis.device_manager:
|
||||
return axis.device_manager
|
||||
raise BECConfigError("Could not access the device_manager")
|
||||
|
||||
def get_axis_by_name(self, name):
|
||||
for axis in self._axis:
|
||||
if axis:
|
||||
if axis.name == name:
|
||||
return axis
|
||||
raise RuntimeError(f"Could not find an axis with name {name}")
|
||||
|
||||
@threadlocked
|
||||
def clear_trajectory_generator(self):
|
||||
self.socket_put("sc")
|
||||
logger.info("LamNI scan stopped and deleted, moving to start position")
|
||||
|
||||
def add_pos_to_scan(self, positions) -> None:
|
||||
def send_positions(parent, positions):
|
||||
parent._min_scan_buffer_reached = False
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:05f},0")
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
|
||||
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive(f"sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtLamniCommunicationError(
|
||||
f"Expected to receive 3 return values. Instead received {return_table}"
|
||||
)
|
||||
mode = int(return_table[0])
|
||||
# mode 0: direct positioning
|
||||
# mode 1: running internal timer (not tested/used anymore)
|
||||
# mode 2: rt point scan running
|
||||
# mode 3: rt point scan starting
|
||||
# mode 5/6: rt continuous scanning (not available in LamNI)
|
||||
number_of_positions_planned = int(return_table[1])
|
||||
current_position_in_scan = int(return_table[2])
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtLamniError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status()
|
||||
|
||||
if number_of_positions_planned == 0:
|
||||
logger.error("Cannot start scan because no target positions are planned.")
|
||||
raise RtLamniError("Cannot start scan because no target positions are planned.")
|
||||
# hier exception
|
||||
# start a point-by-point scan (for cont scan in flomni it would be "sa")
|
||||
self.socket_put_and_receive("sd")
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def _update_flyer_device_info(self):
|
||||
flyer_info = self._get_flyer_device_info()
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_info("rt_scan"),
|
||||
messages.DeviceInfoMessage(device="rt_scan", info=flyer_info),
|
||||
)
|
||||
|
||||
def _get_flyer_device_info(self) -> dict:
|
||||
return {
|
||||
"device_name": self.name,
|
||||
"device_attr_name": getattr(self, "attr_name", ""),
|
||||
"device_dotted_name": getattr(self, "dotted_name", ""),
|
||||
"device_info": {
|
||||
"device_base_class": "ophydobject",
|
||||
"signals": [],
|
||||
"hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]},
|
||||
"describe": {},
|
||||
"describe_configuration": {},
|
||||
"sub_devices": [],
|
||||
"custom_user_access": [],
|
||||
},
|
||||
}
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[5])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[8])
|
||||
self.average_lamni_angle += float(return_table[19])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[3])},
|
||||
"average_x_st_fzp": {"value": float(return_table[4])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[5])},
|
||||
"target_y": {"value": float(return_table[6])},
|
||||
"average_y_st_fzp": {"value": float(return_table[7])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[8])},
|
||||
"average_cap1": {"value": float(return_table[9])},
|
||||
"stdev_cap1": {"value": float(return_table[10])},
|
||||
"average_cap2": {"value": float(return_table[11])},
|
||||
"stdev_cap2": {"value": float(return_table[12])},
|
||||
"average_cap3": {"value": float(return_table[13])},
|
||||
"stdev_cap3": {"value": float(return_table[14])},
|
||||
"average_cap4": {"value": float(return_table[15])},
|
||||
"stdev_cap4": {"value": float(return_table[16])},
|
||||
"average_cap5": {"value": float(return_table[17])},
|
||||
"stdev_cap5": {"value": float(return_table[18])},
|
||||
"average_angle_interf_ST": {"value": float(return_table[19])},
|
||||
"stdev_angle_interf_ST": {"value": float(return_table[20])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)},
|
||||
}
|
||||
return signals
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
previous_point_in_scan = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}."
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_lamni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
),
|
||||
)
|
||||
|
||||
def feedback_status_angle_lamni(self) -> bool:
|
||||
return_table = (self.socket_put_and_receive(f"J7")).split(",")
|
||||
logger.debug(
|
||||
f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}"
|
||||
)
|
||||
return bool(return_table[0])
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
if not self.feedback_status_angle_lamni():
|
||||
self.feedback_disable_and_even_reset_lamni_angle_interferometer()
|
||||
logger.info(f"LamNI resetting interferometer inclusive angular interferomter.")
|
||||
else:
|
||||
self.feedback_disable()
|
||||
logger.info(
|
||||
f"LamNI resetting interferomter except angular interferometer which is already running."
|
||||
)
|
||||
|
||||
# set these as closed loop target position
|
||||
|
||||
self.socket_put(f"pa0,0")
|
||||
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
|
||||
self.socket_put(f"pa1,0")
|
||||
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
|
||||
self.socket_put(
|
||||
f"pa2,0"
|
||||
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
|
||||
|
||||
galil_controller_rt_status = (
|
||||
self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
|
||||
)
|
||||
|
||||
if galil_controller_rt_status == 0:
|
||||
logger.error(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
raise RtLamniError(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
|
||||
time.sleep(0.03)
|
||||
|
||||
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
|
||||
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamx center is not defined")
|
||||
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
|
||||
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamy center is not defined")
|
||||
lsamx_center = lsamx_user_params.get("center")
|
||||
lsamy_center = lsamy_user_params.get("center")
|
||||
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
|
||||
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
|
||||
self.socket_put("J1")
|
||||
|
||||
_waitforfeedbackctr = 0
|
||||
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
|
||||
while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100:
|
||||
time.sleep(0.01)
|
||||
_waitforfeedbackctr = _waitforfeedbackctr + 1
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtLamniError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# ptychography_alignment_done = 0
|
||||
|
||||
def set_device_enabled(self, device_name: str, enabled: bool) -> None:
|
||||
"""enable / disable a device"""
|
||||
if device_name not in self.get_device_manager().devices:
|
||||
logger.warning(
|
||||
f"Device {device_name} is not configured and cannot be enabled/disabled."
|
||||
)
|
||||
return
|
||||
self.get_device_manager().devices[device_name].read_only = not enabled
|
||||
|
||||
|
||||
class RtLamniSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class RtLamniSignalRO(RtLamniSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class RtLamniReadbackSignal(RtLamniSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
return_table = (self.controller.socket_put_and_receive(f"J4")).split(",")
|
||||
print(return_table)
|
||||
if self.parent.axis_Id_numeric == 0:
|
||||
readback_index = 2
|
||||
elif self.parent.axis_Id_numeric == 1:
|
||||
readback_index = 1
|
||||
else:
|
||||
raise RtLamniError("Currently, only two axes are supported.")
|
||||
|
||||
current_pos = float(return_table[readback_index])
|
||||
|
||||
current_pos *= self.parent.sign
|
||||
return current_pos
|
||||
|
||||
|
||||
class RtLamniSetpointSignal(RtLamniSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
RtLamniError: Raised if interferometer feedback is disabled.
|
||||
|
||||
"""
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.controller.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
if interferometer_feedback_not_running != 0:
|
||||
raise RtLamniError(
|
||||
"The interferometer feedback is not running. Either it is turned off or and interferometer error occured."
|
||||
)
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
|
||||
|
||||
|
||||
class RtLamniMotorIsMoving(RtLamniSignalRO):
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class RtLamniFeedbackRunning(RtLamniSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
class RtLamniMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(RtLamniReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint")
|
||||
|
||||
motor_is_moving = Cpt(RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2680.psi.ch",
|
||||
port=3333,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
limits=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = RtLamniController(socket=socket_cls(host=host, port=port))
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.device_manager = device_manager
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
print("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.01)
|
||||
print("Move finished")
|
||||
self._done_moving()
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "um"
|
||||
|
||||
# how is this used later?
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
mock = False
|
||||
if not mock:
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)
|
||||
rty.stage()
|
||||
status = rty.move(0, wait=True)
|
||||
status = rty.move(10, wait=True)
|
||||
rty.read()
|
||||
|
||||
rty.get()
|
||||
rty.describe()
|
||||
|
||||
rty.unstage()
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rtx.stage()
|
||||
# rty.stage()
|
@ -1,818 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RtCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RtError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a CommunicationError was raised. This may happen if the buffer was not empty."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
val = fcn(self, *args, **kwargs)
|
||||
except (RtCommunicationError, RtError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class RtController(Controller):
|
||||
_axes_per_controller = 3
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"set_rotation_angle",
|
||||
"feedback_disable",
|
||||
"feedback_enable_without_reset",
|
||||
"feedback_disable_and_even_reset_lamni_angle_interferometer",
|
||||
"feedback_enable_with_reset",
|
||||
"add_pos_to_scan",
|
||||
"clear_trajectory_generator",
|
||||
"_set_axis_velocity",
|
||||
"_set_axis_velocity_maximum_speed",
|
||||
"_position_sampling_single_read",
|
||||
"_position_sampling_single_reset_and_start_sampling",
|
||||
]
|
||||
|
||||
def on(self, controller_num=0) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
# if not self.connected:
|
||||
# try:
|
||||
# self.sock.open()
|
||||
# # discuss - after disconnect takes a while for the server to be ready again
|
||||
# max_retries = 10
|
||||
# tries = 0
|
||||
# while not self.connected:
|
||||
# try:
|
||||
# welcome_message = self.sock.receive()
|
||||
# self.connected = True
|
||||
# except ConnectionResetError as conn_reset:
|
||||
# if tries > max_retries:
|
||||
# raise conn_reset
|
||||
# tries += 1
|
||||
# time.sleep(2)
|
||||
# except ConnectionRefusedError as conn_error:
|
||||
# logger.error("Failed to open a connection to RTLamNI.")
|
||||
# raise RtCommunicationError from conn_error
|
||||
|
||||
# else:
|
||||
# logger.info("The connection has already been established.")
|
||||
# # warnings.warn(f"The connection has already been established.", stacklevel=2)
|
||||
super().on()
|
||||
# self._update_flyer_device_info()
|
||||
|
||||
def set_axis(self, axis: Device, axis_nr: int) -> None:
|
||||
"""Assign an axis to a device instance.
|
||||
|
||||
Args:
|
||||
axis (Device): Device instance (e.g. GalilMotor)
|
||||
axis_nr (int): Controller axis number
|
||||
|
||||
"""
|
||||
self._axis[axis_nr] = axis
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\n".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
def is_axis_moving(self, axis_Id) -> bool:
|
||||
# this checks that axis is on target
|
||||
axis_is_on_target = bool(float(self.socket_put_and_receive(f"o")))
|
||||
return not axis_is_on_target
|
||||
|
||||
# def is_thread_active(self, thread_id: int) -> bool:
|
||||
# val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
# if val == -1:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
@threadlocked
|
||||
def set_rotation_angle(self, val: float):
|
||||
self.socket_put(f"a{(val-300+30.538)/180*np.pi}")
|
||||
|
||||
@threadlocked
|
||||
def stop_all_axes(self):
|
||||
self.socket_put("sc")
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable(self):
|
||||
self.socket_put("J0")
|
||||
logger.info("LamNI Feedback disabled.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity(self, um_per_s):
|
||||
self.socket_put(f"V{um_per_s}")
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity_maximum_speed(self):
|
||||
self.socket_put(f"V0")
|
||||
|
||||
# for developement of soft continuous scanning
|
||||
@threadlocked
|
||||
def _position_sampling_single_reset_and_start_sampling(self):
|
||||
self.socket_put(f"Ss")
|
||||
|
||||
@threadlocked
|
||||
def _position_sampling_single_read(self):
|
||||
(number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive(
|
||||
f"Sr"
|
||||
).split(",")
|
||||
avg_x = float(sum1) / int(number_of_samples)
|
||||
avg_y = float(sum0) / int(number_of_samples)
|
||||
stdev_x = np.sqrt(
|
||||
float(sum1_2) / int(number_of_samples)
|
||||
- np.power(float(sum1) / int(number_of_samples), 2)
|
||||
)
|
||||
stdev_y = np.sqrt(
|
||||
float(sum0_2) / int(number_of_samples)
|
||||
- np.power(float(sum0) / int(number_of_samples), 2)
|
||||
)
|
||||
return (avg_x, avg_y, stdev_x, stdev_y)
|
||||
|
||||
@threadlocked
|
||||
def feedback_enable_without_reset(self):
|
||||
# read current interferometer position
|
||||
return_table = (self.socket_put_and_receive(f"J4")).split(",")
|
||||
x_curr = float(return_table[2])
|
||||
y_curr = float(return_table[1])
|
||||
# set these as closed loop target position
|
||||
self.socket_put(f"pa0,{x_curr:.4f}")
|
||||
self.socket_put(f"pa1,{y_curr:.4f}")
|
||||
self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
|
||||
self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
|
||||
self.socket_put("J5")
|
||||
logger.info("LamNI Feedback enabled (without reset).")
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable_and_even_reset_lamni_angle_interferometer(self):
|
||||
self.socket_put("J6")
|
||||
logger.info("LamNI Feedback disabled including the angular interferometer.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
def get_device_manager(self):
|
||||
for axis in self._axis:
|
||||
if hasattr(axis, "device_manager") and axis.device_manager:
|
||||
return axis.device_manager
|
||||
raise BECConfigError("Could not access the device_manager")
|
||||
|
||||
def get_axis_by_name(self, name):
|
||||
for axis in self._axis:
|
||||
if axis:
|
||||
if axis.name == name:
|
||||
return axis
|
||||
raise RuntimeError(f"Could not find an axis with name {name}")
|
||||
|
||||
@threadlocked
|
||||
def clear_trajectory_generator(self):
|
||||
self.socket_put("sc")
|
||||
logger.info("LamNI scan stopped and deleted, moving to start position")
|
||||
|
||||
def add_pos_to_scan(self, positions) -> None:
|
||||
def send_positions(parent, positions):
|
||||
parent._min_scan_buffer_reached = False
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]},{pos[1]},0")
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
|
||||
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive(f"sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtCommunicationError(
|
||||
f"Expected to receive 3 return values. Instead received {return_table}"
|
||||
)
|
||||
mode = int(return_table[0])
|
||||
# mode 0: direct positioning
|
||||
# mode 1: running internal timer (not tested/used anymore)
|
||||
# mode 2: rt point scan running
|
||||
# mode 3: rt point scan starting
|
||||
# mode 5/6: rt continuous scanning (not available in LamNI)
|
||||
number_of_positions_planned = int(return_table[1])
|
||||
current_position_in_scan = int(return_table[2])
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status()
|
||||
|
||||
if number_of_positions_planned == 0:
|
||||
logger.error("Cannot start scan because no target positions are planned.")
|
||||
raise RtError("Cannot start scan because no target positions are planned.")
|
||||
# hier exception
|
||||
# start a point-by-point scan (for cont scan in flomni it would be "sa")
|
||||
self.socket_put_and_receive("sd")
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def _update_flyer_device_info(self):
|
||||
flyer_info = self._get_flyer_device_info()
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_info("rt_scan"),
|
||||
messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(),
|
||||
)
|
||||
|
||||
def _get_flyer_device_info(self) -> dict:
|
||||
return {
|
||||
"device_name": self.name,
|
||||
"device_attr_name": getattr(self, "attr_name", ""),
|
||||
"device_dotted_name": getattr(self, "dotted_name", ""),
|
||||
"device_info": {
|
||||
"device_base_class": "ophydobject",
|
||||
"signals": [],
|
||||
"hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]},
|
||||
"describe": {},
|
||||
"describe_configuration": {},
|
||||
"sub_devices": [],
|
||||
"custom_user_access": [],
|
||||
},
|
||||
}
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[5])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[8])
|
||||
self.average_lamni_angle += float(return_table[19])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[3])},
|
||||
"average_x_st_fzp": {"value": float(return_table[4])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[5])},
|
||||
"target_y": {"value": float(return_table[6])},
|
||||
"average_y_st_fzp": {"value": float(return_table[7])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[8])},
|
||||
"average_cap1": {"value": float(return_table[9])},
|
||||
"stdev_cap1": {"value": float(return_table[10])},
|
||||
"average_cap2": {"value": float(return_table[11])},
|
||||
"stdev_cap2": {"value": float(return_table[12])},
|
||||
"average_cap3": {"value": float(return_table[13])},
|
||||
"stdev_cap3": {"value": float(return_table[14])},
|
||||
"average_cap4": {"value": float(return_table[15])},
|
||||
"stdev_cap4": {"value": float(return_table[16])},
|
||||
"average_cap5": {"value": float(return_table[17])},
|
||||
"stdev_cap5": {"value": float(return_table[18])},
|
||||
"average_angle_interf_ST": {"value": float(return_table[19])},
|
||||
"stdev_angle_interf_ST": {"value": float(return_table[20])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)},
|
||||
}
|
||||
return signals
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
previous_point_in_scan = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}."
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_lamni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
def feedback_status_angle_lamni(self) -> bool:
|
||||
return_table = (self.socket_put_and_receive(f"J7")).split(",")
|
||||
logger.debug(
|
||||
f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}"
|
||||
)
|
||||
return bool(return_table[0])
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
if not self.feedback_status_angle_lamni():
|
||||
self.feedback_disable_and_even_reset_lamni_angle_interferometer()
|
||||
logger.info(f"LamNI resetting interferometer inclusive angular interferomter.")
|
||||
else:
|
||||
self.feedback_disable()
|
||||
logger.info(
|
||||
f"LamNI resetting interferomter except angular interferometer which is already running."
|
||||
)
|
||||
|
||||
# set these as closed loop target position
|
||||
|
||||
self.socket_put(f"pa0,0")
|
||||
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
|
||||
self.socket_put(f"pa1,0")
|
||||
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
|
||||
self.socket_put(
|
||||
f"pa2,0"
|
||||
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
|
||||
|
||||
galil_controller_rt_status = (
|
||||
self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
|
||||
)
|
||||
|
||||
if galil_controller_rt_status == 0:
|
||||
logger.error(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
|
||||
time.sleep(0.03)
|
||||
|
||||
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
|
||||
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamx center is not defined")
|
||||
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
|
||||
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamy center is not defined")
|
||||
lsamx_center = lsamx_user_params.get("center")
|
||||
lsamy_center = lsamy_user_params.get("center")
|
||||
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
|
||||
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
|
||||
self.socket_put("J1")
|
||||
|
||||
_waitforfeedbackctr = 0
|
||||
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
|
||||
while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100:
|
||||
time.sleep(0.01)
|
||||
_waitforfeedbackctr = _waitforfeedbackctr + 1
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# ptychography_alignment_done = 0
|
||||
|
||||
def set_device_enabled(self, device_name: str, enabled: bool) -> None:
|
||||
"""enable / disable a device"""
|
||||
if device_name not in self.get_device_manager().devices:
|
||||
logger.warning(
|
||||
f"Device {device_name} is not configured and cannot be enabled/disabled."
|
||||
)
|
||||
return
|
||||
self.get_device_manager().devices[device_name].read_only = not enabled
|
||||
|
||||
|
||||
class RtSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class RtSignalRO(RtSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class RtReadbackSignal(RtSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
return_table = (self.controller.socket_put_and_receive(f"J4")).split(",")
|
||||
print(return_table)
|
||||
if self.parent.axis_Id_numeric == 0:
|
||||
readback_index = 2
|
||||
elif self.parent.axis_Id_numeric == 1:
|
||||
readback_index = 1
|
||||
else:
|
||||
raise RtError("Currently, only two axes are supported.")
|
||||
|
||||
current_pos = float(return_table[readback_index])
|
||||
|
||||
current_pos *= self.parent.sign
|
||||
return current_pos
|
||||
|
||||
|
||||
class RtSetpointSignal(RtSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
RtError: Raised if interferometer feedback is disabled.
|
||||
|
||||
"""
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.controller.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
if interferometer_feedback_not_running != 0:
|
||||
raise RtError(
|
||||
"The interferometer feedback is not running. Either it is turned off or and interferometer error occured."
|
||||
)
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
|
||||
|
||||
|
||||
class RtMotorIsMoving(RtSignalRO):
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class RtFeedbackRunning(RtSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
class RtMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(RtReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(RtSetpointSignal, signal_name="setpoint")
|
||||
|
||||
motor_is_moving = Cpt(RtMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2680.psi.ch",
|
||||
port=3333,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
limits=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = RtController(socket=socket_cls(host=host, port=port))
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.device_manager = device_manager
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
print("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.01)
|
||||
print("Move finished")
|
||||
self._done_moving()
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "um"
|
||||
|
||||
# how is this used later?
|
||||
|
||||
def stage(self) -> List[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> List[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
mock = False
|
||||
if not mock:
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)
|
||||
rty.stage()
|
||||
status = rty.move(0, wait=True)
|
||||
status = rty.move(10, wait=True)
|
||||
rty.read()
|
||||
|
||||
rty.get()
|
||||
rty.describe()
|
||||
|
||||
rty.unstage()
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rtx.stage()
|
||||
# rty.stage()
|
@ -1,9 +0,0 @@
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal
|
||||
|
||||
|
||||
class Xeye(Device):
|
||||
save_frame = Cpt(EpicsSignal, "XOMNY-XEYE-SAVEFRAME:0")
|
||||
acquisition_done = Cpt(EpicsSignal, "XOMNY-XEYE-ACQDONE:0")
|
||||
acquisition = Cpt(EpicsSignal, "XOMNY-XEYE-ACQ:0")
|
||||
x_width = Cpt(EpicsSignal, "XOMNY-XEYE-XWIDTH_X:0")
|
@ -1,2 +0,0 @@
|
||||
from .smaract_controller import SmaractController
|
||||
from .smaract_ophyd import SmaractMotor
|
@ -1,45 +0,0 @@
|
||||
import json
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.ophydobj import OphydObject
|
||||
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
|
||||
def get_user_functions(obj) -> list:
|
||||
exclude_list = ["log", "SUB_CONNECTION_CHANGE"]
|
||||
exclude_classes = [Device, OphydObject, PositionerBase, Signal, Cpt]
|
||||
for cls in exclude_classes:
|
||||
exclude_list.extend(dir(cls))
|
||||
access_list = [
|
||||
func for func in dir(obj) if not func.startswith("_") and func not in exclude_list
|
||||
]
|
||||
|
||||
return access_list
|
||||
|
||||
|
||||
def is_serializable(f) -> bool:
|
||||
try:
|
||||
json.dumps(f)
|
||||
return True
|
||||
except (TypeError, OverflowError):
|
||||
return False
|
||||
|
||||
|
||||
def get_user_interface(obj, obj_interface):
|
||||
# user_funcs = get_user_functions(obj)
|
||||
for f in [f for f in dir(obj) if f in obj.USER_ACCESS]:
|
||||
if f == "controller" or f == "on":
|
||||
print(f)
|
||||
m = getattr(obj, f)
|
||||
if not callable(m):
|
||||
if is_serializable(m):
|
||||
obj_interface[f] = {"type": type(m).__name__}
|
||||
elif isinstance(m, SocketMock):
|
||||
pass
|
||||
else:
|
||||
obj_interface[f] = get_user_interface(m, {})
|
||||
else:
|
||||
obj_interface[f] = {"type": "func"}
|
||||
return obj_interface
|
@ -1,507 +0,0 @@
|
||||
import enum
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from prettytable import PrettyTable
|
||||
from typeguard import typechecked
|
||||
|
||||
from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode
|
||||
from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked
|
||||
|
||||
logger = logging.getLogger("smaract_controller")
|
||||
|
||||
|
||||
class SmaractCommunicationMode(enum.Enum):
|
||||
SYNC = 0
|
||||
ASYNC = 1
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a SmaractCommunicationError was raised. This may happen if the buffer was not empty."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
val = fcn(self, *args, **kwargs)
|
||||
except (SmaractCommunicationError, SmaractErrorCode):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SmaractChannelStatus(enum.Enum):
|
||||
STOPPED = 0
|
||||
STEPPING = 1
|
||||
SCANNING = 2
|
||||
HOLDING = 3
|
||||
TARGETING = 4
|
||||
MOVE_DELAY = 5
|
||||
CALIBRATING = 6
|
||||
FINDING_REFERENCE_MARK = 7
|
||||
LOCKED = 9
|
||||
|
||||
|
||||
class SmaractSensorDefinition:
|
||||
def __init__(self, symbol, type_code, positioner_series, comment, reference_type) -> None:
|
||||
self.symbol = symbol
|
||||
self.type_code = type_code
|
||||
self.comment = comment
|
||||
self.positioner_series = positioner_series
|
||||
self.reference_type = reference_type
|
||||
|
||||
|
||||
class SmaractSensors:
|
||||
smaract_sensor_definition_file = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "smaract_sensors.json"
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.avail_sensors = {}
|
||||
|
||||
with open(self.smaract_sensor_definition_file) as json_file:
|
||||
sensor_list = json.load(json_file)
|
||||
for sensor in sensor_list:
|
||||
self.avail_sensors[sensor["type_code"]] = SmaractSensorDefinition(**sensor)
|
||||
|
||||
|
||||
class SmaractController(Controller):
|
||||
_axes_per_controller = 6
|
||||
_initialized = False
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"smaract_show_all",
|
||||
"move_open_loop_steps",
|
||||
"find_reference_mark",
|
||||
"describe",
|
||||
"axis_is_referenced",
|
||||
"all_axes_referenced",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name="SmaractController",
|
||||
kind=None,
|
||||
parent=None,
|
||||
socket_cls=None,
|
||||
socket_host=None,
|
||||
socket_port=None,
|
||||
attr_name="",
|
||||
labels=None,
|
||||
):
|
||||
if not self._initialized:
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket_cls=socket_cls,
|
||||
socket_host=socket_host,
|
||||
socket_port=socket_port,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
self._sensors = SmaractSensors()
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str):
|
||||
self.sock.put(f":{val}\n".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self):
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@threadlocked
|
||||
def socket_put_and_receive(
|
||||
self, val: str, remove_trailing_chars=True, check_for_errors=True, raise_if_not_status=False
|
||||
) -> str:
|
||||
self.socket_put(val)
|
||||
return_val = ""
|
||||
max_wait_time = 1
|
||||
elapsed_time = 0
|
||||
sleep_time = 0.01
|
||||
while True:
|
||||
ret = self.socket_get()
|
||||
return_val += ret
|
||||
if ret.endswith("\n"):
|
||||
break
|
||||
time.sleep(sleep_time)
|
||||
elapsed_time += sleep_time
|
||||
if elapsed_time > max_wait_time:
|
||||
break
|
||||
if remove_trailing_chars:
|
||||
return_val = self._remove_trailing_characters(return_val)
|
||||
logger.debug(f"Sending {val}; Returned {return_val}")
|
||||
if check_for_errors:
|
||||
self._check_for_error(return_val, raise_if_not_status=raise_if_not_status)
|
||||
return return_val
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_status(self, axis_Id_numeric: int) -> SmaractChannelStatus:
|
||||
"""Returns the current movement status code of a positioner or end effector.This command can be used to check whether a previously issued movement command has been completed.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number
|
||||
|
||||
Returns:
|
||||
SmaractChannelStatus: Channel status
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(f"GS{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":S{axis_Id_numeric}"):
|
||||
return SmaractChannelStatus(int(return_val.split(",")[1]))
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def is_axis_moving(self, axis_Id_numeric: int) -> bool:
|
||||
"""Check if axis is moving. Returns true upon open loop move, scanning, closed loop move or reference mark search.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
|
||||
Returns:
|
||||
bool: True if axis is moving.
|
||||
"""
|
||||
axis_status = self.get_status(axis_Id_numeric)
|
||||
return axis_status in [
|
||||
SmaractChannelStatus.STEPPING,
|
||||
SmaractChannelStatus.SCANNING,
|
||||
SmaractChannelStatus.TARGETING,
|
||||
SmaractChannelStatus.FINDING_REFERENCE_MARK,
|
||||
]
|
||||
|
||||
@retry_once
|
||||
def stop_all_axes(self):
|
||||
return [
|
||||
self.socket_put_and_receive(f"S{ax.axis_Id_numeric}", raise_if_not_status=True)
|
||||
for ax in self._axis
|
||||
if ax is not None
|
||||
]
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def axis_is_referenced(self, axis_Id_numeric: int) -> bool:
|
||||
return_val = self.socket_put_and_receive(f"GPPK{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":PPK{axis_Id_numeric}"):
|
||||
return bool(int(return_val.split(",")[1]))
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
return all(
|
||||
self.axis_is_referenced(ax.axis_Id_numeric) for ax in self._axis if ax is not None
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_position(self, axis_Id_numeric: int) -> float:
|
||||
"""Returns the current position of a positioner.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
|
||||
Returns:
|
||||
float: Position in mm
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(f"GP{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":P{axis_Id_numeric}"):
|
||||
return float(return_val.split(",")[1]) / 1e6
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
@typechecked
|
||||
def move_axis_to_absolute_position(
|
||||
self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000
|
||||
) -> None:
|
||||
"""Instructs a positioner to move to a specific position.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
target_val (float): Target position in mm.
|
||||
hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000.
|
||||
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"MPA{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}",
|
||||
raise_if_not_status=True,
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
@typechecked
|
||||
def move_axis_to_relative_position(
|
||||
self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000
|
||||
) -> None:
|
||||
"""Instructs a positioner to move to a position relative to its current position.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
target_val (float): Relative position to move to in mm.
|
||||
hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000.
|
||||
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"MPR{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}",
|
||||
raise_if_not_status=True,
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
@typechecked
|
||||
def move_open_loop_steps(
|
||||
self, axis_Id_numeric: int, steps: int, amplitude: int = 4000, frequency: int = 2000
|
||||
) -> None:
|
||||
"""Move open loop steps. It performs a burst of steps with the given parameters.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
steps (int): Number and direction of steps to perform. The valid range is -30,000..30,000. A value of 0 stops the positioner, but see S command. A value of 30,000 or -30,000 performs an unbounded move. This should be used with caution since the positioner will only stop on an S command.
|
||||
amplitude (int): Amplitude that the steps are performed with. Lower amplitude values result in a smaller step width. The parameter must be given as a 12bit value (range 0..4,095). 0 corresponds to 0V, 4,095 to 100V. Default: 4000
|
||||
frequency (int): Frequency in Hz that the steps are performed with. The valid range is 1..18,500. Default: 2000.
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True
|
||||
)
|
||||
|
||||
@retry_once
|
||||
def get_communication_mode(self) -> SmaractCommunicationMode:
|
||||
return_val = self.socket_put_and_receive("GCM")
|
||||
if self._message_starts_with(return_val, f":CM"):
|
||||
return SmaractCommunicationMode(int(return_val.strip(":CM")))
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_channel_type(self, axis_Id_numeric) -> str:
|
||||
return_val = self.socket_put_and_receive(f"GCT{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":CT{axis_Id_numeric}"):
|
||||
return return_val.split(",")[1]
|
||||
|
||||
@retry_once
|
||||
def get_interface_version(self) -> str:
|
||||
"""This command may be used to retrieve the interface version of the system. It is useful to check if changes
|
||||
have been made to the software interface. An application may check the version in order to ensure that the
|
||||
system behaves as the application expects it to do.
|
||||
|
||||
Returns:
|
||||
str: interface version
|
||||
"""
|
||||
return_val = self.socket_put_and_receive("GIV")
|
||||
if self._message_starts_with(return_val, f":IV"):
|
||||
return return_val.strip(":IV")
|
||||
|
||||
@retry_once
|
||||
def get_number_of_channels(self) -> int:
|
||||
"""This command may be used to determine how many control channels are available on a system. This
|
||||
includes positioner channels and end effector channels. Each channel is of a specific type. Use the GCT
|
||||
command to determine the types of the channels.
|
||||
Note that the number of channels does not represent the number positioners and/or end effectors that are
|
||||
currently connected to the system.
|
||||
The channel indexes throughout the interface are zero based. If your system has N channels then the valid
|
||||
range for a channel index is 0.. N-1.
|
||||
|
||||
Returns:
|
||||
int: number of channels
|
||||
"""
|
||||
return_val = self.socket_put_and_receive("GNC")
|
||||
if self._message_starts_with(return_val, f":N"):
|
||||
return int(return_val.strip(":N"))
|
||||
|
||||
@retry_once
|
||||
def get_system_id(self) -> str:
|
||||
"""This command may be used to physically identify a system connected to the PC. Each system has a unique
|
||||
ID which makes it possible to distinguish one from another.
|
||||
The ID returned is a generic decimal number that uniquely identifies the system.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive("GSI")
|
||||
if self._message_starts_with(return_val, f":ID"):
|
||||
return return_val.strip(":ID")
|
||||
|
||||
@retry_once
|
||||
def reset(self) -> None:
|
||||
"""When this command is sent the system will perform a reset. It has the same effect as a power down/power
|
||||
up cycle. The system replies with an acknowledge string before resetting itself.
|
||||
"""
|
||||
self.socket_put_and_receive("R", raise_if_not_status=True)
|
||||
|
||||
@retry_once
|
||||
def set_hcm_mode(self, mode: int):
|
||||
"""If a Hand Control Module (HCM) is connected to the system, this command may be used to enable or
|
||||
disable it in order to avoid interference while the software is in control of the system. There are three possible
|
||||
modes to set:
|
||||
0: In this mode the Hand Control Module is disabled. It may not be used to control positioners.
|
||||
1: This is the default setting where the Hand Control Module may be used to control the positioners.
|
||||
2: In this mode the Hand Control Module cannot be used to control the positioners. However, if there
|
||||
are positioners with sensors attached, their position data will still be displayed.
|
||||
|
||||
Args:
|
||||
mode (int): HCM mode
|
||||
|
||||
"""
|
||||
if mode not in range(3):
|
||||
raise ValueError(f"HCM mode must be 0, 1 or 2. Received: {mode}.")
|
||||
self.socket_put_and_receive(f"SHE{mode}", raise_if_not_status=True)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_position_limits(self, axis_Id_numeric: int) -> list:
|
||||
"""May be used to read out the travel range limit that is currently
|
||||
configured for a linear channel.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis
|
||||
|
||||
Returns:
|
||||
list: [low_limit, high_limit] in mm
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(f"GPL{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":GPL{axis_Id_numeric}"):
|
||||
return [
|
||||
float(limit) / 1e6
|
||||
for limit in return_val.strip(f":GPL{axis_Id_numeric},").split(",")
|
||||
]
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def set_position_limits(
|
||||
self, axis_Id_numeric: int, low_limit: float, high_limit: float
|
||||
) -> None:
|
||||
"""For positioners with integrated sensors this command may be used to limit the travel range of a linear
|
||||
positioner by software. By default there is no limit set. If defined the
|
||||
positioner will not move beyond the limit. This affects open-loop as well as closed-loop movements.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis
|
||||
low_limit (float): low limit in mm
|
||||
high_limit (float): high limit in mm
|
||||
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"SPL{axis_Id_numeric},{np.round(low_limit*1e6)},{np.round(high_limit*1e6)}",
|
||||
raise_if_not_status=True,
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_sensor_type(self, axis_Id_numeric: int) -> SmaractSensorDefinition:
|
||||
return_val = self.socket_put_and_receive(f"GST{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":ST{axis_Id_numeric}"):
|
||||
return self._sensors.avail_sensors.get(int(return_val.strip(f":ST{axis_Id_numeric},")))
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def find_reference_mark(
|
||||
self, axis_Id_numeric: int, direction: int, holdTime: int, autoZero: int
|
||||
) -> None:
|
||||
return_val = self.socket_put_and_receive(
|
||||
f"FRM{axis_Id_numeric},{direction},{holdTime},{autoZero}"
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def set_closed_loop_move_speed(self, axis_Id_numeric: int, move_speed: float) -> None:
|
||||
"""This command configures the speed control feature of a channel for closed-loop commands move_axis_to_absolute_position. By default the speed control is inactive. In this state the behavior of closed-loop commands is influenced by the maximum driving frequency. If a movement speed is configured, all following closed-loop commands will be executed with the new speed.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
move_speed (float): Movement speed given in mm/s for linear positioners. The valid range is 0 .. 100. A value of 0 (default) deactivates the speed control feature.
|
||||
"""
|
||||
move_speed_in_nm_per_s = int(round(move_speed * 1e6))
|
||||
|
||||
if move_speed_in_nm_per_s > 100e6 or move_speed_in_nm_per_s < 0:
|
||||
raise ValueError("Move speed must be within 0 to 100 mm/s.")
|
||||
|
||||
self.socket_put_and_receive(
|
||||
f"SCLS{axis_Id_numeric},{move_speed_in_nm_per_s}", raise_if_not_status=True
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_closed_loop_move_speed(self, axis_Id_numeric: int) -> float:
|
||||
"""Returns the currently configured movement speed that is used for closed-loop commands for a channel.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
|
||||
Returns:
|
||||
float: move speed in mm/s. A return value of 0 means that the speed control feature is disabled.
|
||||
"""
|
||||
|
||||
return_val = self.socket_put_and_receive(f"GCLS{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":CLS{axis_Id_numeric}"):
|
||||
return float(return_val.strip(f":CLS{axis_Id_numeric},")) * 1e6
|
||||
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = ["Axis", "Name", "Connected", "Referenced", "Closed Loop Speed", "Position"]
|
||||
for ax in range(self._axes_per_controller):
|
||||
axis = self._axis[ax]
|
||||
if axis is not None:
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
axis.name,
|
||||
axis.connected,
|
||||
self.axis_is_referenced(axis.axis_Id_numeric),
|
||||
self.get_closed_loop_move_speed(axis.axis_Id_numeric),
|
||||
axis.readback.read().get(axis.name).get("value"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row([None for t in t.field_names])
|
||||
print(t)
|
||||
|
||||
@axis_checked
|
||||
def _error_str(self, axis_Id_numeric: int, error_number: int):
|
||||
return f":E{axis_Id_numeric},{error_number}"
|
||||
|
||||
def _get_error_code_from_msg(self, msg: str) -> int:
|
||||
if msg.startswith(":E"):
|
||||
return int(msg.split(",")[-1])
|
||||
else:
|
||||
return -1
|
||||
|
||||
def _get_axis_from_error_code(self, msg: str) -> int:
|
||||
if msg.startswith(":E"):
|
||||
try:
|
||||
return int(msg.strip(":E").split(",")[0])
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def _check_for_error(self, msg: str, axis_Id_numeric: int = None, raise_if_not_status=False):
|
||||
if msg.startswith(":E"):
|
||||
if axis_Id_numeric is None:
|
||||
axis_Id_numeric = self._get_axis_from_error_code(msg)
|
||||
|
||||
if axis_Id_numeric is None:
|
||||
raise SmaractCommunicationError(
|
||||
"Could not retrieve axis number from error message."
|
||||
)
|
||||
|
||||
if msg != self._error_str(axis_Id_numeric, 0):
|
||||
error_code = self._get_error_code_from_msg(msg)
|
||||
if error_code != 0:
|
||||
raise SmaractErrorCode(error_code)
|
||||
else:
|
||||
if raise_if_not_status:
|
||||
raise SmaractCommunicationError(
|
||||
"Expected error / status message but failed to parse it."
|
||||
)
|
||||
|
||||
def _remove_trailing_characters(self, var: str) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\n")[0]
|
||||
return var
|
||||
|
||||
def _message_starts_with(self, msg: str, leading_chars: str) -> bool:
|
||||
if msg.startswith(leading_chars):
|
||||
return True
|
||||
raise SmaractCommunicationError(
|
||||
f"Expected to receive a return message starting with {leading_chars} but instead"
|
||||
f" received '{msg}'"
|
||||
)
|
@ -1,47 +0,0 @@
|
||||
SMARACT_ERRORS = {
|
||||
1: "Syntax Error: The command could not be processed due to a syntactical error.",
|
||||
2: "Invalid Command Error: The command given is not known to the system.",
|
||||
3: "Overflow Error: This error occurs if a parameter given is too large and therefore cannot be processed.",
|
||||
4: "Parse Error: The command could not be processed due to a parse error.",
|
||||
5: "Too Few Parameters Error: The specified command requires more parameters in order to be executed.",
|
||||
6: "Too Many Parameters Error: There were too many parameters given for the specified command.",
|
||||
7: "Invalid Parameter Error: A parameter given exceeds the valid range. Please see the command description for valid ranges of the parameters.",
|
||||
8: "Wrong Mode Error: This error is generated if the specified command is not available in the current communication mode. For example, the SRC command is not executable in synchronous mode.",
|
||||
129: "No Sensor Present Error: This error occurs if a command was given that requires sensor feedback, but the addressed positioner has none attached.",
|
||||
140: "Sensor Disabled Error: This error occurs if a command was given that requires sensor feedback, but the sensor of the addressed positioner is disabled (see SSE command).",
|
||||
141: "Command Overridden Error: This error is only generated in the asynchronous communication mode. When the software commands a movement which is then interrupted by the Hand Control Module, an error of this type is generated.",
|
||||
142: "End Stop Reached Error: This error is generated in asynchronous mode if the target position of a closed-loop command could not be reached, because a mechanical end stop was detected. After this error the positioner will have a movement status code of 0 (stopped).",
|
||||
143: "Wrong Sensor Type Error: This error occurs if a closed-loop command does not match the sensor type that is currently configured for the addressed channel. For example, issuing a GP command while the targeted channel is configured as rotary will lead to this error.",
|
||||
144: "Could Not Find Reference Mark Error: This error is generated in asynchronous mode (see SCM) if the search for a reference mark was aborted.",
|
||||
145: "Wrong End Effector Type Error: This error occurs if a command does not match the end effector type that is currently configured for the addressed channel. For example, sending GF while the targeted channel is configured for a gripper will lead to this error.",
|
||||
146: "Movement Locked Error: This error occurs if a movement command is issued while the system is in the locked state. ",
|
||||
147: "Range Limit Reached Error: If a range limit is defined (SPL or SAL) and the positioner is about to move beyond this limit, then the positioner will stop and report this error (only in asynchronous mode, see SCM). After this error the positioner will have status code of 0 (stopped).",
|
||||
148: "Physical Position Unknown Error: A range limit is only allowed to be defined if the positioner “knows” its physical position. If this is not the case, the commands SPL and SAL will return this error code.",
|
||||
150: "Command Not Processable Error: This error is generated if a command is sent to a channel when it is in a state where the command cannot be processed. For example, to change the sensor type of a channel the addressed channel must be completely stopped. In this case send a stop command before changing the type.",
|
||||
151: "Waiting For Trigger Error: If there is at least one command queued in the command queue then you may only append more commands (if the queue is not full), but you may not issue movement commands for immediate execution. Doing so will generate this error. See section 2.4.5 “Command Queues“.",
|
||||
152: "Command Not Triggerable Error: After sending a ATC command you are required to issue a movement command that is to be triggered by the given event source. Commands that cannot be triggered will generate this error.",
|
||||
153: "Command Queue Full Error: This error is generated if you attempt to append more commands to the command queue, but the queue cannot hold anymore commands. The queue capacity may be read out with a get channel property command (GCP on p.30).",
|
||||
154: "Invalid Component Error: Indicates that a component (e.g. SCP) was selected that does not exist.",
|
||||
155: "Invalid Sub Component Error: Indicates that a sub component (e.g. SCP) was selected that does not exist.",
|
||||
156: "Invalid Property Error: Indicates that a property (e.g. SCP) was selected that does not exist.",
|
||||
157: "Permission Denied Error: This error is generated when you call a functionality which is not unlocked for the system (e.g. Low Vibration Mode).",
|
||||
}
|
||||
|
||||
|
||||
class SmaractError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SmaractCommunicationError(SmaractError):
|
||||
pass
|
||||
|
||||
|
||||
class SmaractErrorCode(SmaractError):
|
||||
def __init__(self, error_code: int, message=""):
|
||||
self.error_code = error_code
|
||||
self.error_code_message = SMARACT_ERRORS.get(error_code, "UNKNOWN ERROR")
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.error_code} / {self.error_code_message}. {self.message}"
|
@ -1,321 +0,0 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
|
||||
from ophyd_devices.smaract.smaract_controller import SmaractController
|
||||
from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractError
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SmaractSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class SmaractSignalRO(SmaractSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
@threadlocked
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class SmaractReadbackSignal(SmaractSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.get_position(self.parent.axis_Id_numeric) * self.parent.sign
|
||||
|
||||
|
||||
class SmaractSetpointSignal(SmaractSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.setpoint
|
||||
|
||||
@threadlocked
|
||||
def _socket_set(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
|
||||
if self.controller.axis_is_referenced(self.parent.axis_Id_numeric):
|
||||
self.controller.move_axis_to_absolute_position(self.parent.axis_Id_numeric, target_val)
|
||||
# parameters are axis_no,pos_mm*1e6, hold_time_sec*1e3
|
||||
else:
|
||||
raise SmaractError(f"Axis {self.parent.axis_Id_numeric} is not referenced.")
|
||||
|
||||
|
||||
class SmaractMotorIsMoving(SmaractSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
|
||||
|
||||
|
||||
class SmaractAxisReferenced(SmaractSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.parent.controller.axis_is_referenced(self.parent.axis_Id_numeric)
|
||||
|
||||
|
||||
class SmaractAxisLimits(SmaractSignalBase):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
limits_msg = self.controller.socket_put_and_receive(f"GPL{self.parent.axis_Id_numeric}")
|
||||
if limits_msg.startswith(":PL"):
|
||||
limits = [
|
||||
float(limit)
|
||||
for limit in limits_msg.strip(f":PL{self.parent.axis_Id_numeric},").split(",")
|
||||
]
|
||||
else:
|
||||
raise SmaractCommunicationError("Expected to receive message starting with :PL.")
|
||||
return limits
|
||||
|
||||
# def _socket_set(self, val):
|
||||
|
||||
|
||||
class SmaractMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(SmaractReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(SmaractSetpointSignal, signal_name="setpoint")
|
||||
|
||||
# motor_resolution = Cpt(
|
||||
# SmaractMotorResolution, signal_name="resolution", kind="config"
|
||||
# )
|
||||
|
||||
motor_is_moving = Cpt(SmaractMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
# all_axes_referenced = Cpt(
|
||||
# SmaractAxesReferenced, signal_name="all_axes_referenced", kind="config"
|
||||
# )
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2680.psi.ch",
|
||||
port=8085,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = SmaractController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 4)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.1)
|
||||
val = self.readback.read()
|
||||
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||
self._done_moving(success=success)
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
mock = False
|
||||
if not mock:
|
||||
lsmarA = SmaractMotor("A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1)
|
||||
lsmarB = SmaractMotor("B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1)
|
||||
|
||||
lsmarA.stage()
|
||||
lsmarB.stage()
|
||||
# status = leyey.move(2, wait=True)
|
||||
# status = leyey.move(2, wait=True)
|
||||
lsmarA.read()
|
||||
lsmarB.read()
|
||||
|
||||
lsmarA.get()
|
||||
lsmarB.get()
|
||||
lsmarA.describe()
|
||||
|
||||
lsmarA.unstage()
|
||||
lsmarA.controller.off()
|
||||
# status = leyey.move(10, wait=False)
|
||||
# print(lSmaract_controller)
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
lsmarA = SmaractMotor(
|
||||
"A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
|
||||
)
|
||||
lsmarB = SmaractMotor(
|
||||
"B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
|
||||
)
|
||||
lsmarA.stage()
|
||||
lsmarB.stage()
|
||||
|
||||
lsmarA.read()
|
||||
lsmarB.read()
|
||||
|
||||
lsmarA.get()
|
||||
lsmarB.get()
|
||||
lsmarA.describe()
|
||||
|
||||
lsmarA.unstage()
|
||||
lsmarA.controller.off()
|
||||
# status = leyey.move(10, wait=False)
|
||||
# print(lSmaract_controller)
|
@ -1,303 +0,0 @@
|
||||
[
|
||||
{
|
||||
"symbol": "S",
|
||||
"type_code": 1,
|
||||
"positioner_series": "SLCxxxxs",
|
||||
"comment": "linear positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR",
|
||||
"type_code": 2,
|
||||
"positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s, SR7012s, SR4513s, SR5018s",
|
||||
"comment": "rotary positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SP",
|
||||
"type_code": 5,
|
||||
"positioner_series": "SLCxxxxrs",
|
||||
"comment": "linear positioner with nano sensor, large actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SC",
|
||||
"type_code": 6,
|
||||
"positioner_series": "SLCxxxxsc",
|
||||
"comment": "linear positioner with nano sensor, distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SR20",
|
||||
"type_code": 8,
|
||||
"positioner_series": "SR2013s, SR1612s",
|
||||
"comment": "rotary positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "M",
|
||||
"type_code": 9,
|
||||
"positioner_series": "SLCxxxxm",
|
||||
"comment": "linear positioner with micro sensor",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GD",
|
||||
"type_code": 11,
|
||||
"positioner_series": "SGO60.5m",
|
||||
"comment": "goniometer with micro sensor (60.5mm radius)",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GE",
|
||||
"type_code": 12,
|
||||
"positioner_series": "SGO77.5m",
|
||||
"comment": "goniometer with micro sensor (77.5mm radius)",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GF",
|
||||
"type_code": 14,
|
||||
"positioner_series": "SR1209m",
|
||||
"comment": "rotary positioner with micro sensor",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G605S",
|
||||
"type_code": 16,
|
||||
"positioner_series": "SGO60.5s",
|
||||
"comment": "goniometer with nano sensor (60.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G775S",
|
||||
"type_code": 17,
|
||||
"positioner_series": "SGO77.5s",
|
||||
"comment": "goniometer with nano sensor (77.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SC500",
|
||||
"type_code": 18,
|
||||
"positioner_series": "SLLxxsc",
|
||||
"comment": "linear positioner with nano sensor, distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "G955S",
|
||||
"type_code": 19,
|
||||
"positioner_series": "SGO95.5s",
|
||||
"comment": "goniometer with nano sensor (95.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR77",
|
||||
"type_code": 20,
|
||||
"positioner_series": "SR77xxs",
|
||||
"comment": "rotary positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SD",
|
||||
"type_code": 21,
|
||||
"positioner_series": "SLCxxxxds, SLLxxs",
|
||||
"comment": "like S, but with extended scanning Range",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "R20ME",
|
||||
"type_code": 22,
|
||||
"positioner_series": "SR2013sx, SR1410sx",
|
||||
"comment": "rotary positioner with MicroE sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR2",
|
||||
"type_code": 23,
|
||||
"positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s",
|
||||
"comment": "like SR, for high applied masses",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SCD",
|
||||
"type_code": 24,
|
||||
"positioner_series": "SLCxxxxdsc",
|
||||
"comment": "like SP, but with distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SRC",
|
||||
"type_code": 25,
|
||||
"positioner_series": "SR7021sc",
|
||||
"comment": "like SR, but with distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SR36M",
|
||||
"type_code": 26,
|
||||
"positioner_series": "SR3610m",
|
||||
"comment": "rotary positioner, no end stops",
|
||||
"reference_type": "none"
|
||||
},
|
||||
{
|
||||
"symbol": "SR36ME",
|
||||
"type_code": 27,
|
||||
"positioner_series": "SR3610m",
|
||||
"comment": "rotary positioner with end stops",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "SR50M",
|
||||
"type_code": 28,
|
||||
"positioner_series": "SR5018m",
|
||||
"comment": "rotary positioner, no end stops",
|
||||
"reference_type": "none"
|
||||
},
|
||||
{
|
||||
"symbol": "SR50ME",
|
||||
"type_code": 29,
|
||||
"positioner_series": "SR5018m",
|
||||
"comment": "rotary positioner with end stops",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G1045S",
|
||||
"type_code": 30,
|
||||
"positioner_series": "SGO104.5s",
|
||||
"comment": "goniometer with nano sensor (104.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G1395S",
|
||||
"type_code": 31,
|
||||
"positioner_series": "SGO139.5s",
|
||||
"comment": "goniometer with nano sensor (139.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "MD",
|
||||
"type_code": 32,
|
||||
"positioner_series": "SLCxxxxdme",
|
||||
"comment": "like M, but with large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G935M",
|
||||
"type_code": 33,
|
||||
"positioner_series": "SGO93.5me",
|
||||
"comment": "goniometer with micro sensor (93.5mm radius)",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "SHL20",
|
||||
"type_code": 34,
|
||||
"positioner_series": "SHL-20",
|
||||
"comment": "high load vertical positioner",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SCT",
|
||||
"type_code": 35,
|
||||
"positioner_series": "SLCxxxxscu",
|
||||
"comment": "like SCD, but with even larger actuator",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SR77T",
|
||||
"type_code": 36,
|
||||
"positioner_series": "SR7021s",
|
||||
"comment": "like SR77, but with larger actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR120",
|
||||
"type_code": 37,
|
||||
"positioner_series": "SR120xxs",
|
||||
"comment": "large rotary positioner",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LC",
|
||||
"type_code": 38,
|
||||
"positioner_series": "SLCxxxxl",
|
||||
"comment": "linear positioner with improved micro sensor",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "LR",
|
||||
"type_code": 39,
|
||||
"positioner_series": "SRxxxxl",
|
||||
"comment": "rotary positioner with improved micro sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LCD",
|
||||
"type_code": 40,
|
||||
"positioner_series": "SLCxxxxdl",
|
||||
"comment": "like LC, but with large actuator",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "L",
|
||||
"type_code": 41,
|
||||
"positioner_series": "SLCxxxxl",
|
||||
"comment": "linear positioner with improved micro sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LD",
|
||||
"type_code": 42,
|
||||
"positioner_series": "SLCxxxxdl",
|
||||
"comment": "like L, but with large actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LE",
|
||||
"type_code": 43,
|
||||
"positioner_series": "SLCxxxxl",
|
||||
"comment": "Linear positioner with improved micro sensor",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "LED",
|
||||
"type_code": 44,
|
||||
"positioner_series": "SLCxxxxdl",
|
||||
"comment": "Like L, but with large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GDD",
|
||||
"type_code": 45,
|
||||
"positioner_series": "SGO60.5md",
|
||||
"comment": "goniometer with micro sensor (60.5mm radius), large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GED",
|
||||
"type_code": 46,
|
||||
"positioner_series": "SGO77.5md",
|
||||
"comment": "goniometer with micro sensor (77.5mm radius), large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G935S",
|
||||
"type_code": 47,
|
||||
"positioner_series": "SGO96.5s",
|
||||
"comment": "goniometer with nano sensor (93.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G605DS",
|
||||
"type_code": 48,
|
||||
"positioner_series": "SGO60.5ds",
|
||||
"comment": "goniometer with nano sensor (60.5mm radius), large actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G775DS",
|
||||
"type_code": 49,
|
||||
"positioner_series": "SGO77.5ds",
|
||||
"comment": "goniometer with nano sensor (77.5mm radius), large actuator",
|
||||
"reference_type": "mark"
|
||||
}
|
||||
]
|
@ -234,99 +234,3 @@ class MockPV:
|
||||
use_monitor=use_monitor,
|
||||
)
|
||||
return data["value"] if data is not None else None
|
||||
|
||||
|
||||
class DeviceMock:
|
||||
"""Device Mock. Used for testing in combination with the DeviceManagerMock
|
||||
|
||||
Args:
|
||||
name (str): name of the device
|
||||
value (float, optional): initial value of the device. Defaults to 0.0.
|
||||
Returns:
|
||||
DeviceMock: DeviceMock object
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, value: float = 0.0):
|
||||
self.name = name
|
||||
self.read_buffer = value
|
||||
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
||||
self._read_only = False
|
||||
self._enabled = True
|
||||
|
||||
def read(self):
|
||||
"""Read method for DeviceMock"""
|
||||
return {self.name: {"value": self.read_buffer}}
|
||||
|
||||
def readback(self):
|
||||
"""Readback method for DeviceMock"""
|
||||
return self.read_buffer
|
||||
|
||||
@property
|
||||
def read_only(self) -> bool:
|
||||
"""read only property"""
|
||||
return self._read_only
|
||||
|
||||
@read_only.setter
|
||||
def read_only(self, val: bool):
|
||||
"""read only setter"""
|
||||
self._read_only = val
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""enabled property"""
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, val: bool):
|
||||
"""enabled setter"""
|
||||
self._enabled = val
|
||||
|
||||
@property
|
||||
def user_parameter(self):
|
||||
"""user_parameter property"""
|
||||
return self._config["userParameter"]
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
"""obj property"""
|
||||
return self
|
||||
|
||||
|
||||
class DMMock:
|
||||
"""Mock for DeviceManager
|
||||
|
||||
The mocked DeviceManager creates a device containert and a connector.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.devices = DeviceContainer()
|
||||
self.connector = ConnectorMock()
|
||||
|
||||
def add_device(self, name: str, value: float = 0.0):
|
||||
"""Add device to the DeviceManagerMock"""
|
||||
self.devices[name] = DeviceMock(name, value)
|
||||
|
||||
|
||||
# #TODO check what is the difference to SynSignal!
|
||||
# class MockSignal(Signal):
|
||||
# """Can mock an OphydSignal"""
|
||||
# def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs):
|
||||
# self.read_pv = read_pv
|
||||
# self._string = bool(string)
|
||||
# super().__init__(name=name, parent=parent, **kwargs)
|
||||
# self._waited_for_connection = False
|
||||
# self._subscriptions = []
|
||||
|
||||
# def wait_for_connection(self):
|
||||
# self._waited_for_connection = True
|
||||
|
||||
# def subscribe(self, method, event_type, **kw):
|
||||
# self._subscriptions.append((method, event_type, kw))
|
||||
|
||||
# def describe_configuration(self):
|
||||
# return {self.name + "_conf": {"source": "SIM:test"}}
|
||||
|
||||
# def read_configuration(self):
|
||||
# return {self.name + "_conf": {"value": 0}}
|
@ -1,6 +1,5 @@
|
||||
import abc
|
||||
import functools
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
import typing
|
||||
|
@ -13,7 +13,7 @@ classifiers = [
|
||||
"Topic :: Scientific/Engineering",
|
||||
]
|
||||
dependencies = [
|
||||
"ophyd~=1.9",
|
||||
"ophyd ~= 1.9, <= 1.9.5",
|
||||
"typeguard ~= 4.0",
|
||||
"prettytable ~= 3.9",
|
||||
"bec_lib ~= 2.0",
|
||||
|
@ -1,298 +0,0 @@
|
||||
# pylint: skip-file
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from ophyd_devices.epics.devices.delay_generator_csaxs import DDGSetup
|
||||
from ophyd_devices.epics.devices.psi_delay_generator_base import TriggerSource
|
||||
|
||||
|
||||
def patch_dual_pvs(device):
|
||||
for walk in device.walk_signals():
|
||||
if not hasattr(walk.item, "_read_pv"):
|
||||
continue
|
||||
if not hasattr(walk.item, "_write_pv"):
|
||||
continue
|
||||
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||
walk.item._read_pv = walk.item._write_pv
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_DDGSetup():
|
||||
mock_ddg = mock.MagicMock()
|
||||
yield DDGSetup(parent=mock_ddg)
|
||||
|
||||
|
||||
# Fixture for scaninfo
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
{
|
||||
"scan_id": "1234",
|
||||
"scan_type": "step",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"exp_time": 0.1,
|
||||
"readout_time": 0.1,
|
||||
},
|
||||
{
|
||||
"scan_id": "1234",
|
||||
"scan_type": "step",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 5,
|
||||
"exp_time": 0.01,
|
||||
"readout_time": 0,
|
||||
},
|
||||
{
|
||||
"scan_id": "1234",
|
||||
"scan_type": "fly",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"exp_time": 1,
|
||||
"readout_time": 0.2,
|
||||
},
|
||||
{
|
||||
"scan_id": "1234",
|
||||
"scan_type": "fly",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 5,
|
||||
"exp_time": 0.1,
|
||||
"readout_time": 0.4,
|
||||
},
|
||||
]
|
||||
)
|
||||
def scaninfo(request):
|
||||
return request.param
|
||||
|
||||
|
||||
# Fixture for DDG config default values
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
{
|
||||
"delay_burst": 0.0,
|
||||
"delta_width": 0.0,
|
||||
"additional_triggers": 0,
|
||||
"polarity": [0, 0, 0, 0, 0],
|
||||
"amplitude": 0.0,
|
||||
"offset": 0.0,
|
||||
"thres_trig_level": 0.0,
|
||||
},
|
||||
{
|
||||
"delay_burst": 0.1,
|
||||
"delta_width": 0.1,
|
||||
"additional_triggers": 1,
|
||||
"polarity": [0, 0, 1, 0, 0],
|
||||
"amplitude": 5,
|
||||
"offset": 0.0,
|
||||
"thres_trig_level": 2.5,
|
||||
},
|
||||
]
|
||||
)
|
||||
def ddg_config_defaults(request):
|
||||
return request.param
|
||||
|
||||
|
||||
# Fixture for DDG config scan values
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
{
|
||||
"fixed_ttl_width": [0, 0, 0, 0, 0],
|
||||
"trigger_width": None,
|
||||
"set_high_on_exposure": False,
|
||||
"set_high_on_stage": False,
|
||||
"set_trigger_source": "SINGLE_SHOT",
|
||||
"premove_trigger": False,
|
||||
},
|
||||
{
|
||||
"fixed_ttl_width": [0, 0, 0, 0, 0],
|
||||
"trigger_width": 0.1,
|
||||
"set_high_on_exposure": True,
|
||||
"set_high_on_stage": False,
|
||||
"set_trigger_source": "SINGLE_SHOT",
|
||||
"premove_trigger": True,
|
||||
},
|
||||
{
|
||||
"fixed_ttl_width": [0, 0, 0, 0, 0],
|
||||
"trigger_width": 0.1,
|
||||
"set_high_on_exposure": False,
|
||||
"set_high_on_stage": False,
|
||||
"set_trigger_source": "EXT_RISING_EDGE",
|
||||
"premove_trigger": False,
|
||||
},
|
||||
]
|
||||
)
|
||||
def ddg_config_scan(request):
|
||||
return request.param
|
||||
|
||||
|
||||
# Fixture for delay pairs
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
{"all_channels": ["channelAB", "channelCD"], "all_delay_pairs": ["AB", "CD"]},
|
||||
{"all_channels": [], "all_delay_pairs": []},
|
||||
{"all_channels": ["channelT0", "channelAB", "channelCD"], "all_delay_pairs": ["AB", "CD"]},
|
||||
]
|
||||
)
|
||||
def channel_pairs(request):
|
||||
return request.param
|
||||
|
||||
|
||||
def test_check_scan_id(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan):
|
||||
"""Test the check_scan_id method."""
|
||||
# Set first attributes of parent class
|
||||
for k, v in scaninfo.items():
|
||||
setattr(mock_DDGSetup.parent.scaninfo, k, v)
|
||||
for k, v in ddg_config_defaults.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
for k, v in ddg_config_scan.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
# Call the function you want to test
|
||||
mock_DDGSetup.check_scan_id()
|
||||
mock_DDGSetup.parent.scaninfo.load_scan_metadata.assert_called_once()
|
||||
|
||||
|
||||
def test_on_pre_scan(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan):
|
||||
"""Test the check_scan_id method."""
|
||||
# Set first attributes of parent class
|
||||
for k, v in scaninfo.items():
|
||||
setattr(mock_DDGSetup.parent.scaninfo, k, v)
|
||||
for k, v in ddg_config_defaults.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
for k, v in ddg_config_scan.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
# Call the function you want to test
|
||||
mock_DDGSetup.on_pre_scan()
|
||||
if ddg_config_scan["premove_trigger"]:
|
||||
mock_DDGSetup.parent.trigger_shot.put.assert_called_once_with(1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["SINGLE_SHOT", "EXT_RISING_EDGE"])
|
||||
def test_on_trigger(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan, source):
|
||||
"""Test the on_trigger method."""
|
||||
# Set first attributes of parent class
|
||||
for k, v in scaninfo.items():
|
||||
setattr(mock_DDGSetup.parent.scaninfo, k, v)
|
||||
for k, v in ddg_config_defaults.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
for k, v in ddg_config_scan.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
# Call the function you want to test
|
||||
mock_DDGSetup.parent.source.name = "source"
|
||||
mock_DDGSetup.parent.source.read.return_value = {
|
||||
mock_DDGSetup.parent.source.name: {"value": getattr(TriggerSource, source)}
|
||||
}
|
||||
mock_DDGSetup.on_trigger()
|
||||
if source == "SINGLE_SHOT":
|
||||
mock_DDGSetup.parent.trigger_shot.put.assert_called_once_with(1)
|
||||
|
||||
|
||||
def test_initialize_default_parameter(
|
||||
mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan, channel_pairs
|
||||
):
|
||||
"""Test the initialize_default_parameter method."""
|
||||
# Set first attributes of parent class
|
||||
for k, v in scaninfo.items():
|
||||
setattr(mock_DDGSetup.parent.scaninfo, k, v)
|
||||
for k, v in ddg_config_defaults.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
for k, v in ddg_config_scan.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
# Call the function you want to test
|
||||
mock_DDGSetup.parent.all_channels = channel_pairs["all_channels"]
|
||||
mock_DDGSetup.parent.all_delay_pairs = channel_pairs["all_delay_pairs"]
|
||||
calls = []
|
||||
calls.extend(
|
||||
[
|
||||
mock.call("polarity", ddg_config_defaults["polarity"][ii], [channel])
|
||||
for ii, channel in enumerate(channel_pairs["all_channels"])
|
||||
]
|
||||
)
|
||||
calls.extend([mock.call("amplitude", ddg_config_defaults["amplitude"])])
|
||||
calls.extend([mock.call("offset", ddg_config_defaults["offset"])])
|
||||
calls.extend(
|
||||
[
|
||||
mock.call(
|
||||
"reference", 0, [f"channel{pair}.ch1" for pair in channel_pairs["all_delay_pairs"]]
|
||||
)
|
||||
]
|
||||
)
|
||||
calls.extend(
|
||||
[
|
||||
mock.call(
|
||||
"reference", 0, [f"channel{pair}.ch2" for pair in channel_pairs["all_delay_pairs"]]
|
||||
)
|
||||
]
|
||||
)
|
||||
mock_DDGSetup.initialize_default_parameter()
|
||||
mock_DDGSetup.parent.set_channels.assert_has_calls(calls)
|
||||
|
||||
|
||||
def test_prepare_ddg(mock_DDGSetup, scaninfo, ddg_config_defaults, ddg_config_scan, channel_pairs):
|
||||
"""Test the prepare_ddg method."""
|
||||
# Set first attributes of parent class
|
||||
for k, v in scaninfo.items():
|
||||
setattr(mock_DDGSetup.parent.scaninfo, k, v)
|
||||
for k, v in ddg_config_defaults.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
for k, v in ddg_config_scan.items():
|
||||
getattr(mock_DDGSetup.parent, k).get.return_value = v
|
||||
# Call the function you want to test
|
||||
mock_DDGSetup.parent.all_channels = channel_pairs["all_channels"]
|
||||
mock_DDGSetup.parent.all_delay_pairs = channel_pairs["all_delay_pairs"]
|
||||
|
||||
mock_DDGSetup.prepare_ddg()
|
||||
mock_DDGSetup.parent.set_trigger.assert_called_once_with(
|
||||
getattr(TriggerSource, ddg_config_scan["set_trigger_source"])
|
||||
)
|
||||
if scaninfo["scan_type"] == "step":
|
||||
if ddg_config_scan["set_high_on_exposure"]:
|
||||
num_burst_cycle = 1 + ddg_config_defaults["additional_triggers"]
|
||||
exp_time = ddg_config_defaults["delta_width"] + scaninfo["frames_per_trigger"] * (
|
||||
scaninfo["exp_time"] + scaninfo["readout_time"]
|
||||
)
|
||||
total_exposure = exp_time
|
||||
delay_burst = ddg_config_defaults["delay_burst"]
|
||||
else:
|
||||
exp_time = ddg_config_defaults["delta_width"] + scaninfo["exp_time"]
|
||||
total_exposure = exp_time + scaninfo["readout_time"]
|
||||
delay_burst = ddg_config_defaults["delay_burst"]
|
||||
num_burst_cycle = (
|
||||
scaninfo["frames_per_trigger"] + ddg_config_defaults["additional_triggers"]
|
||||
)
|
||||
elif scaninfo["scan_type"] == "fly":
|
||||
if ddg_config_scan["set_high_on_exposure"]:
|
||||
num_burst_cycle = 1 + ddg_config_defaults["additional_triggers"]
|
||||
exp_time = (
|
||||
ddg_config_defaults["delta_width"]
|
||||
+ scaninfo["num_points"] * scaninfo["exp_time"]
|
||||
+ (scaninfo["num_points"] - 1) * scaninfo["readout_time"]
|
||||
)
|
||||
total_exposure = exp_time
|
||||
delay_burst = ddg_config_defaults["delay_burst"]
|
||||
else:
|
||||
exp_time = ddg_config_defaults["delta_width"] + scaninfo["exp_time"]
|
||||
total_exposure = exp_time + scaninfo["readout_time"]
|
||||
delay_burst = ddg_config_defaults["delay_burst"]
|
||||
num_burst_cycle = scaninfo["num_points"] + ddg_config_defaults["additional_triggers"]
|
||||
|
||||
# mock_DDGSetup.parent.burst_enable.assert_called_once_with(
|
||||
# mock.call(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||
# )
|
||||
mock_DDGSetup.parent.burst_enable.assert_called_once_with(
|
||||
num_burst_cycle, delay_burst, total_exposure, config="first"
|
||||
)
|
||||
if not ddg_config_scan["trigger_width"]:
|
||||
call = mock.call("width", exp_time)
|
||||
assert call in mock_DDGSetup.parent.set_channels.mock_calls
|
||||
else:
|
||||
call = mock.call("width", ddg_config_scan["trigger_width"])
|
||||
assert call in mock_DDGSetup.parent.set_channels.mock_calls
|
||||
if ddg_config_scan["set_high_on_exposure"]:
|
||||
calls = [
|
||||
mock.call("width", value, channels=[channel])
|
||||
for value, channel in zip(
|
||||
ddg_config_scan["fixed_ttl_width"], channel_pairs["all_channels"]
|
||||
)
|
||||
if value != 0
|
||||
]
|
||||
if calls:
|
||||
assert all(calls in mock_DDGSetup.parent.set_channels.mock_calls)
|
@ -1,9 +1,9 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
|
||||
from ophyd_devices.utils.dynamic_pseudo import ComputedSignal
|
||||
from tests.utils import DMMock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -1,455 +0,0 @@
|
||||
# pylint: skip-file
|
||||
import threading
|
||||
from unittest import mock
|
||||
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import MessageEndpoints, messages
|
||||
|
||||
from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS
|
||||
from tests.utils import DMMock, MockPV
|
||||
|
||||
|
||||
def patch_dual_pvs(device):
|
||||
for walk in device.walk_signals():
|
||||
if not hasattr(walk.item, "_read_pv"):
|
||||
continue
|
||||
if not hasattr(walk.item, "_write_pv"):
|
||||
continue
|
||||
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||
walk.item._read_pv = walk.item._write_pv
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "eiger"
|
||||
prefix = "X12SA-ES-EIGER9M:"
|
||||
sim_mode = False
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"),
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
),
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(Eiger9McSAXS, "_init"):
|
||||
det = Eiger9McSAXS(
|
||||
name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode
|
||||
)
|
||||
patch_dual_pvs(det)
|
||||
yield det
|
||||
|
||||
|
||||
def test_init():
|
||||
"""Test the _init function:"""
|
||||
name = "eiger"
|
||||
prefix = "X12SA-ES-EIGER9M:"
|
||||
sim_mode = False
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"),
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
),
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
with (
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9MSetup.initialize_default_parameter"
|
||||
) as mock_default,
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9MSetup.initialize_detector"
|
||||
) as mock_init_det,
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9MSetup.initialize_detector_backend"
|
||||
) as mock_init_backend,
|
||||
):
|
||||
Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode)
|
||||
mock_default.assert_called_once()
|
||||
mock_init_det.assert_called_once()
|
||||
mock_init_backend.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_source, detector_state, expected_exception", [(2, 1, True), (2, 0, False)]
|
||||
)
|
||||
def test_initialize_detector(mock_det, trigger_source, detector_state, expected_exception):
|
||||
"""Test the _init function:
|
||||
|
||||
This includes testing the functions:
|
||||
- _init_detector
|
||||
- _stop_det
|
||||
- _set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
|
||||
"""
|
||||
mock_det.cam.detector_state._read_pv.mock_data = detector_state
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.initialize_detector()
|
||||
else:
|
||||
mock_det.custom_prepare.initialize_detector() # call the method you want to test
|
||||
assert mock_det.cam.acquire.get() == 0
|
||||
assert mock_det.cam.detector_state.get() == detector_state
|
||||
assert mock_det.cam.trigger_mode.get() == trigger_source
|
||||
|
||||
|
||||
def test_trigger(mock_det):
|
||||
"""Test the trigger function:
|
||||
Validate that trigger calls the custom_prepare.on_trigger() function
|
||||
"""
|
||||
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
|
||||
mock_det.trigger()
|
||||
mock_on_trigger.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
|
||||
)
|
||||
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||
if readout_time is None:
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
else:
|
||||
mock_det.scaninfo.readout_time = readout_time
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"eacc, exp_url, daq_status, daq_cfg, expected_exception",
|
||||
[
|
||||
("e12345", "http://xbl-daq-29:5000", {"state": "READY"}, {"writer_user_id": 12543}, False),
|
||||
("e12345", "http://xbl-daq-29:5000", {"state": "READY"}, {"writer_user_id": 15421}, False),
|
||||
("e12345", "http://xbl-daq-29:5000", {"state": "BUSY"}, {"writer_user_id": 15421}, True),
|
||||
("e12345", "http://xbl-daq-29:5000", {"state": "READY"}, {"writer_ud": 12345}, True),
|
||||
],
|
||||
)
|
||||
def test_initialize_detector_backend(
|
||||
mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception
|
||||
):
|
||||
"""Test self.custom_prepare.initialize_detector_backend (std daq in this case)
|
||||
|
||||
This includes testing the functions:
|
||||
|
||||
- _update_service_config
|
||||
|
||||
Validation upon checking set values in mocked std_daq instance
|
||||
"""
|
||||
with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq:
|
||||
instance = mock_std_daq.return_value
|
||||
instance.stop_writer.return_value = None
|
||||
instance.get_status.return_value = daq_status
|
||||
instance.get_config.return_value = daq_cfg
|
||||
mock_det.scaninfo.username = eacc
|
||||
# scaninfo.username.return_value = eacc
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.initialize_detector_backend()
|
||||
else:
|
||||
mock_det.custom_prepare.initialize_detector_backend()
|
||||
|
||||
instance.stop_writer.assert_called_once()
|
||||
instance.get_status.assert_called()
|
||||
instance.set_config.assert_called_once_with(daq_cfg)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception",
|
||||
[
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
},
|
||||
{"state": "READY"},
|
||||
{"writer_user_id": 12543},
|
||||
5,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
},
|
||||
{"state": "BUSY"},
|
||||
{"writer_user_id": 15421},
|
||||
5,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 18.4,
|
||||
},
|
||||
{"state": "READY"},
|
||||
{"writer_user_id": 12345},
|
||||
4,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stage(
|
||||
mock_det, scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception
|
||||
):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_std_daq.stop_writer.return_value = None
|
||||
mock_std_daq.get_status.return_value = daq_status
|
||||
mock_std_daq.get_config.return_value = daq_cfg
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||
# TODO consider putting energy as variable in scaninfo
|
||||
mock_det.device_manager.add_device("mokev", value=12.4)
|
||||
mock_det.cam.beam_energy.put(scaninfo["mokev"])
|
||||
mock_det.stopped = stopped
|
||||
mock_det.cam.detector_state._read_pv.mock_data = detector_state
|
||||
with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw:
|
||||
mock_det.filepath = scaninfo["filepath"]
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.stage()
|
||||
else:
|
||||
mock_det.stage()
|
||||
mock_prep_fw.assert_called_once()
|
||||
# Check _prep_det
|
||||
assert mock_det.cam.num_images.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
assert mock_det.cam.num_frames.get() == 1
|
||||
|
||||
mock_publish_file_location.assert_called_with(done=False)
|
||||
assert mock_det.cam.acquire.get() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, daq_status, expected_exception",
|
||||
[
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
},
|
||||
{"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
},
|
||||
{"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}},
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
},
|
||||
{"state": "BUSY", "acquisition": {"state": "ERROR"}},
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prepare_detector_backend(mock_det, scaninfo, daq_status, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq,
|
||||
mock.patch.object(mock_det.custom_prepare, "filepath_exists") as mock_file_path_exists,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector_backend") as mock_stop_backend,
|
||||
mock.patch.object(mock_det, "scaninfo"),
|
||||
):
|
||||
mock_std_daq.start_writer_async.return_value = None
|
||||
mock_std_daq.get_status.return_value = daq_status
|
||||
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
mock_file_path_exists.assert_called_once()
|
||||
assert mock_stop_backend.call_count == 2
|
||||
|
||||
else:
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
mock_file_path_exists.assert_called_once()
|
||||
mock_stop_backend.assert_called_once()
|
||||
|
||||
daq_writer_call = {
|
||||
"output_file": scaninfo["filepath"],
|
||||
"n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]),
|
||||
}
|
||||
mock_std_daq.start_writer_async.assert_called_with(daq_writer_call)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stopped, expected_exception", [(False, False), (True, True)])
|
||||
def test_unstage(mock_det, stopped, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_det.stopped = stopped
|
||||
if expected_exception:
|
||||
mock_det.unstage()
|
||||
assert mock_det.stopped is True
|
||||
else:
|
||||
mock_det.unstage()
|
||||
mock_finished.assert_called_once()
|
||||
mock_publish_file_location.assert_called_with(done=True, successful=True)
|
||||
assert mock_det.stopped is False
|
||||
|
||||
|
||||
def test_stop_detector_backend(mock_det):
|
||||
with mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq:
|
||||
mock_std_daq.stop_writer.return_value = None
|
||||
mock_det.std_client = mock_std_daq
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
mock_std_daq.stop_writer.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
({"filepath": "test.h5", "successful": True, "done": False, "scan_id": "123"}),
|
||||
({"filepath": "test.h5", "successful": False, "done": True, "scan_id": "123"}),
|
||||
({"filepath": "test.h5", "successful": None, "done": True, "scan_id": "123"}),
|
||||
],
|
||||
)
|
||||
def test_publish_file_location(mock_det, scaninfo):
|
||||
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
|
||||
mock_det.filepath = scaninfo["filepath"]
|
||||
mock_det.custom_prepare.publish_file_location(
|
||||
done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
)
|
||||
if scaninfo["successful"] is None:
|
||||
msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"])
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
)
|
||||
expected_calls = [
|
||||
mock.call(
|
||||
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
mock.call(
|
||||
MessageEndpoints.file_event(mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
]
|
||||
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_detector_backend,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_detector_backend.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, scaninfo, cam_state, daq_status, expected_exception",
|
||||
[
|
||||
(
|
||||
False,
|
||||
{"num_points": 500, "frames_per_trigger": 4},
|
||||
0,
|
||||
{"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}},
|
||||
False,
|
||||
),
|
||||
(
|
||||
False,
|
||||
{"num_points": 500, "frames_per_trigger": 4},
|
||||
0,
|
||||
{"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}},
|
||||
True,
|
||||
),
|
||||
(
|
||||
False,
|
||||
{"num_points": 500, "frames_per_trigger": 1},
|
||||
1,
|
||||
{"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}},
|
||||
True,
|
||||
),
|
||||
(
|
||||
False,
|
||||
{"num_points": 500, "frames_per_trigger": 1},
|
||||
0,
|
||||
{"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}},
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "std_client") as mock_std_daq,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector_backend") as mock_stop_backend,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
):
|
||||
mock_std_daq.get_status.return_value = daq_status
|
||||
mock_det.cam.acquire._read_pv.mock_state = cam_state
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.finished()
|
||||
assert mock_det.stopped is stopped
|
||||
else:
|
||||
mock_det.custom_prepare.finished()
|
||||
if stopped:
|
||||
assert mock_det.stopped is stopped
|
||||
|
||||
mock_stop_backend.assert_called()
|
||||
mock_stop_det.assert_called_once()
|
@ -1,312 +0,0 @@
|
||||
# pylint: skip-file
|
||||
import os
|
||||
import threading
|
||||
from unittest import mock
|
||||
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import MessageEndpoints, messages
|
||||
|
||||
from ophyd_devices.epics.devices.falcon_csaxs import FalconcSAXS, FalconTimeoutError
|
||||
from tests.utils import DMMock, MockPV
|
||||
|
||||
|
||||
def patch_dual_pvs(device):
|
||||
for walk in device.walk_signals():
|
||||
if not hasattr(walk.item, "_read_pv"):
|
||||
continue
|
||||
if not hasattr(walk.item, "_write_pv"):
|
||||
continue
|
||||
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||
walk.item._read_pv = walk.item._write_pv
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "falcon"
|
||||
prefix = "X12SA-SITORO:"
|
||||
sim_mode = False
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter") as filemixin,
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
) as mock_service_config,
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(FalconcSAXS, "_init"):
|
||||
det = FalconcSAXS(
|
||||
name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode
|
||||
)
|
||||
patch_dual_pvs(det)
|
||||
yield det
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_source, mapping_source, ignore_gate, pixels_per_buffer, detector_state,"
|
||||
" expected_exception",
|
||||
[(1, 1, 0, 20, 0, False), (1, 1, 0, 20, 1, True)],
|
||||
)
|
||||
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
|
||||
def test_init_detector(
|
||||
mock_det,
|
||||
trigger_source,
|
||||
mapping_source,
|
||||
ignore_gate,
|
||||
pixels_per_buffer,
|
||||
detector_state,
|
||||
expected_exception,
|
||||
):
|
||||
"""Test the _init function:
|
||||
|
||||
This includes testing the functions:
|
||||
- _init_detector
|
||||
- _stop_det
|
||||
- _set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
|
||||
"""
|
||||
mock_det.value_pixel_per_buffer = pixels_per_buffer
|
||||
mock_det.state._read_pv.mock_data = detector_state
|
||||
if expected_exception:
|
||||
with pytest.raises(FalconTimeoutError):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.initialize_detector()
|
||||
else:
|
||||
mock_det.custom_prepare.initialize_detector()
|
||||
assert mock_det.state.get() == detector_state
|
||||
assert mock_det.collect_mode.get() == mapping_source
|
||||
assert mock_det.pixel_advance_mode.get() == trigger_source
|
||||
assert mock_det.ignore_gate.get() == ignore_gate
|
||||
|
||||
assert mock_det.preset_mode.get() == 1
|
||||
assert mock_det.erase_all.get() == 1
|
||||
assert mock_det.input_logic_polarity.get() == 0
|
||||
assert mock_det.auto_pixels_per_buffer.get() == 0
|
||||
assert mock_det.pixels_per_buffer.get() == pixels_per_buffer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
|
||||
)
|
||||
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||
if readout_time is None:
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
else:
|
||||
mock_det.scaninfo.readout_time = readout_time
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
|
||||
|
||||
def test_initialize_default_parameter(mock_det):
|
||||
with mock.patch.object(
|
||||
mock_det.custom_prepare, "update_readout_time"
|
||||
) as mock_update_readout_time:
|
||||
mock_det.custom_prepare.initialize_default_parameter()
|
||||
assert mock_det.value_pixel_per_buffer == 20
|
||||
mock_update_readout_time.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"exp_time": 0.1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_stage(mock_det, scaninfo):
|
||||
"""Test the stage function:
|
||||
|
||||
This includes testing _prep_det
|
||||
"""
|
||||
with (
|
||||
mock.patch.object(mock_det, "set_trigger") as mock_set_trigger,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "prepare_detector_backend"
|
||||
) as mock_prep_data_backend,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
mock.patch.object(mock_det.custom_prepare, "arm_acquisition") as mock_arm_acquisition,
|
||||
):
|
||||
mock_det.scaninfo.exp_time = scaninfo["exp_time"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.stage()
|
||||
mock_set_trigger.assert_called_once()
|
||||
assert mock_det.preset_real.get() == scaninfo["exp_time"]
|
||||
assert mock_det.pixels_per_run.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
mock_prep_data_backend.assert_called_once()
|
||||
mock_publish_file_location.assert_called_once_with(done=False)
|
||||
mock_arm_acquisition.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
(
|
||||
{
|
||||
"filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data.h5",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data1234.h5",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prepare_data_backend(mock_det, scaninfo):
|
||||
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.scan_number = 1
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
file_path, file_name = os.path.split(scaninfo["filepath"])
|
||||
assert mock_det.hdf5.file_path.get() == file_path
|
||||
assert mock_det.hdf5.file_name.get() == file_name
|
||||
assert mock_det.hdf5.file_template.get() == "%s%s"
|
||||
assert mock_det.hdf5.num_capture.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
assert mock_det.hdf5.file_write_mode.get() == 2
|
||||
assert mock_det.hdf5.array_counter.get() == 0
|
||||
assert mock_det.hdf5.capture.get() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
({"filepath": "test.h5", "successful": True, "done": False, "scan_id": "123"}),
|
||||
({"filepath": "test.h5", "successful": False, "done": True, "scan_id": "123"}),
|
||||
({"filepath": "test.h5", "successful": None, "done": True, "scan_id": "123"}),
|
||||
],
|
||||
)
|
||||
def test_publish_file_location(mock_det, scaninfo):
|
||||
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
|
||||
mock_det.filepath = scaninfo["filepath"]
|
||||
mock_det.custom_prepare.publish_file_location(
|
||||
done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
)
|
||||
if scaninfo["successful"] is None:
|
||||
msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"])
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
)
|
||||
expected_calls = [
|
||||
mock.call(
|
||||
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
mock.call(
|
||||
MessageEndpoints.file_event(mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
]
|
||||
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("detector_state, expected_exception", [(1, False), (0, True)])
|
||||
def test_arm_acquisition(mock_det, detector_state, expected_exception):
|
||||
with mock.patch.object(mock_det, "stop") as mock_stop:
|
||||
mock_det.state._read_pv.mock_data = detector_state
|
||||
if expected_exception:
|
||||
with pytest.raises(FalconTimeoutError):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.arm_acquisition()
|
||||
mock_stop.assert_called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.arm_acquisition()
|
||||
assert mock_det.start_all.get() == 1
|
||||
|
||||
|
||||
def test_trigger(mock_det):
|
||||
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
|
||||
mock_det.trigger()
|
||||
mock_on_trigger.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stopped, expected_abort", [(False, False), (True, True)])
|
||||
def test_unstage(mock_det, stopped, expected_abort):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_det.stopped = stopped
|
||||
if expected_abort:
|
||||
mock_det.unstage()
|
||||
assert mock_det.stopped is stopped
|
||||
assert mock_publish_file_location.call_count == 0
|
||||
else:
|
||||
mock_det.unstage()
|
||||
mock_finished.assert_called_once()
|
||||
mock_publish_file_location.assert_called_with(done=True, successful=True)
|
||||
assert mock_det.stopped is stopped
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_detector_backend,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_detector_backend.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, scaninfo",
|
||||
[
|
||||
(False, {"num_points": 500, "frames_per_trigger": 1}),
|
||||
(True, {"num_points": 500, "frames_per_trigger": 1}),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, scaninfo):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_file_writer,
|
||||
):
|
||||
mock_det.stopped = stopped
|
||||
mock_det.dxp.current_pixel._read_pv.mock_data = int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
mock_det.hdf5.array_counter._read_pv.mock_data = int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.custom_prepare.finished()
|
||||
assert mock_det.stopped is stopped
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
@ -1,60 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from utils import SocketMock
|
||||
|
||||
from ophyd_devices.galil.fupr_ophyd import FuprGalilController, FuprGalilMotor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fsamroy():
|
||||
FuprGalilController._reset_controller()
|
||||
fsamroy_motor = FuprGalilMotor(
|
||||
"A", name="fsamroy", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
fsamroy_motor.controller.on()
|
||||
assert isinstance(fsamroy_motor.controller, FuprGalilController)
|
||||
yield fsamroy_motor
|
||||
fsamroy_motor.controller.off()
|
||||
fsamroy_motor.controller._reset_controller()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pos,msg_received,msg_put,sign",
|
||||
[
|
||||
(-0.5, b" -12800\n\r", [b"TPA\r", b"MG_BGA\r", b"TPA\r"], 1),
|
||||
(-0.5, b" 12800\n\r", [b"TPA\r", b"MG_BGA\r", b"TPA\r"], -1),
|
||||
],
|
||||
)
|
||||
def test_axis_get(fsamroy, pos, msg_received, msg_put, sign):
|
||||
fsamroy.sign = sign
|
||||
fsamroy.device_manager = mock.MagicMock()
|
||||
fsamroy.controller.sock.flush_buffer()
|
||||
fsamroy.controller.sock.buffer_recv = msg_received
|
||||
val = fsamroy.read()
|
||||
assert val["fsamroy"]["value"] == pos
|
||||
assert fsamroy.readback.get() == pos
|
||||
assert fsamroy.controller.sock.buffer_put == msg_put
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"target_pos,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
[b"MG axisref\r", b"PAA=0\r", b"PAA=0\r", b"BGA\r"],
|
||||
[b"1.00", b"-1", b":", b":", b":", b":", b"-1"],
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_axis_put(fsamroy, target_pos, socket_put_messages, socket_get_messages):
|
||||
fsamroy.controller.sock.flush_buffer()
|
||||
fsamroy.controller.sock.buffer_recv = socket_get_messages
|
||||
fsamroy.user_setpoint.put(target_pos)
|
||||
assert fsamroy.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
def test_drive_axis_to_limit(fsamroy):
|
||||
fsamroy.controller.sock.flush_buffer()
|
||||
with pytest.raises(NotImplementedError):
|
||||
fsamroy.controller.drive_axis_to_limit(0, "forward")
|
@ -1,149 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from utils import SocketMock
|
||||
|
||||
from ophyd_devices.galil.galil_ophyd import GalilController, GalilMotor
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def leyey():
|
||||
GalilController._reset_controller()
|
||||
leyey_motor = GalilMotor(
|
||||
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyey_motor.controller.on()
|
||||
yield leyey_motor
|
||||
leyey_motor.controller.off()
|
||||
leyey_motor.controller._reset_controller()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def leyex():
|
||||
GalilController._reset_controller()
|
||||
leyex_motor = GalilMotor(
|
||||
"A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyex_motor.controller.on()
|
||||
yield leyex_motor
|
||||
leyex_motor.controller.off()
|
||||
leyex_motor.controller._reset_controller()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pos,msg,sign", [(1, b" -12800\n\r", 1), (-1, b" -12800\n\r", -1)])
|
||||
def test_axis_get(leyey, pos, msg, sign):
|
||||
leyey.sign = sign
|
||||
leyey.controller.sock.flush_buffer()
|
||||
leyey.controller.sock.buffer_recv = msg
|
||||
val = leyey.read()
|
||||
assert val["leyey"]["value"] == pos
|
||||
assert leyey.readback.get() == pos
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"target_pos,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
[
|
||||
b"MG allaxref\r",
|
||||
b"MG_XQ0\r",
|
||||
b"naxis=7\r",
|
||||
b"ntarget=0.000\r",
|
||||
b"movereq=1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"MG_XQ0\r",
|
||||
],
|
||||
[b"1.00", b"-1", b":", b":", b":", b":", b"-1"],
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages):
|
||||
leyey.controller.sock.flush_buffer()
|
||||
leyey.controller.sock.buffer_recv = socket_get_messages
|
||||
leyey.user_setpoint.put(target_pos)
|
||||
assert leyey.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis_nr,direction,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
"forward",
|
||||
[
|
||||
b"naxis=0\r",
|
||||
b"ndir=1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FES\r",
|
||||
b"MG_BGA\r",
|
||||
b"MGbcklact[axis]\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG_XQ2\r",
|
||||
b"MG _LRA, _LFA\r",
|
||||
],
|
||||
[b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"],
|
||||
),
|
||||
(
|
||||
1,
|
||||
"reverse",
|
||||
[
|
||||
b"naxis=1\r",
|
||||
b"ndir=-1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FES\r",
|
||||
b"MG_BGB\r",
|
||||
b"MGbcklact[axis]\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG_XQ2\r",
|
||||
b"MG _LRB, _LFB\r",
|
||||
],
|
||||
[b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages):
|
||||
leyex.controller.sock.flush_buffer()
|
||||
leyex.controller.sock.buffer_recv = socket_get_messages
|
||||
leyex.controller.drive_axis_to_limit(axis_nr, direction)
|
||||
assert leyex.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis_nr,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
[
|
||||
b"naxis=0\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FRM\r",
|
||||
b"MG_BGA\r",
|
||||
b"MGbcklact[axis]\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG_XQ2\r",
|
||||
b"MG axisref[0]\r",
|
||||
],
|
||||
[b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"],
|
||||
),
|
||||
(
|
||||
1,
|
||||
[
|
||||
b"naxis=1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FRM\r",
|
||||
b"MG_BGB\r",
|
||||
b"MGbcklact[axis]\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG_XQ2\r",
|
||||
b"MG axisref[1]\r",
|
||||
],
|
||||
[b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages):
|
||||
leyex.controller.sock.flush_buffer()
|
||||
leyex.controller.sock.buffer_recv = socket_get_messages
|
||||
leyex.controller.find_reference(axis_nr)
|
||||
assert leyex.controller.sock.buffer_put == socket_put_messages
|
@ -1,172 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from utils import SocketMock
|
||||
|
||||
from ophyd_devices.galil.fgalil_ophyd import FlomniGalilController, FlomniGalilMotor
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def leyey():
|
||||
FlomniGalilController._reset_controller()
|
||||
leyey_motor = FlomniGalilMotor(
|
||||
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyey_motor.controller.on()
|
||||
yield leyey_motor
|
||||
leyey_motor.controller.off()
|
||||
leyey_motor.controller._reset_controller()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def leyex():
|
||||
FlomniGalilController._reset_controller()
|
||||
leyex_motor = FlomniGalilMotor(
|
||||
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyex_motor.controller.on()
|
||||
yield leyex_motor
|
||||
leyex_motor.controller.off()
|
||||
leyex_motor.controller._reset_controller()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pos,msg,sign", [(1, b" -12800\n\r", 1), (-1, b" -12800\n\r", -1)])
|
||||
def test_axis_get(leyey, pos, msg, sign):
|
||||
leyey.sign = sign
|
||||
leyey.controller.sock.flush_buffer()
|
||||
leyey.controller.sock.buffer_recv = msg
|
||||
val = leyey.read()
|
||||
assert val["leyey"]["value"] == pos
|
||||
assert leyey.readback.get() == pos
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"target_pos,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
[
|
||||
b"MG allaxref\r",
|
||||
b"MG_XQ0\r",
|
||||
b"naxis=7\r",
|
||||
b"ntarget=0.000\r",
|
||||
b"movereq=1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"MG_XQ0\r",
|
||||
],
|
||||
[b"1.00", b"-1", b":", b":", b":", b":", b"-1"],
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_axis_put(leyey, target_pos, socket_put_messages, socket_get_messages):
|
||||
leyey.controller.sock.flush_buffer()
|
||||
leyey.controller.sock.buffer_recv = socket_get_messages
|
||||
leyey.user_setpoint.put(target_pos)
|
||||
assert leyey.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis_nr,direction,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
"forward",
|
||||
[
|
||||
b"naxis=0\r",
|
||||
b"ndir=1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FES\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOA\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOA\r",
|
||||
b"MG _LRA, _LFA\r",
|
||||
],
|
||||
[b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.000 0.000"],
|
||||
),
|
||||
(
|
||||
1,
|
||||
"reverse",
|
||||
[
|
||||
b"naxis=1\r",
|
||||
b"ndir=-1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FES\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOB\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOB\r",
|
||||
b"MG _LRB, _LFB\r",
|
||||
],
|
||||
[b":", b":", b":", b":", b"0", b"0", b"-1", b"-1", b"0.000 1.000"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_drive_axis_to_limit(leyex, axis_nr, direction, socket_put_messages, socket_get_messages):
|
||||
leyex.controller.sock.flush_buffer()
|
||||
leyex.controller.sock.buffer_recv = socket_get_messages
|
||||
leyex.controller.drive_axis_to_limit(axis_nr, direction)
|
||||
assert leyex.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis_nr,socket_put_messages,socket_get_messages",
|
||||
[
|
||||
(
|
||||
0,
|
||||
[
|
||||
b"naxis=0\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FRM\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOA\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOA\r",
|
||||
b"MG axisref[0]\r",
|
||||
],
|
||||
[b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"],
|
||||
),
|
||||
(
|
||||
1,
|
||||
[
|
||||
b"naxis=1\r",
|
||||
b"XQ#NEWPAR\r",
|
||||
b"XQ#FRM\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOB\r",
|
||||
b"MG_XQ0\r",
|
||||
b"MG _MOB\r",
|
||||
b"MG axisref[1]\r",
|
||||
],
|
||||
[b":", b":", b":", b"0", b"0", b"-1", b"-1", b"1.00"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages):
|
||||
leyex.controller.sock.flush_buffer()
|
||||
leyex.controller.sock.buffer_recv = socket_get_messages
|
||||
leyex.controller.find_reference(axis_nr)
|
||||
assert leyex.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis_Id,socket_put_messages,socket_get_messages,triggered",
|
||||
[
|
||||
("A", [b"MG @IN[14]\r"], [b" 1.0000\n"], True),
|
||||
("B", [b"MG @IN[14]\r"], [b" 0.0000\n"], False),
|
||||
],
|
||||
)
|
||||
def test_fosaz_light_curtain_is_triggered(
|
||||
axis_Id, socket_put_messages, socket_get_messages, triggered
|
||||
):
|
||||
"""test that the light curtain is triggered"""
|
||||
fosaz = FlomniGalilMotor(
|
||||
axis_Id, name="fosaz", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
fosaz.controller.on()
|
||||
fosaz.controller.sock.flush_buffer()
|
||||
fosaz.controller.sock.buffer_recv = socket_get_messages
|
||||
assert fosaz.controller.fosaz_light_curtain_is_triggered() == triggered
|
||||
assert fosaz.controller.sock.buffer_put == socket_put_messages
|
||||
fosaz.controller.off()
|
||||
fosaz.controller._reset_controller()
|
@ -1,331 +0,0 @@
|
||||
# pylint: skip-file
|
||||
import threading
|
||||
from unittest import mock
|
||||
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import MessageEndpoints, messages
|
||||
|
||||
from ophyd_devices.epics.devices.mcs_csaxs import (
|
||||
MCScSAXS,
|
||||
MCSError,
|
||||
MCSTimeoutError,
|
||||
ReadoutMode,
|
||||
TriggerSource,
|
||||
)
|
||||
from tests.utils import DMMock, MockPV
|
||||
|
||||
|
||||
def patch_dual_pvs(device):
|
||||
for walk in device.walk_signals():
|
||||
if not hasattr(walk.item, "_read_pv"):
|
||||
continue
|
||||
if not hasattr(walk.item, "_write_pv"):
|
||||
continue
|
||||
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||
walk.item._read_pv = walk.item._write_pv
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "mcs"
|
||||
prefix = "X12SA-MCS:"
|
||||
sim_mode = False
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter") as filemixin,
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
) as mock_service_config,
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(MCScSAXS, "_init"):
|
||||
det = MCScSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode)
|
||||
patch_dual_pvs(det)
|
||||
yield det
|
||||
|
||||
|
||||
def test_init():
|
||||
"""Test the _init function:"""
|
||||
name = "eiger"
|
||||
prefix = "X12SA-ES-EIGER9M:"
|
||||
sim_mode = False
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"),
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
),
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
with (
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.mcs_csaxs.MCSSetup.initialize_default_parameter"
|
||||
) as mock_default,
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.mcs_csaxs.MCSSetup.initialize_detector"
|
||||
) as mock_init_det,
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.mcs_csaxs.MCSSetup.initialize_detector_backend"
|
||||
) as mock_init_backend,
|
||||
):
|
||||
MCScSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode)
|
||||
mock_default.assert_called_once()
|
||||
mock_init_det.assert_called_once()
|
||||
mock_init_backend.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_source, channel_advance, channel_source1, pv_channels",
|
||||
[
|
||||
(
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
{
|
||||
"user_led": 0,
|
||||
"mux_output": 5,
|
||||
"input_pol": 0,
|
||||
"output_pol": 1,
|
||||
"count_on_start": 0,
|
||||
"stop_all": 1,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_initialize_detector(
|
||||
mock_det, trigger_source, channel_advance, channel_source1, pv_channels
|
||||
):
|
||||
"""Test the _init function:
|
||||
|
||||
This includes testing the functions:
|
||||
- initialize_detector
|
||||
- stop_det
|
||||
- parent.set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
|
||||
"""
|
||||
mock_det.custom_prepare.initialize_detector() # call the method you want to test
|
||||
assert mock_det.channel_advance.get() == channel_advance
|
||||
assert mock_det.channel1_source.get() == channel_source1
|
||||
assert mock_det.user_led.get() == pv_channels["user_led"]
|
||||
assert mock_det.mux_output.get() == pv_channels["mux_output"]
|
||||
assert mock_det.input_polarity.get() == pv_channels["input_pol"]
|
||||
assert mock_det.output_polarity.get() == pv_channels["output_pol"]
|
||||
assert mock_det.count_on_start.get() == pv_channels["count_on_start"]
|
||||
assert mock_det.input_mode.get() == trigger_source
|
||||
|
||||
|
||||
def test_trigger(mock_det):
|
||||
"""Test the trigger function:
|
||||
Validate that trigger calls the custom_prepare.on_trigger() function
|
||||
"""
|
||||
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
|
||||
mock_det.trigger()
|
||||
mock_on_trigger.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, num_lines, num_points, done", [(100, 5, 500, False), (500, 5, 500, True)]
|
||||
)
|
||||
def test_progress_update(mock_det, value, num_lines, num_points, done):
|
||||
mock_det.num_lines.set(num_lines)
|
||||
mock_det.scaninfo.num_points = num_points
|
||||
calls = mock.call(sub_type="progress", value=value, max_value=num_points, done=done)
|
||||
with mock.patch.object(mock_det, "_run_subs") as mock_run_subs:
|
||||
mock_det.custom_prepare._progress_update(value=value)
|
||||
mock_run_subs.assert_called_once()
|
||||
assert mock_run_subs.call_args == calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"values, expected_nothing",
|
||||
[([[100, 120, 140], [200, 220, 240], [300, 320, 340]], False), ([100, 200, 300], True)],
|
||||
)
|
||||
def test_on_mca_data(mock_det, values, expected_nothing):
|
||||
"""Test the on_mca_data function:
|
||||
Validate that on_mca_data calls the custom_prepare.on_mca_data() function
|
||||
"""
|
||||
with mock.patch.object(mock_det.custom_prepare, "_send_data_to_bec") as mock_send_data:
|
||||
mock_object = mock.MagicMock()
|
||||
for ii, name in enumerate(mock_det.custom_prepare.mca_names):
|
||||
mock_object.attr_name = name
|
||||
mock_det.custom_prepare._on_mca_data(obj=mock_object, value=values[ii])
|
||||
if not expected_nothing and ii < (len(values) - 1):
|
||||
assert mock_det.custom_prepare.mca_data[name] == values[ii]
|
||||
|
||||
if not expected_nothing:
|
||||
mock_send_data.assert_called_once()
|
||||
assert mock_det.custom_prepare.acquisition_done is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata, mca_data",
|
||||
[
|
||||
(
|
||||
{"scan_id": 123},
|
||||
{"mca1": [100, 120, 140], "mca3": [200, 220, 240], "mca4": [300, 320, 340]},
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_send_data_to_bec(mock_det, metadata, mca_data):
|
||||
mock_det.scaninfo.scan_msg = mock.MagicMock()
|
||||
mock_det.scaninfo.scan_msg.metadata = metadata
|
||||
mock_det.scaninfo.scan_id = metadata["scan_id"]
|
||||
mock_det.custom_prepare.mca_data = mca_data
|
||||
mock_det.custom_prepare._send_data_to_bec()
|
||||
device_metadata = mock_det.scaninfo.scan_msg.metadata
|
||||
metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()})
|
||||
data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata)
|
||||
calls = mock.call(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scan_id=metadata["scan_id"], device=mock_det.name
|
||||
),
|
||||
msg={"data": data},
|
||||
expire=1800,
|
||||
)
|
||||
|
||||
assert mock_det.connector.xadd.call_args == calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, triggersource, stopped, expected_exception",
|
||||
[
|
||||
(
|
||||
{"num_points": 500, "frames_per_trigger": 1, "scan_type": "step"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{"num_points": 500, "frames_per_trigger": 1, "scan_type": "fly"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{"num_points": 5001, "frames_per_trigger": 2, "scan_type": "step"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
{"num_points": 500, "frames_per_trigger": 2, "scan_type": "random"},
|
||||
TriggerSource.MODE3,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stage(mock_det, scaninfo, triggersource, stopped, expected_exception):
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.scan_type = scaninfo["scan_type"]
|
||||
mock_det.stopped = stopped
|
||||
with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw:
|
||||
if expected_exception:
|
||||
with pytest.raises(MCSError):
|
||||
mock_det.stage()
|
||||
mock_prep_fw.assert_called_once()
|
||||
else:
|
||||
mock_det.stage()
|
||||
mock_prep_fw.assert_called_once()
|
||||
# Check set_trigger
|
||||
mock_det.input_mode.get() == triggersource
|
||||
if scaninfo["scan_type"] == "step":
|
||||
assert mock_det.num_use_all.get() == int(scaninfo["frames_per_trigger"]) * int(
|
||||
scaninfo["num_points"]
|
||||
)
|
||||
elif scaninfo["scan_type"] == "fly":
|
||||
assert mock_det.num_use_all.get() == int(scaninfo["num_points"])
|
||||
mock_det.preset_real.get() == 0
|
||||
|
||||
# # CHeck custom_prepare.arm_acquisition
|
||||
# assert mock_det.custom_prepare.counter == 0
|
||||
# assert mock_det.erase_start.get() == 1
|
||||
# mock_prep_fw.assert_called_once()
|
||||
# # Check _prep_det
|
||||
# assert mock_det.cam.num_images.get() == int(
|
||||
# scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
# )
|
||||
# assert mock_det.cam.num_frames.get() == 1
|
||||
|
||||
# mock_publish_file_location.assert_called_with(done=False)
|
||||
# assert mock_det.cam.acquire.get() == 1
|
||||
|
||||
|
||||
def test_prepare_detector_backend(mock_det):
|
||||
mock_det.custom_prepare.prepare_detector_backend()
|
||||
assert mock_det.erase_all.get() == 1
|
||||
assert mock_det.read_mode.get() == ReadoutMode.EVENT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stopped, expected_exception", [(False, False), (True, True)])
|
||||
def test_unstage(mock_det, stopped, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_det.stopped = stopped
|
||||
if expected_exception:
|
||||
mock_det.unstage()
|
||||
assert mock_det.stopped is True
|
||||
else:
|
||||
mock_det.unstage()
|
||||
mock_finished.assert_called_once()
|
||||
mock_publish_file_location.assert_called_with(done=True, successful=True)
|
||||
assert mock_det.stopped is False
|
||||
|
||||
|
||||
def test_stop_detector_backend(mock_det):
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
assert mock_det.custom_prepare.acquisition_done is True
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_detector_backend,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_detector_backend.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, acquisition_done, acquiring_state, expected_exception",
|
||||
[
|
||||
(False, True, 0, False),
|
||||
(False, False, 0, True),
|
||||
(False, True, 1, True),
|
||||
(True, True, 0, True),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, acquisition_done, acquiring_state, expected_exception):
|
||||
mock_det.custom_prepare.acquisition_done = acquisition_done
|
||||
mock_det.acquiring._read_pv.mock_data = acquiring_state
|
||||
mock_det.scaninfo.num_points = 500
|
||||
mock_det.num_lines.put(500)
|
||||
mock_det.current_channel._read_pv.mock_data = 1
|
||||
mock_det.stopped = stopped
|
||||
|
||||
if expected_exception:
|
||||
with pytest.raises(MCSTimeoutError):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.finished()
|
||||
else:
|
||||
mock_det.custom_prepare.finished()
|
||||
if stopped:
|
||||
assert mock_det.stopped is stopped
|
@ -1,141 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from ophyd_devices.npoint import NPointAxis, NPointController
|
||||
|
||||
|
||||
class SocketMock:
|
||||
def __init__(self, sock=None):
|
||||
self.buffer_put = ""
|
||||
self.buffer_recv = ""
|
||||
self.is_open = False
|
||||
if sock is None:
|
||||
self.open()
|
||||
else:
|
||||
self.sock = sock
|
||||
|
||||
def connect(self, host, port):
|
||||
print(f"connecting to {host} port {port}")
|
||||
# self.sock.create_connection((host, port))
|
||||
# self.sock.connect((host, port))
|
||||
|
||||
def _put(self, msg_bytes):
|
||||
self.buffer_put = msg_bytes
|
||||
print(self.buffer_put)
|
||||
|
||||
def _recv(self, buffer_length=1024):
|
||||
print(self.buffer_recv)
|
||||
return self.buffer_recv
|
||||
|
||||
def _initialize_socket(self):
|
||||
pass
|
||||
|
||||
def put(self, msg):
|
||||
return self._put(msg)
|
||||
|
||||
def receive(self, buffer_length=1024):
|
||||
return self._recv(buffer_length=buffer_length)
|
||||
|
||||
def open(self):
|
||||
self._initialize_socket()
|
||||
self.is_open = True
|
||||
|
||||
def close(self):
|
||||
self.sock = None
|
||||
self.is_open = False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pos,msg",
|
||||
[
|
||||
(5, b"\xa2\x18\x12\x83\x11\xcd\xcc\x00\x00U"),
|
||||
(0, b"\xa2\x18\x12\x83\x11\x00\x00\x00\x00U"),
|
||||
(-5, b"\xa2\x18\x12\x83\x1133\xff\xffU"),
|
||||
],
|
||||
)
|
||||
def test_axis_put(pos, msg):
|
||||
controller = NPointController(SocketMock())
|
||||
npointx = NPointAxis(controller, 0, "nx")
|
||||
controller.on()
|
||||
npointx.set(pos)
|
||||
assert npointx.controller.socket.buffer_put == msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pos, msg_in, msg_out",
|
||||
[
|
||||
(5.0, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x11\xcd\xcc\x00\x00U"),
|
||||
(0, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x11\x00\x00\x00\x00U"),
|
||||
(-5, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x1133\xff\xffU"),
|
||||
],
|
||||
)
|
||||
def test_axis_get_out(pos, msg_in, msg_out):
|
||||
controller = NPointController(SocketMock())
|
||||
npointx = NPointAxis(controller, 0, "nx")
|
||||
controller.on()
|
||||
npointx.controller.socket.buffer_recv = msg_out
|
||||
assert pytest.approx(npointx.get(), rel=0.01) == pos
|
||||
# assert controller.socket.buffer_put == msg_in
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis, msg_in, msg_out",
|
||||
[
|
||||
(0, b"\xa04\x13\x83\x11U", b"\xa0\x34\x13\x83\x11\xcd\xcc\x00\x00U"),
|
||||
(1, b"\xa04#\x83\x11U", b"\xa0\x34\x13\x83\x11\x00\x00\x00\x00U"),
|
||||
(2, b"\xa043\x83\x11U", b"\xa0\x34\x13\x83\x1133\xff\xffU"),
|
||||
],
|
||||
)
|
||||
def test_axis_get_in(axis, msg_in, msg_out):
|
||||
controller = NPointController(SocketMock())
|
||||
npointx = NPointAxis(controller, 0, "nx")
|
||||
controller.on()
|
||||
controller.socket.buffer_recv = msg_out
|
||||
controller._get_current_pos(axis)
|
||||
assert controller.socket.buffer_put == msg_in
|
||||
|
||||
|
||||
def test_axis_out_of_range():
|
||||
controller = NPointController(SocketMock())
|
||||
with pytest.raises(ValueError):
|
||||
npointx = NPointAxis(controller, 3, "nx")
|
||||
|
||||
|
||||
def test_get_axis_out_of_range():
|
||||
controller = NPointController(SocketMock())
|
||||
with pytest.raises(ValueError):
|
||||
controller._get_current_pos(3)
|
||||
|
||||
|
||||
def test_set_axis_out_of_range():
|
||||
controller = NPointController(SocketMock())
|
||||
with pytest.raises(ValueError):
|
||||
controller._set_target_pos(3, 5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"in_buffer, byteorder, signed, val",
|
||||
[
|
||||
(["0x0", "0x0", "0xcc", "0xcd"], "big", True, 52429),
|
||||
(["0xcd", "0xcc", "0x0", "0x0"], "little", True, 52429),
|
||||
(["cd", "cc", "00", "00"], "little", True, 52429),
|
||||
],
|
||||
)
|
||||
def test_hex_list_to_int(in_buffer, byteorder, signed, val):
|
||||
assert NPointController._hex_list_to_int(in_buffer, byteorder=byteorder, signed=signed) == val
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis, msg_in, msg_out",
|
||||
[
|
||||
(0, b"\xa0x\x10\x83\x11U", b"\xa0\x78\x13\x83\x11\x64\x00\x00\x00U"),
|
||||
(1, b"\xa0x \x83\x11U", b"\xa0\x78\x13\x83\x11\x64\x00\x00\x00U"),
|
||||
(2, b"\xa0x0\x83\x11U", b"\xa0\x78\x13\x83\x11\x64\x00\x00\x00U"),
|
||||
],
|
||||
)
|
||||
def test_get_range(axis, msg_in, msg_out):
|
||||
controller = NPointController(SocketMock())
|
||||
npointx = NPointAxis(controller, 0, "nx")
|
||||
controller.on()
|
||||
controller.socket.buffer_recv = msg_out
|
||||
val = controller._get_range(axis)
|
||||
assert controller.socket.buffer_put == msg_in and val == 100
|
@ -1,468 +0,0 @@
|
||||
# pylint: skip-file
|
||||
import os
|
||||
import threading
|
||||
from unittest import mock
|
||||
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import MessageEndpoints, messages
|
||||
|
||||
from ophyd_devices.epics.devices.pilatus_csaxs import PilatuscSAXS
|
||||
from tests.utils import DMMock, MockPV
|
||||
|
||||
|
||||
def patch_dual_pvs(device):
|
||||
for walk in device.walk_signals():
|
||||
if not hasattr(walk.item, "_read_pv"):
|
||||
continue
|
||||
if not hasattr(walk.item, "_write_pv"):
|
||||
continue
|
||||
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||
walk.item._read_pv = walk.item._write_pv
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "pilatus"
|
||||
prefix = "X12SA-ES-PILATUS300K:"
|
||||
sim_mode = False
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.epics.devices.psi_detector_base.FileWriter"),
|
||||
mock.patch(
|
||||
"ophyd_devices.epics.devices.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
),
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(PilatuscSAXS, "_init"):
|
||||
det = PilatuscSAXS(
|
||||
name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode
|
||||
)
|
||||
patch_dual_pvs(det)
|
||||
yield det
|
||||
|
||||
|
||||
@pytest.mark.parametrize("trigger_source, detector_state", [(1, 0)])
|
||||
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
|
||||
def test_init_detector(mock_det, trigger_source, detector_state):
|
||||
"""Test the _init function:
|
||||
|
||||
This includes testing the functions:
|
||||
- _init_detector
|
||||
- _stop_det
|
||||
- _set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
|
||||
"""
|
||||
mock_det.custom_prepare.initialize_detector() # call the method you want to test
|
||||
assert mock_det.cam.acquire.get() == detector_state
|
||||
assert mock_det.cam.trigger_mode.get() == trigger_source
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, stopped, expected_exception",
|
||||
[
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
},
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
},
|
||||
True,
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stage(mock_det, scaninfo, stopped, expected_exception):
|
||||
with mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location:
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||
mock_det.device_manager.add_device("mokev", value=12.4)
|
||||
mock_det.stopped = stopped
|
||||
with (
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "prepare_detector_backend"
|
||||
) as mock_data_backend,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "update_readout_time"
|
||||
) as mock_update_readout_time,
|
||||
):
|
||||
mock_det.filepath = scaninfo["filepath"]
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.stage()
|
||||
else:
|
||||
mock_det.stage()
|
||||
mock_data_backend.assert_called_once()
|
||||
mock_update_readout_time.assert_called()
|
||||
# Check _prep_det
|
||||
assert mock_det.cam.num_images.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
assert mock_det.cam.num_frames.get() == 1
|
||||
|
||||
mock_publish_file_location.assert_called_with(done=False)
|
||||
|
||||
|
||||
def test_pre_scan(mock_det):
|
||||
mock_det.custom_prepare.pre_scan()
|
||||
assert mock_det.cam.acquire.get() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
|
||||
)
|
||||
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||
if readout_time is None:
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
else:
|
||||
mock_det.scaninfo.readout_time = readout_time
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
(
|
||||
{
|
||||
"filepath": "test.h5",
|
||||
"filepath_raw": "test5_raw.h5",
|
||||
"successful": True,
|
||||
"done": False,
|
||||
"scan_id": "123",
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath": "test.h5",
|
||||
"filepath_raw": "test5_raw.h5",
|
||||
"successful": False,
|
||||
"done": True,
|
||||
"scan_id": "123",
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath": "test.h5",
|
||||
"filepath_raw": "test5_raw.h5",
|
||||
"successful": None,
|
||||
"done": True,
|
||||
"scan_id": "123",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_publish_file_location(mock_det, scaninfo):
|
||||
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
|
||||
mock_det.filepath = scaninfo["filepath"]
|
||||
mock_det.filepath_raw = scaninfo["filepath_raw"]
|
||||
mock_det.custom_prepare.publish_file_location(
|
||||
done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
)
|
||||
if scaninfo["successful"] is None:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"],
|
||||
done=scaninfo["done"],
|
||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||
)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"],
|
||||
done=scaninfo["done"],
|
||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||
successful=scaninfo["successful"],
|
||||
)
|
||||
expected_calls = [
|
||||
mock.call(
|
||||
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
mock.call(
|
||||
MessageEndpoints.file_event(mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
]
|
||||
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"requests_state, expected_exception, url_delete, url_put",
|
||||
[
|
||||
(
|
||||
True,
|
||||
False,
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/stop",
|
||||
),
|
||||
(
|
||||
False,
|
||||
False,
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/stop",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stop_detector_backend(mock_det, requests_state, expected_exception, url_delete, url_put):
|
||||
with (
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "send_requests_delete"
|
||||
) as mock_send_requests_delete,
|
||||
mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put,
|
||||
):
|
||||
instance_delete = mock_send_requests_delete.return_value
|
||||
instance_delete.ok = requests_state
|
||||
instance_put = mock_send_requests_put.return_value
|
||||
instance_put.ok = requests_state
|
||||
if expected_exception:
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
mock_send_requests_delete.assert_called_once_with(url=url_delete)
|
||||
mock_send_requests_put.assert_called_once_with(url=url_put)
|
||||
instance_delete.raise_for_status.called_once()
|
||||
instance_put.raise_for_status.called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
mock_send_requests_delete.assert_called_once_with(url=url_delete)
|
||||
mock_send_requests_put.assert_called_once_with(url=url_put)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, data_msgs, urls, requests_state, expected_exception",
|
||||
[
|
||||
(
|
||||
{
|
||||
"filepath_raw": "pilatus_2.h5",
|
||||
"eacc": "e12345",
|
||||
"scan_number": 1000,
|
||||
"scan_directory": "S00000_00999",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
|
||||
},
|
||||
[
|
||||
{
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": (
|
||||
"/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999"
|
||||
),
|
||||
}
|
||||
]
|
||||
},
|
||||
[
|
||||
"zmqWriter",
|
||||
"e12345",
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": 500,
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": "e12345",
|
||||
},
|
||||
],
|
||||
["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}],
|
||||
],
|
||||
[
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/run",
|
||||
"http://xbl-daq-34:8091/pilatus_2/wait",
|
||||
],
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath_raw": "pilatus_2.h5",
|
||||
"eacc": "e12345",
|
||||
"scan_number": 1000,
|
||||
"scan_directory": "S00000_00999",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
|
||||
},
|
||||
[
|
||||
{
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": (
|
||||
"/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999"
|
||||
),
|
||||
}
|
||||
]
|
||||
},
|
||||
[
|
||||
"zmqWriter",
|
||||
"e12345",
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": 500,
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": "e12345",
|
||||
},
|
||||
],
|
||||
["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}],
|
||||
],
|
||||
[
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/run",
|
||||
"http://xbl-daq-34:8091/pilatus_2/wait",
|
||||
],
|
||||
False, # return of res.ok is False!
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer,
|
||||
mock.patch.object(mock_det, "filewriter") as mock_filewriter,
|
||||
mock.patch.object(mock_det.custom_prepare, "create_directory") as mock_create_directory,
|
||||
mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put,
|
||||
):
|
||||
mock_det.scaninfo.scan_number = scaninfo["scan_number"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.username = scaninfo["eacc"]
|
||||
mock_filewriter.compile_full_filename.return_value = scaninfo["filepath_raw"]
|
||||
mock_filewriter.get_scan_directory.return_value = scaninfo["scan_directory"]
|
||||
instance = mock_send_requests_put.return_value
|
||||
instance.ok = requests_state
|
||||
instance.raise_for_status.side_effect = Exception
|
||||
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
instance.raise_for_status.assert_called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
|
||||
mock_close_file_writer.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
|
||||
# Assert values set on detector
|
||||
assert mock_det.cam.file_path.get() == "/dev/shm/zmq/"
|
||||
assert (
|
||||
mock_det.cam.file_name.get()
|
||||
== f"{scaninfo['eacc']}_2_{scaninfo['scan_number']:05d}"
|
||||
)
|
||||
assert mock_det.cam.auto_increment.get() == 1
|
||||
assert mock_det.cam.file_number.get() == 0
|
||||
assert mock_det.cam.file_format.get() == 0
|
||||
assert mock_det.cam.file_template.get() == "%s%s_%5.5d.cbf"
|
||||
# Remove last / from destinationPath
|
||||
mock_create_directory.assert_called_once_with(
|
||||
os.path.join(data_msgs[0]["source"][0]["destinationPath"])
|
||||
)
|
||||
assert mock_send_requests_put.call_count == 3
|
||||
|
||||
calls = [
|
||||
mock.call(url=url, data=data_msg, headers=scaninfo["headers"])
|
||||
for url, data_msg in zip(urls, data_msgs)
|
||||
]
|
||||
for call, mock_call in zip(calls, mock_send_requests_put.call_args_list):
|
||||
assert call == mock_call
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stopped, expected_exception", [(False, False), (True, True)])
|
||||
def test_unstage(mock_det, stopped, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_det.stopped = stopped
|
||||
if expected_exception:
|
||||
mock_det.unstage()
|
||||
assert mock_det.stopped is True
|
||||
else:
|
||||
mock_det.unstage()
|
||||
mock_finished.assert_called_once()
|
||||
mock_publish_file_location.assert_called_with(done=True, successful=True)
|
||||
assert mock_det.stopped is False
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer,
|
||||
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, mcs_stage_state, expected_exception",
|
||||
[
|
||||
(False, ophyd.Staged.no, False),
|
||||
(True, ophyd.Staged.no, True),
|
||||
(False, ophyd.Staged.yes, True),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, mcs_stage_state, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det, "device_manager") as mock_dm,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_friter,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
|
||||
):
|
||||
mock_dm.devices.mcs.obj._staged = mcs_stage_state
|
||||
mock_det.stopped = stopped
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.finished()
|
||||
assert mock_det.stopped is stopped
|
||||
mock_stop_file_friter.assert_called()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.finished()
|
||||
if stopped:
|
||||
assert mock_det.stopped is stopped
|
||||
|
||||
mock_stop_file_friter.assert_called()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_close_file_writer.assert_called_once()
|
@ -1,89 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from utils import SocketMock
|
||||
|
||||
from ophyd_devices.rt_lamni import RtFlomniController, RtFlomniMotor
|
||||
from ophyd_devices.rt_lamni.rt_ophyd import RtError
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def rt_flomni():
|
||||
rt_flomni = RtFlomniController(
|
||||
name="rt_flomni", socket_cls=SocketMock, socket_host="localhost", socket_port=8081
|
||||
)
|
||||
with mock.patch.object(rt_flomni, "get_device_manager"):
|
||||
with mock.patch.object(rt_flomni, "sock"):
|
||||
rtx = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rtx.name = "rtx"
|
||||
rty = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rty.name = "rty"
|
||||
rtz = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rtz.name = "rtz"
|
||||
rt_flomni.set_axis(rtx, 0)
|
||||
rt_flomni.set_axis(rty, 1)
|
||||
rt_flomni.set_axis(rtz, 2)
|
||||
yield rt_flomni
|
||||
|
||||
|
||||
def test_rt_flomni_move_to_zero(rt_flomni):
|
||||
rt_flomni.move_to_zero()
|
||||
assert rt_flomni.sock.mock_calls == [
|
||||
mock.call.put(b"pa0,0\n"),
|
||||
mock.call.put(b"pa1,0\n"),
|
||||
mock.call.put(b"pa2,0\n"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("return_value,is_running", [(b"1.00\n", False), (b"0.00\n", True)])
|
||||
def test_rt_flomni_feedback_is_running(rt_flomni, return_value, is_running):
|
||||
rt_flomni.sock.receive.return_value = return_value
|
||||
assert rt_flomni.feedback_is_running() == is_running
|
||||
assert mock.call.put(b"l2\n") in rt_flomni.sock.mock_calls
|
||||
|
||||
|
||||
def test_feedback_enable_with_reset(rt_flomni):
|
||||
|
||||
device_manager = rt_flomni.get_device_manager()
|
||||
device_manager.devices.fsamx.user_parameter.get.return_value = 0.05
|
||||
device_manager.devices.fsamx.obj.readback.get.return_value = 0.05
|
||||
|
||||
with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True):
|
||||
with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on:
|
||||
with mock.patch.object(rt_flomni, "pid_y", return_value=0.05):
|
||||
with mock.patch.object(
|
||||
rt_flomni, "slew_rate_limiters_on_target", return_value=True
|
||||
) as slew_rate_limiters_on_target:
|
||||
|
||||
rt_flomni.feedback_enable_with_reset()
|
||||
laser_tracker_on.assert_called_once()
|
||||
|
||||
|
||||
def test_move_samx_to_scan_region(rt_flomni):
|
||||
device_manager = rt_flomni.get_device_manager()
|
||||
device_manager.devices.rtx.user_parameter.get.return_value = 1
|
||||
rt_flomni.move_samx_to_scan_region(20, 2)
|
||||
assert mock.call(b"v0\n") not in rt_flomni.sock.put.mock_calls
|
||||
assert mock.call(b"v1\n") in rt_flomni.sock.put.mock_calls
|
||||
|
||||
|
||||
def test_feedback_enable_without_reset(rt_flomni):
|
||||
with mock.patch.object(rt_flomni, "set_device_enabled") as set_device_enabled:
|
||||
with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True):
|
||||
with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on:
|
||||
rt_flomni.feedback_enable_without_reset()
|
||||
laser_tracker_on.assert_called_once()
|
||||
assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls
|
||||
assert mock.call("fsamx", False) in set_device_enabled.mock_calls
|
||||
assert mock.call("fsamy", False) in set_device_enabled.mock_calls
|
||||
assert mock.call("foptx", False) in set_device_enabled.mock_calls
|
||||
assert mock.call("fopty", False) in set_device_enabled.mock_calls
|
||||
|
||||
|
||||
def test_feedback_enable_without_reset_raises(rt_flomni):
|
||||
with mock.patch.object(rt_flomni, "feedback_is_running", return_value=False):
|
||||
with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on:
|
||||
with pytest.raises(RtError):
|
||||
rt_flomni.feedback_enable_without_reset()
|
||||
laser_tracker_on.assert_called_once()
|
||||
assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls
|
@ -7,6 +7,7 @@ from unittest import mock
|
||||
import h5py
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
from ophyd import Device, Signal
|
||||
|
||||
from ophyd_devices.ophyd_base_devices.bec_protocols import (
|
||||
@ -20,7 +21,6 @@ from ophyd_devices.sim.sim import SimCamera, SimFlyer, SimMonitor, SimPositioner
|
||||
from ophyd_devices.sim.sim_frameworks import H5ImageReplayProxy, SlitProxy
|
||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal
|
||||
from ophyd_devices.utils.bec_device_base import BECDevice, BECDeviceBase
|
||||
from tests.utils import DMMock
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@ -120,15 +120,18 @@ def test_flyer__init__(flyer):
|
||||
@pytest.mark.parametrize("center", [-10, 0, 10])
|
||||
def test_monitor_readback(monitor, center):
|
||||
"""Test the readback method of SimMonitor."""
|
||||
motor_pos = 0
|
||||
monitor.sim.device_manager.add_device("samx", value=motor_pos)
|
||||
for model_name in monitor.sim.sim_get_models():
|
||||
monitor.sim.sim_select_model(model_name)
|
||||
monitor.sim.sim_params["noise_multipler"] = 10
|
||||
monitor.sim.sim_params["ref_motor"] = "samx"
|
||||
if "c" in monitor.sim.sim_params:
|
||||
monitor.sim.sim_params["c"] = center
|
||||
elif "center" in monitor.sim.sim_params:
|
||||
monitor.sim.sim_params["center"] = center
|
||||
assert isinstance(monitor.read()[monitor.name]["value"], monitor.BIT_DEPTH)
|
||||
expected_value = monitor.sim._model.eval(monitor.sim._model_params, x=0)
|
||||
expected_value = monitor.sim._model.eval(monitor.sim._model_params, x=motor_pos)
|
||||
print(expected_value, monitor.read()[monitor.name]["value"])
|
||||
tolerance = (
|
||||
monitor.sim.sim_params["noise_multipler"] + 1
|
||||
|
@ -1,217 +0,0 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from utils import SocketMock
|
||||
|
||||
from ophyd_devices.smaract import SmaractController
|
||||
from ophyd_devices.smaract.smaract_controller import SmaractCommunicationMode
|
||||
from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode
|
||||
from ophyd_devices.smaract.smaract_ophyd import SmaractMotor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller():
|
||||
SmaractController._reset_controller()
|
||||
controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123)
|
||||
controller.on()
|
||||
controller.sock.flush_buffer()
|
||||
yield controller
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lsmarA():
|
||||
SmaractController._reset_controller()
|
||||
motor_a = SmaractMotor(
|
||||
"A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
|
||||
)
|
||||
motor_a.controller.on()
|
||||
motor_a.controller.sock.flush_buffer()
|
||||
motor_a.stage()
|
||||
yield motor_a
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis,position,get_message,return_msg",
|
||||
[
|
||||
(0, 50, b":GP0\n", b":P0,50000000\n"),
|
||||
(1, 0, b":GP1\n", b":P1,0\n"),
|
||||
(0, -50, b":GP0\n", b":P0,-50000000\n"),
|
||||
(0, -50.23, b":GP0\n", b":P0,-50230000\n"),
|
||||
],
|
||||
)
|
||||
def test_get_position(controller, axis, position, get_message, return_msg):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
val = controller.get_position(axis)
|
||||
assert val == position
|
||||
assert controller.sock.buffer_put[0] == get_message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"axis,is_referenced,get_message,return_msg,exception",
|
||||
[
|
||||
(0, True, b":GPPK0\n", b":PPK0,1\n", None),
|
||||
(1, True, b":GPPK1\n", b":PPK1,1\n", None),
|
||||
(0, False, b":GPPK0\n", b":PPK0,0\n", None),
|
||||
(200, False, b":GPPK0\n", b":PPK0,0\n", ValueError),
|
||||
],
|
||||
)
|
||||
def test_axis_is_referenced(controller, axis, is_referenced, get_message, return_msg, exception):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
if exception is not None:
|
||||
with pytest.raises(exception):
|
||||
val = controller.axis_is_referenced(axis)
|
||||
else:
|
||||
val = controller.axis_is_referenced(axis)
|
||||
assert val == is_referenced
|
||||
assert controller.sock.buffer_put[0] == get_message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"return_msg,exception,raised",
|
||||
[
|
||||
(b"false\n", SmaractCommunicationError, False),
|
||||
(b":E0,1", SmaractErrorCode, True),
|
||||
(b":E,1", SmaractCommunicationError, True),
|
||||
(b":E,-1", SmaractCommunicationError, True),
|
||||
],
|
||||
)
|
||||
def test_socket_put_and_receive_raises_exception(controller, return_msg, exception, raised):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
with pytest.raises(exception):
|
||||
controller.socket_put_and_receive(b"test", raise_if_not_status=True)
|
||||
|
||||
controller.sock.flush_buffer()
|
||||
controller.sock.buffer_recv = return_msg
|
||||
|
||||
if raised:
|
||||
with pytest.raises(exception):
|
||||
controller.socket_put_and_receive(b"test")
|
||||
else:
|
||||
assert controller.socket_put_and_receive(b"test") == return_msg.split(b"\n")[0].decode()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode,get_message,return_msg", [(0, b":GCM\n", b":CM0\n"), (1, b":GCM\n", b":CM1\n")]
|
||||
)
|
||||
def test_communication_mode(controller, mode, get_message, return_msg):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
val = controller.get_communication_mode()
|
||||
assert controller.sock.buffer_put[0] == get_message
|
||||
assert val == SmaractCommunicationMode(mode)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"is_moving,get_message,return_msg",
|
||||
[
|
||||
(0, b":GS0\n", b":S0,0\n"),
|
||||
(1, b":GS0\n", b":S0,1\n"),
|
||||
(1, b":GS0\n", b":S0,2\n"),
|
||||
(0, b":GS0\n", b":S0,3\n"),
|
||||
(1, b":GS0\n", b":S0,4\n"),
|
||||
(0, b":GS0\n", b":S0,5\n"),
|
||||
(0, b":GS0\n", b":S0,6\n"),
|
||||
(1, b":GS0\n", b":S0,7\n"),
|
||||
(0, b":GS0\n", b":S0,9\n"),
|
||||
(0, [b":GS0\n", b":GS0\n"], [b":E0,0\n", b":S0,9"]),
|
||||
],
|
||||
)
|
||||
def test_axis_is_moving(controller, is_moving, get_message, return_msg):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
val = controller.is_axis_moving(0)
|
||||
assert val == is_moving
|
||||
if isinstance(controller.sock.buffer_put, list) and len(controller.sock.buffer_put) == 1:
|
||||
controller.sock.buffer_put = controller.sock.buffer_put[0]
|
||||
assert controller.sock.buffer_put == get_message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sensor_id,axis,get_msg,return_msg",
|
||||
[
|
||||
(1, 0, b":GST0\n", b":ST0,1\n"),
|
||||
(6, 0, b":GST0\n", b":ST0,6\n"),
|
||||
(6, 1, b":GST1\n", b":ST1,6\n"),
|
||||
],
|
||||
)
|
||||
def test_get_sensor_definition(controller, sensor_id, axis, get_msg, return_msg):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
sensor = controller.get_sensor_type(axis)
|
||||
assert sensor.type_code == sensor_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"move_speed,axis,get_msg,return_msg",
|
||||
[
|
||||
(50, 0, b":SCLS0,50000000\n", b":E-1,0"),
|
||||
(0, 0, b":SCLS0,0\n", b":E-1,0"),
|
||||
(20.23, 1, b":SCLS1,20230000\n", b":E-1,0"),
|
||||
],
|
||||
)
|
||||
def test_set_move_speed(controller, move_speed, axis, get_msg, return_msg):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
controller.set_closed_loop_move_speed(axis, move_speed)
|
||||
assert controller.sock.buffer_put[0] == get_msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pos,axis,hold_time,get_msg,return_msg",
|
||||
[
|
||||
(50, 0, None, b":MPA0,50000000,1000\n", b":E0,0"),
|
||||
(0, 0, 800, b":MPA0,0,800\n", b":E0,0"),
|
||||
(20.23, 1, None, b":MPA1,20230000,1000\n", b":E0,0"),
|
||||
],
|
||||
)
|
||||
def test_move_axis_to_absolute_position(controller, pos, axis, hold_time, get_msg, return_msg):
|
||||
controller.sock.buffer_recv = return_msg
|
||||
if hold_time is not None:
|
||||
controller.move_axis_to_absolute_position(axis, pos, hold_time=hold_time)
|
||||
else:
|
||||
controller.move_axis_to_absolute_position(axis, pos)
|
||||
assert controller.sock.buffer_put[0] == get_msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pos,get_msg,return_msg",
|
||||
[
|
||||
(
|
||||
50,
|
||||
[b":GPPK0\n", b":MPA0,50000000,1000\n", b":GS0\n", b":GP0\n"],
|
||||
[b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,50000000\n"],
|
||||
),
|
||||
(
|
||||
0,
|
||||
[b":GPPK0\n", b":MPA0,0,1000\n", b":GS0\n", b":GP0\n"],
|
||||
[b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,0000000\n"],
|
||||
),
|
||||
(
|
||||
20.23,
|
||||
[b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"],
|
||||
[b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,20230000\n"],
|
||||
),
|
||||
(
|
||||
20.23,
|
||||
[b":GPPK0\n", b":GPPK0\n", b":MPA0,20230000,1000\n", b":GS0\n", b":GP0\n"],
|
||||
[b":S0,0\n", b":PPK0,1\n", b":E0,0\n", b":S0,0\n", b":P0,20230000\n"],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_move_axis(lsmarA, pos, get_msg, return_msg):
|
||||
controller = lsmarA.controller
|
||||
controller.sock.buffer_recv = return_msg
|
||||
lsmarA.move(pos)
|
||||
assert controller.sock.buffer_put == get_msg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_axes,get_msg,return_msg", [(1, [b":S0\n"], [b":E0,0"])])
|
||||
def test_stop_axis(lsmarA, num_axes, get_msg, return_msg):
|
||||
controller = lsmarA.controller
|
||||
controller.sock.buffer_recv = return_msg
|
||||
controller.stop_all_axes()
|
||||
assert controller.sock.buffer_put == get_msg
|
||||
|
||||
|
||||
def test_all_axes_referenced(lsmarA):
|
||||
controller = lsmarA.controller
|
||||
with mock.patch.object(controller, "axis_is_referenced", return_value=True) as mock_is_ref:
|
||||
val = controller.all_axes_referenced()
|
||||
assert val
|
||||
mock_is_ref.assert_called_once_with(0)
|
Reference in New Issue
Block a user