diff --git a/ophyd_devices/galil/galil_ophyd.py b/ophyd_devices/galil/galil_ophyd.py index 3672a6d..420ea87 100644 --- a/ophyd_devices/galil/galil_ophyd.py +++ b/ophyd_devices/galil/galil_ophyd.py @@ -6,9 +6,9 @@ from typing import List import numpy as np from ophyd import Component as Cpt -from ophyd import Device, PositionerBase +from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait -from ophyd.utils import ReadOnlyError +from ophyd.utils import ReadOnlyError, LimitError from ophyd_devices.utils.controller import Controller, threadlocked from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected from prettytable import PrettyTable @@ -29,7 +29,7 @@ class BECConfigError(Exception): def retry_once(fcn): - """Decorator to rerun a function in case a SmaractCommunicationError was raised. This may happen if the buffer was not empty.""" + """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): @@ -49,6 +49,7 @@ class GalilController(Controller): "galil_show_all", "socket_put_and_receive", "socket_put_confirmed", + "lgalil_is_air_off_and_orchestra_enabled" ] def __init__( @@ -347,6 +348,8 @@ class GalilMotor(Device, PositionerBase): 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" @@ -364,6 +367,7 @@ class GalilMotor(Device, PositionerBase): parent=None, host="mpc2680.psi.ch", port=8081, + limits=None, sign=1, socket_cls=SocketIO, device_manager=None, @@ -399,6 +403,30 @@ class GalilMotor(Device, PositionerBase): 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 @@ -438,7 +466,7 @@ class GalilMotor(Device, PositionerBase): If motion fails other than timing out """ self._started_moving = False - timeout = kwargs.pop("timeout", 4) + timeout = kwargs.pop("timeout", 100) status = super().move(position, timeout=timeout, **kwargs) self.user_setpoint.put(position, wait=False) diff --git a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py index 5987aa9..7a05a59 100644 --- a/ophyd_devices/rt_lamni/rt_lamni_ophyd.py +++ b/ophyd_devices/rt_lamni/rt_lamni_ophyd.py @@ -12,8 +12,10 @@ from ophyd_devices.utils.socket import ( SocketSignal, ) from bec_utils import bec_logger -from ophyd.utils import ReadOnlyError +from ophyd.utils import ReadOnlyError,LimitError from ophyd.status import wait as status_wait +from ophyd import Signal + logger = bec_logger.logger @@ -60,7 +62,11 @@ class RtLamniController(Controller): "feedback_disable_and_even_reset_lamni_angle_interferometer", "feedback_enable_with_reset", "add_pos_to_scan", - "clear_trajectory_generator" + "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, @@ -75,6 +81,7 @@ class RtLamniController(Controller): 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, @@ -83,14 +90,19 @@ class RtLamniController(Controller): 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() - #discuss - after disconnect takes a while for the server to be ready again - welcome_message = self.sock.receive() - self.connected = True + try: + self.sock.open() + #discuss - after disconnect takes a while for the server to be ready again + welcome_message = self.sock.receive() + self.connected = True + except ConnectionRefusedError as conn_error: + logger.error("Failed to open a connection to RTLamNI.") + else: logger.info("The connection has already been established.") # warnings.warn(f"The connection has already been established.", stacklevel=2) @@ -146,8 +158,8 @@ class RtLamniController(Controller): def set_rotation_angle(self, val:float): self.socket_put(f"a{(val-300+30.538)/180*np.pi}") - def stop_all_axes(self) -> str: - return 0 #self.socket_put_and_receive(f"XQ#STOP,1") + def stop_all_axes(self): + self.socket_put("sc") def feedback_disable(self): self.socket_put("J0") @@ -158,6 +170,25 @@ class RtLamniController(Controller): #motor_par("lopty","disable",0) #motor_par("loptz","disable",0) + def _set_axis_velocity(self, um_per_s): + self.socket_put(f"V{um_per_s}") + + def _set_axis_velocity_maximum_speed(self): + self.socket_put(f"V0") + + #for developement of soft continuous scanning + def _position_sampling_single_reset_and_start_sampling(self): + self.socket_put(f"Ss") + + 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) + + def feedback_enable_without_reset(self): #read current interferometer position return_table = (self.socket_put_and_receive(f"J4")).split(",") @@ -201,11 +232,21 @@ class RtLamniController(Controller): self.socket_put("sc") logger.info("LamNI scan stopped and deleted, moving to start position") - def add_pos_to_scan(self, posx, posy) -> int: - self.socket_put_and_receive(f"s{posx},{posy},0") + 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 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) @@ -219,12 +260,14 @@ class RtLamniController(Controller): def start_scan(self): interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) if interferometer_feedback_not_running == 1: - logger.info("Cannot start scan because feedback loop is not running or there is an interferometer error.") + 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.info("Cannot start scan because no target positions are planned.") + 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") @@ -235,28 +278,59 @@ class RtLamniController(Controller): def kickoff(self): - time.sleep(0.3) + while not self._min_scan_buffer_reached: + time.sleep(0.001) self.start_scan() - while self.get_scan_status()[0]!=0: - time.sleep(0.1) + time.sleep(0.1) self.start_readout() - + + def read_positions_from_sampler(self): - number_of_samples_to_read = self.get_scan_status()[1] #number of valid samples, will be updated upon first data read + #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 + average_stdeviations_x_st_fzp = 0 average_stdeviations_y_st_fzp = 0 average_lamni_angle = 0 - while number_of_samples_to_read > read_counter: - #fprintf(dname,"DataPoint TotalPoints Target_x Average_x_st_fzp Stdev_x_st_fzp Target_y Average_y_st_fzp Stdev_y_st_fzp Average_cap1 Stdev_cap1 Average_cap2 Stdev_cap2 Average_cap3 Stdev_cap3 Average_cap4 Stdev_cap4 Average_cap5 Stdev_cap5 Average_angle_interf_ST Stdev_angle_interf_ST\n") - # 0 1 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 + + mode,number_of_positions_planned,current_position_in_scan = self.get_scan_status() + + #if not (mode==2 or mode==3): + # error + + #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(.01) + if (current_position_in_scan>5): + while current_position_in_scan > read_counter: + 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 + average_stdeviations_x_st_fzp=average_stdeviations_x_st_fzp+float(return_table[5]) + average_stdeviations_y_st_fzp=average_stdeviations_y_st_fzp+float(return_table[8]) + average_lamni_angle = average_lamni_angle+float(return_table[19]) + + time.sleep(.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"{return_table}") + logger.info(f"Read {read_counter} out of {number_of_positions_planned}") + #logger.info(f"{return_table}") + read_counter = read_counter+1 average_stdeviations_x_st_fzp=average_stdeviations_x_st_fzp+float(return_table[5]) average_stdeviations_y_st_fzp=average_stdeviations_y_st_fzp+float(return_table[8]) average_lamni_angle = average_lamni_angle+float(return_table[19]) - read_counter=read_counter+1 + logger.info(f"LamNI statistics: Average of all standard deviations: x {average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {average_lamni_angle/number_of_samples_to_read}.") + def feedback_status_angle_lamni(self) -> bool: @@ -283,48 +357,39 @@ class RtLamniController(Controller): self.get_device_manager().devices.lsamrot.obj.move(0, wait=True) - #!lgalil_is_air_off_and_orchestra_enabled: - - # try: - ### self.parent.device_manager.devices[ - # self.parent.galil - # ].obj.controller.set_rotation_angle(val[self.parent.name]["value"]) - # except KeyError: - # logger.warning("Failed to set RT value during readback.") - # lgalil_is_air_off_and_orchestra_enabled - #Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller. - #exit with failure - + 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) - #global _lsamx_center - #global _lsamy_center - #umv lsamx _lsamx_center - #umv lsamy _lsamy_center + #needs to be changed to configurable center psoition self.get_device_manager().devices.lsamx.obj.move(8.866, wait=True) self.get_device_manager().devices.lsamy.obj.move(10.18, wait=True) self.socket_put("J1") _waitforfeedbackctr=0 - #this is implemented as class and not function. why? RtLamniFeedbackRunning + interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0]) -# while self.RtLamniFeedbackRunning _rt_status_feedback(self) == 1 && _waitforfeedbackctr<100) -# { -# sleep(0.01) -# _waitforfeedbackctr++ -# #p _waitforfeedbackctr -# } -# motor_par("lsamx","disable",1) -# motor_par("lsamy","disable",1) -# motor_par("loptx","disable",1) -# motor_par("lopty","disable",1) -# motor_par("loptz","disable",1) -# rt_feedback_status + 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]) + # motor_par("lsamx","disable",1) + # motor_par("lsamy","disable",1) + # motor_par("loptx","disable",1) + # motor_par("lopty","disable",1) + # motor_par("loptz","disable",1) + 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 + #ptychography_alignment_done = 0 @@ -440,6 +505,8 @@ class RtLamniMotor(Device, PositionerBase): 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" @@ -460,6 +527,7 @@ class RtLamniMotor(Device, PositionerBase): sign=1, socket_cls=SocketIO, device_manager=None, + limits = None, **kwargs, ): self.axis_Id = axis_Id @@ -483,8 +551,31 @@ class RtLamniMotor(Device, PositionerBase): 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) + # 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(): @@ -527,7 +618,7 @@ class RtLamniMotor(Device, PositionerBase): If motion fails other than timing out """ self._started_moving = False - timeout = kwargs.pop("timeout", 4) + timeout = kwargs.pop("timeout", 100) status = super().move(position, timeout=timeout, **kwargs) self.user_setpoint.put(position, wait=False) diff --git a/ophyd_devices/smaract/smaract_ophyd.py b/ophyd_devices/smaract/smaract_ophyd.py index f7c3207..5f7ae71 100644 --- a/ophyd_devices/smaract/smaract_ophyd.py +++ b/ophyd_devices/smaract/smaract_ophyd.py @@ -5,9 +5,9 @@ from typing import List import numpy as np from ophyd import Component as Cpt -from ophyd import Device, PositionerBase +from ophyd import Device, PositionerBase, Signal from ophyd.status import wait as status_wait -from ophyd.utils import ReadOnlyError +from ophyd.utils import ReadOnlyError, LimitError from ophyd_devices.smaract.smaract_controller import SmaractController from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractError from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected @@ -93,7 +93,8 @@ class SmaractMotor(Device, PositionerBase): # ) 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" # ) @@ -114,6 +115,7 @@ class SmaractMotor(Device, PositionerBase): parent=None, host="mpc2680.psi.ch", port=8085, + limits=None, sign=1, socket_cls=SocketIO, **kwargs, @@ -138,6 +140,29 @@ class SmaractMotor(Device, PositionerBase): 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():