omny galil first commit
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user