From 294d3b0d4e0522e228ae5355f96e542947b8698b Mon Sep 17 00:00:00 2001 From: Holler Mirko Date: Thu, 27 Jun 2024 16:24:32 +0200 Subject: [PATCH] omny galil first commit --- csaxs_bec/device_configs/omny_config.yaml | 33 +- csaxs_bec/devices/omny/galil/galil_ophyd.py | 76 ++++- csaxs_bec/devices/omny/galil/ogalil_ophyd.py | 307 +++++++++++++++++++ 3 files changed, 399 insertions(+), 17 deletions(-) create mode 100644 csaxs_bec/devices/omny/galil/ogalil_ophyd.py diff --git a/csaxs_bec/device_configs/omny_config.yaml b/csaxs_bec/device_configs/omny_config.yaml index 4a0be02..23827ae 100644 --- a/csaxs_bec/device_configs/omny_config.yaml +++ b/csaxs_bec/device_configs/omny_config.yaml @@ -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 \ No newline at end of file diff --git a/csaxs_bec/devices/omny/galil/galil_ophyd.py b/csaxs_bec/devices/omny/galil/galil_ophyd.py index 1ab9112..73b27fe 100644 --- a/csaxs_bec/devices/omny/galil/galil_ophyd.py +++ b/csaxs_bec/devices/omny/galil/galil_ophyd.py @@ -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() diff --git a/csaxs_bec/devices/omny/galil/ogalil_ophyd.py b/csaxs_bec/devices/omny/galil/ogalil_ophyd.py new file mode 100644 index 0000000..3eb9019 --- /dev/null +++ b/csaxs_bec/devices/omny/galil/ogalil_ophyd.py @@ -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)