Adding motor class and PEPing up

This commit is contained in:
gac-x05la
2024-08-26 16:56:58 +02:00
committed by mohacsi_i
parent 069c902be2
commit a0ae47aba6
6 changed files with 216 additions and 68 deletions

View File

@@ -69,11 +69,19 @@ class GigaFrostCameraMixin(CustomDetectorMixin):
source_port = 3000 + j
if j < 4:
extend_header_table(
udp_header_table, self.parent.macSouth.get(), self.parent.ipSouth.get(), dest_port, source_port
udp_header_table,
self.parent.macSouth.get(),
self.parent.ipSouth.get(),
dest_port,
source_port
)
else:
extend_header_table(
udp_header_table, self.parent.macNorth.get(), self.parent.ipNorth.get(), dest_port, source_port
udp_header_table,
self.parent.macNorth.get(),
self.parent.ipNorth.get(),
dest_port,
source_port
)
return udp_header_table
@@ -230,17 +238,25 @@ class GigaFrostCamera(PSIDetectorBase):
cmdWriteService = Component(EpicsSignal, "WRITE_SRV.PROC", put_complete=True, kind=Kind.omitted)
# Standard camera configs
cfgExposure = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgFramerate = Component(EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgScanId = Component(EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgCntNum = Component(EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgCorrMode = Component(EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgExposure = Component(
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgFramerate = Component(
EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgRoiX = Component(
EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgRoiY = Component(
EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgScanId = Component(
EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgCntNum = Component(
EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config)
cfgCorrMode = Component(
EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config)
# Software signals
cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True)
cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted)
cmdSoftTrigger = Component(
EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted)
cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True)
# Trigger configuration PVs
@@ -252,7 +268,11 @@ class GigaFrostCamera(PSIDetectorBase):
kind=Kind.config,
)
cfgCntEndBit = Component(
EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config
EpicsSignal,
"CNT_ENDBIT_RBV",
write_pv="CNT_ENDBIT",
put_complete=True,
kind=Kind.config
)
# Enable modes
cfgTrigEnableExt = Component(
@@ -373,10 +393,6 @@ class GigaFrostCamera(PSIDetectorBase):
name,
auto_soft_enable=False,
backend_url=const.BE999_DAFL_CLIENT,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
**kwargs,
):
# Ugly hack to pass values before on_init()
@@ -388,10 +404,6 @@ class GigaFrostCamera(PSIDetectorBase):
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
@@ -414,7 +426,7 @@ class GigaFrostCamera(PSIDetectorBase):
status = DeviceStatus(self)
sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.050
sleep(sleep_time)
logger.info(f"[GF2] Slept for: {sleep_time} seconds")
logger.info("[%s] Slept for %f seconds", self.name, sleep_time)
status.set_finished()
return status
@@ -658,13 +670,10 @@ class GigaFrostCamera(PSIDetectorBase):
mode_external = self.cfgTrigEnableExt.get()
mode_auto = self.cfgTrigEnableAuto.get()
if mode_soft and not mode_auto:
if mode_external:
return "soft+ext"
else:
return "soft"
elif mode_auto and not mode_soft and not mode_external:
return "soft+ext" if mode_external else "soft"
if mode_auto and not mode_soft and not mode_external:
return "always"
elif mode_external and not mode_soft and not mode_auto:
if mode_external and not mode_soft and not mode_auto:
return "external"
return None

View File

@@ -29,13 +29,6 @@ class GigaFrostClientMixin(CustomDetectorMixin):
This class will be called by the custom_prepare_cls attribute of the detector class.
"""
def on_init(self) -> None:
"""Initialize the camera, set channel values
on_init is automatically called during __init__ of the sub devices.
"""
return super().on_init()
def on_stage(self) -> None:
"""
Specify actions to be executed during stage in preparation for a scan.
@@ -120,6 +113,7 @@ class GigaFrostClient(PSIDetectorBase):
cam = Component(GigaFrostCamera, prefix="X02DA-CAM-GF2:", name="cam")
daq = Component(StdDaqClient, name="daq")
# pylint: disable=too-many-arguments
def __init__(
self,
prefix="",
@@ -130,31 +124,25 @@ class GigaFrostClient(PSIDetectorBase):
daq_ws_url = "ws://localhost:8080",
daq_rest_url = "http://localhost:5000",
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
**kwargs,
):
self.__class__.__dict__["cam"].kwargs['backend_url'] = backend_url
self.__class__.__dict__["cam"].kwargs['auto_soft_enable'] = auto_soft_enable
self.__class__.__dict__["daq"].kwargs['ws_url'] = daq_ws_url
self.__class__.__dict__["daq"].kwargs['rest_url'] = daq_rest_url
#self.__class__.__dict__["daq"].__class__.__dict__["config"].kwargs['rest_url'] = daq_rest_url
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
def configure(self, d: dict=None):
"""Configure the next scan with the GigaFRoST camera and standard DAQ backend
"""Configure the next scan with the GigaFRoST camera and standard DAQ backend.
It also makes some simple checks for consistent configuration, but otherwise
status feedback is missing on both sides.
Parameters
----------
@@ -163,30 +151,22 @@ class GigaFrostClient(PSIDetectorBase):
for an unlimited number of images (limited by the ringbuffer size and
backend speed). (default = 10000)
nimages : int, optional
Number of images to be taken during each scan. Set to -1 for an
unlimited number of images (limited by the ringbuffer size and
backend speed). (default = 10)
Number of images to be taken during each trigger (i.e. burst).
Maximum is 16777215 images. (default = 10)
exposure : float, optional
Exposure time [ms]. (default = 0.2)
Exposure time, max 40 ms. [ms]. (default = 0.2)
period : float, optional
Exposure period [ms], ignored in soft trigger mode. (default = 1.0)
pixel_width : int, optional
Image size in the x-direction [pixels] (default = 2016)
Image size in the x-direction, must be multiple of 48 [pixels] (default = 2016)
pixel_height : int, optional
Image size in the y-direction [pixels] (default = 2016)
Image size in the y-direction, must be multiple of 16 [pixels] (default = 2016)
scanid : int, optional
Scan identification number to be associated with the scan data
(default = 0)
Scan identification number to be associated with the scan data.
ToDo: This should be retrieved from the BEC. (default = 0)
correction_mode : int, optional
The correction to be applied to the imaging data. The following
modes are available (default = 5):
* 0: Bypass. No corrections are applied to the data.
* 1: Send correction factor A instead of pixel values
* 2: Send correction factor B instead of pixel values
* 3: Send correction factor C instead of pixel values
* 4: Invert pixel values, but do not apply any linearity correction
* 5: Apply the full linearity correction
"""
# Unstage camera (reconfiguration will anyway stop camera)
super().unstage()

View File

@@ -102,7 +102,7 @@ class StdDaqPreview(Device):
self._stop_polling = True
return super().unstage()
def stop(self, success=False):
def stop(self, *, success=False):
"""Stop a running preview"""
self.unstage()
@@ -119,7 +119,13 @@ class StdDaqPreview(Device):
try:
# pylint: disable=no-member
meta, data = self._socket.recv_multipart(flags=zmq.NOBLOCK)
r = self._socket.recv_multipart(flags=zmq.NOBLOCK)
if len(r)==2:
meta, data = r
else:
sleep(0.1)
continue
t_curr = time()
t_elapsed = t_curr - t_last
if t_elapsed > self.throttle.get():
@@ -127,7 +133,8 @@ class StdDaqPreview(Device):
if header["type"]=="uint16":
image = np.frombuffer(data, dtype=np.uint16)
if image.size != np.prod(header['shape']):
raise ValueError(f"Unexpected array size of {image.size} for header: {header}")
err = f"Unexpected array size of {image.size} for header: {header}"
raise ValueError(err)
image = image.reshape(header['shape'])
# Update image and update subscribers
@@ -202,7 +209,9 @@ class StdDaqPreviewMixin(CustomDetectorMixin):
self.parent.image.put(image, force=True)
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
t_last=t_curr
logger.info(f"[{self.parent.name}]\tUpdated frame {header['frame']}\tMean: {np.mean(image)}")
name = self.parent.name
nfo = f"[{name}]\tFrameNo: {header['frame']}\tMean: {np.mean(image)}"
logger.info(nfo)
except ValueError:
# Happens when ZMQ partially delivers the multipart message
pass

View File

@@ -52,7 +52,7 @@ class StdDaqRestClient(Device):
cfg_start_udp_port = Component(Signal, kind=Kind.config)
cfg_writer_user_id = Component(Signal, kind=Kind.config)
cfg_submodule_info = Component(Signal, kind=Kind.config)
cfg_max_number_of_forwarders_spawned = Component(Signal, kind=Kind.config)
cfg_max_number_of_forwarders = Component(Signal, kind=Kind.config)
cfg_use_all_forwarders = Component(Signal, kind=Kind.config)
cfg_module_sync_queue_size = Component(Signal, kind=Kind.config)
cfg_module_positions = Component(Signal, kind=Kind.config)
@@ -89,7 +89,7 @@ class StdDaqRestClient(Device):
self.cfg_start_udp_port.set(cfg['start_udp_port']).wait()
self.cfg_writer_user_id.set(cfg['writer_user_id']).wait()
#self.cfg_submodule_info.set(cfg['submodule_info']).wait()
self.cfg_max_number_of_forwarders_spawned.set(cfg['max_number_of_forwarders_spawned']).wait()
self.cfg_max_number_of_forwarders.set(cfg['max_number_of_forwarders']).wait()
self.cfg_use_all_forwarders.set(cfg['use_all_forwarders']).wait()
self.cfg_module_sync_queue_size.set(cfg['module_sync_queue_size']).wait()
#self.cfg_module_positions.set(cfg['module_positions']).wait()
@@ -109,7 +109,7 @@ class StdDaqRestClient(Device):
'writer_user_id': int(self.cfg_writer_user_id.get()),
'log_level': "debug",
'submodule_info': {},
'max_number_of_forwarders_spawned': int(self.cfg_max_number_of_forwarders_spawned.get()),
'max_number_of_forwarders': int(self.cfg_max_number_of_forwarders.get()),
'use_all_forwarders': bool(self.cfg_use_all_forwarders.get()),
'module_sync_queue_size': int(self.cfg_module_sync_queue_size.get()),
'module_positions': {}
@@ -165,7 +165,7 @@ class StdDaqRestClient(Device):
self.cfg_pixel_width.set(pixel_width).wait()
self.write_daq_config()
logger.info(f"[{self.name}] Reconfigured the StdDAQ")
logger.info("[%s] Reconfigured the StdDAQ", self.name)
# No feedback on restart, we just sleep
sleep(3)
@@ -192,7 +192,7 @@ class StdDaqRestClient(Device):
self.read_daq_config()
return super().unstage()
def stop(self, success=False):
def stop(self, *, success=False):
"""Stop op: Read the current configuration from the DAQ
"""
self.unstage()

View File

@@ -85,7 +85,8 @@ class StdDaqClient(Device):
num_retry += 1
sleep(3)
if num_retry==5:
raise ConnectionRefusedError("The standard DAQ websocket interface refused connection 5 times.")
raise ConnectionRefusedError(
"The stdDAQ websocket interface refused connection 5 times.")
def __del__(self):
"""Try to close the socket"""
@@ -174,7 +175,7 @@ class StdDaqClient(Device):
pass
return super().unstage()
def stop(self, success=False):
def stop(self, *, success=False):
""" Stop a running acquisition
WARN: This will also close the connection!!!

View File

@@ -0,0 +1,149 @@
""" Extension class for EpicsMotor
This module extends the basic EpicsMotor with additional functionality. It
exposes additional parameters of the EPICS MotorRecord and provides a more
detailed interface for motors using the new ECMC-based motion systems at PSI.
"""
import warnings
from ophyd import Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind
from ophyd.status import MoveStatus
class SpmgStates:
""" Enum for the EPICS MotorRecord's SPMG state"""
# pylint: disable=too-few-public-methods
STOP = 0
PAUSE = 1
MOVE= 2
GO = 3
class EpicsMotorMR(EpicsMotor):
""" Extended EPICS Motor class
Special motor class that exposes additional motor record functionality.
It extends EpicsMotor base class to provide some simple status checks
before movement.
"""
SUB_PROGRESS = "progress"
motor_deadband = Component(
EpicsSignalRO, ".RDBD", auto_monitor=True, kind=Kind.config)
motor_mode = Component(
EpicsSignal, ".SPMG", auto_monitor=True, put_complete=True, kind=Kind.omitted)
_start_position = None
_target_position = None
# pylint: disable=too-many-arguments
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
**kwargs,
):
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
**kwargs,
)
self.subscribe(self._progress_update, run=False)
def move(self, position, wait=True, **kwargs) -> MoveStatus:
""" Extended move function with a few sanity checks
Note that the default EpicsMotor only supports the 'GO' movement mode.
"""
# Reset SPMG before move
spmg = self.motor_mode.get()
if spmg != SpmgStates.GO:
self.motor_mode.put(SpmgStates.GO).wait()
#Warni if trying to move beyond an active limit
if self.high_limit_switch and position > self.position:
warnings.warn("Attempting to move above active HLS", RuntimeWarning)
if self.low_limit_switch and position < self.position:
warnings.warn("Attempting to move below active LLS", RuntimeWarning)
self._start_position = self.position
self._target_position = position
return super().move(position, wait, **kwargs)
def _progress_update(self, value, **kwargs) -> None:
"""Progress update on the current movement"""
if (self._start_position is None) or (self._target_position is None) or (not self.moving):
self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1)
return
progress = abs(
(value - self._start_position) / (self._target_position - self._start_position)
)
self._run_subs(
sub_type=self.SUB_PROGRESS, value=progress, max_value=1, done=self.moving)
class EpicsMotorEC(EpicsMotorMR):
""" Detailed ECMC EPICS motor class
Special motor class to provide additional functionality for ECMC based motors.
It exposes additional diagnostic fields and includes basic error management.
"""
USER_ACCESS = ['reset']
enable_readback = Component(EpicsSignalRO, "-EnaAct", auto_monitor=True, kind=Kind.normal)
enable = Component(
EpicsSignal, "-EnaCmd-RB", write_pv="-EnaCmd", auto_monitor=True, kind=Kind.normal)
homed = Component(EpicsSignalRO, "-Homed", auto_monitor=True, kind=Kind.normal)
velocity_readback = Component(EpicsSignalRO, "-VelAct", auto_monitor=True, kind=Kind.normal)
position_readback = Component(EpicsSignalRO, "-PosAct", auto_monitor=True, kind=Kind.normal)
position_error = Component(EpicsSignalRO, "-PosErr", auto_monitor=True, kind=Kind.normal)
#high_interlock = Component(EpicsSignalRO, "-SumIlockFwd", auto_monitor=True, kind=Kind.normal)
#low_interlock = Component(EpicsSignalRO, "-SumIlockBwd", auto_monitor=True, kind=Kind.normal)
ecmc_status = Component(EpicsSignalRO, "-Status", auto_monitor=True, kind=Kind.normal)
error = Component(EpicsSignalRO, "-ErrId", auto_monitor=True, kind=Kind.normal)
error_msg = Component(EpicsSignalRO, "-MsgTxt", auto_monitor=True, kind=Kind.normal)
error_reset = Component(EpicsSignal, "-ErrRst", put_complete=True, kind=Kind.omitted)
def move(self, position, wait=True, **kwargs) -> MoveStatus:
""" Extended move function with a few sanity checks
Note that the default EpicsMotor only supports the 'GO' movement mode.
"""
# Reset SPMG before move
error = self.error.get()
if error:
raise RuntimeError(f"Motor is in error state with message: '{self.error_msg.get()}'")
return super().move(position, wait, **kwargs)
def reset(self):
""" Resets an ECMC axis
Recovers an ECMC axis from a previous error. Note that this does not
solve the cause of the error, that you'll have to do yourself.
Common error causes:
-------------------------
- MAX_POSITION_LAG_EXCEEDED : The PID tuning is wrong.
- MAX_VELOCITY_EXCEEDED : Either the PID is wrong or the motor is sticking-slipping
- BOTH_LIMITS_ACTIVE : The motors are probably not connected
"""
# Reset the error
self.error_reset.set(1, settle_time=0.1).wait()
# Check if it disappeared
if self.error.get():
raise RuntimeError(f"Failed to reset axis error: '{self.error_msg.get()}'")