omny galil first commit

This commit is contained in:
Holler Mirko
2024-06-27 16:24:32 +02:00
committed by wakonig_k
parent 87f002a68b
commit 294d3b0d4e
3 changed files with 399 additions and 17 deletions
+29 -4
View File
@@ -1,8 +1,33 @@
omny_samples:
description: OMNYSampleStorage
deviceClass: csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage
deviceConfig: {}
############################################################
##################### OMNY samples #########################
############################################################
#omny_samples: description: OMNYSampleStorage
# deviceClass: csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage
# deviceConfig: {}
# enabled: true
# onFailure: buffer
# readOnly: false
# readoutPriority: baseline
############################################################
##################### OMNY Galil motors ####################
############################################################
ofzpx:
description: FZP X
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
deviceConfig:
axis_Id: A
host: mpc3217.psi.ch
limits:
- 0
- 0
port: 8081
sign: -1
deviceTags:
- omny
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
in: 0
+63 -13
View File
@@ -105,6 +105,35 @@ class GalilController(Controller):
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 folerr_status(self, axis_Id_numeric) -> bool:
return bool(float(self.socket_put_and_receive(f"MG folaxerr[{axis_Id_numeric}]").strip()))
def motor_temperature(self, axis_Id_numeric) -> float:
voltage = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
voltage2 = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
if voltage2 < voltage:
voltage = voltage2
# convert from [-10,10]V to [0,300]degC
temperature_degC = round((voltage+10.0) / 20.0 * 300.0, 1)
#the motors of the parking station have a different offset
#the range is reduced, so if at the limit, we show an extreme value
# if(ogalil_no==1 && axis_no==6) {
# temperature_degC = (voltage+10.0-11.4) / 20.0 * 300.0
# if (voltage > 9.9)
# temperature_degC=300
# }
# if(ogalil_no==1 && axis_no==7) {
# temperature_degC = (voltage+10.0-12) / 20.0 * 300.0
# if (voltage > 9.9)
# temperature_degC=300
# }
return temperature_degC
def all_axes_referenced(self) -> bool:
"""
@@ -199,7 +228,7 @@ class GalilController(Controller):
def describe(self) -> None:
t = PrettyTable()
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
t.field_names = [
field_names = [
"Axis",
"Name",
"Connected",
@@ -208,20 +237,40 @@ class GalilController(Controller):
"Limits",
"Position",
]
# in case of OMNY
if self.sock.host == "mpc3217.psi.ch":
field_names.append("Temperature")
field_names.append("FolErr")
t.field_names = field_names
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"),
]
)
if self.sock.host == "mpc3217.psi.ch":
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"),
self.motor_temperature(axis.axis_Id_numeric),
self.folerr_status(axis.axis_Id_numeric),
]
)
else:
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)
@@ -229,6 +278,8 @@ class GalilController(Controller):
self.show_running_threads()
self.show_status_other()
def show_status_other(self) -> None:
"""
Show additional device-specific status information.
@@ -275,7 +326,6 @@ class GalilReadbackSignal(GalilSignalRO):
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()
@@ -0,0 +1,307 @@
import threading
import time
import functools
import urllib.request
import xml.etree.ElementTree as ET
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd_devices.utils.controller import threadlocked
from ophyd.status import wait as status_wait
from ophyd.utils import LimitError
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
from csaxs_bec.devices.omny.galil.galil_ophyd import (
BECConfigError,
GalilAxesReferenced,
GalilController,
GalilMotorIsMoving,
GalilSetpointSignal,
GalilSignalRO,
)
logger = bec_logger.logger
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 GalilMotorResolution(GalilSignalRO):
@retry_once
@threadlocked
def _socket_get(self):
#encoder steps per mm, for rotation we have 89565.8666667 instead. to be implemented
return 51200
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"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 == 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 OMNYGalilController(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",
"is_motor_on",
"all_axes_referenced",
]
def _ogalil_switchsocket_status(self):
contents = urllib.request.urlopen("http://mpc3217:8091/netio.xml").read()
root = ET.fromstring(contents)
returnvalue=[]
for j in range(0,4):
status = int(root[1][j][2].text)
returnvalue.append(int(root[1][j][2].text))
if status:
print(f"Controller {j+1} is ON")
else:
print(f"Controller {j+1} is OFF")
return(returnvalue)
def show_status_other(self):
if self.get_digital_input(5):
print("Digital input 5 test.")
else:
print("Digital input 5 test.")
swver = float(self.socket_put_and_receive("MGswver"))
print(f"Ogalil OMNY firmware version {swver:2.0f}.")
self._ogalil_switchsocket_status()
class OMNYGalilMotor(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="mpc3217.psi.ch",
port=8081,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.controller = OMNYGalilController(
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("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("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 stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)