Source code for hardware_interfaces.newport_control

# Helper modules
from typing import List, Tuple
import logging
import time

# Import essential modules
import serial
import threading

# Set up logger
logger = logging.getLogger(__name__)


[docs]class NewportControl: """ Connect the DC Servo Controller/ Driver Model: CONEX-CC. Reference the connected micrometer-screw LTA-HL. The newport motor is used to move the sample up and down. This prevents that the measurement is taken of only one position on the sample. Why is that necessary? This is useful for UV/VIS experiments, where the excitation has enough energy to "burn out" the sample (Even to the point where the sample sticks ("gets glued") to the sample-container). The movement of the sample makes it possible to distribute the shots over a larger sample area (in this case we move up and down in a line) rather than one single point. With this, it is possible to measure for a longer time until it is necessary to change the sample. The idea is, that while the delay-stage etc. are driving to a new (e.g. delay), the micrometer-screw moves the sample to a new spot. The size of the laser-spot is around 100 micrometers. Therefore, the stepsize should be chosen a little bit higher (i.e. 120 micrometer). This can be done in combination with the shutter (i.e. shutter closes, micrometer-screw moves) which is part of the main program. After reaching the final position (limit of stage axis), the screw should move back. Ideally with a different stepsize such that the position which are set, are not the same as before. The screw should be moved every acquisition (e.g. 500 shots). The up and down movements should continue until the measurement is done. Note, that the controller+screw always have to be initialized before they can be used. **Important parameters of controller:** * Bits per second: 921,600 - baudrate: 921600 * Data bits (bytesize): 8 * Parity: None * Stop bits: 1 * Flow control: Xon/Xoff * Terminator: CRLF (\\r\\n) Note: This class was specifically build for the Newport micrometer screw with the purpose of moving a sample container up and down within certain limits to avoid burning of the sample. Technically it could be used for other Newport devices. For that, the functions must be reevaluated and checked. We would recommend writing a more general class in that case. I.e. NewClass() containing all functions shown in the CONEX-CC documentation. Args: port (str): COM port to which the controller is connected. Please check in the device manager of your operating system. reference_time (float, optional): Sets the time to wait while referenceing. Mainly for troubleshooting. Defaults to 60 because that is the time it takes to go from one limit to the other. lower_bound (float, optional): Sets the minimum position where the motor should travel to. In millimeters. Revelevant for move_sample() method. Defaults to None (physical lower limit of micrometer screw LTA-HL). upper_bound (float, optional): Sets the maximum position where the motor should travel to. In millimeters. Revelevant for move_sample() method. Defaults to None (physical upper limit of micrometer screw LTA-HL). name (str, optional): Name / Identifier to give to this controller. This is relevant for log statements, especially when there is more than one motor in the setup. Defaults to "Newport Controller". Attributes: port (str): COM port to which the controller is connected. position (float): Current position of Micrometer-screw. velocity (float): Current velocity of Micrometer-screw. lower_limit (float): Position of lower limit switch of the Micrometer-screw. upper_limit (float): Position of upper limit switch of the Micrometer-screw. lower_bound (float): User defined lower bound. Typically depends on size of sample container. upper_bound (float): User defined upper bound. Typically depends on size of sample container. direction (int): Variable which is used as an identifier for the direction in which the Micrometer-screw travels. continuous_move_stop (threading.Event): Event that is used to stop continuous movement of micrometer screw. continuously_moving (bool): Indicator whether micrometer screw is continuously moving or not. ser (serial.Serial): Pyserial object which initiates the communication with the Counter. References: | CONEX_CC_Controller_Documentation_Serial.pdf """ def __init__( self, port: str, reference_time: float = 60, lower_bound: float = None, upper_bound: float = None, name: str = "micrometer screw", ): # Assigning attributes self.name = name self.port = port # Connecting the controller with pyserial self.ser = serial.Serial( self.port, 921600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1, xonxoff=True, ) logger.info('Port is open. Controller "{}" is ready.'.format(self.name)) # Start initialization commands In VBA script the command RS## # was used to reset the controller address to #1. This however # is not necessary since only one controller is plugged in. # Using RS## (which is not xxRS !!) makes it so that every # readout also returns "RSAddress #1" which is unnecessary # Execute: Reset controller with xxRS command xx is 1 since the # controller address is now =1 self.send_command("1RS") time.sleep( 1 ) # For initialization wait a little longer (1 sec) to be sure the controller finished # Execute: Referencing ("homing") with xxOR command self.send_command("1OR") time.sleep(reference_time) # Read out lower limit switch command = "1SL?" self.send_command(command) self.lower_limit = float(self.read(command)) logger.info( "The absolute lower limit of the motor is {} mm".format(self.lower_limit) ) time.sleep( 0.05 ) # Other commands need approx. 10 ms to execute a command. 50 ms should be plenty then # Read out upper limit switch command = "1SR?" self.send_command(command) self.upper_limit = float(self.read(command)) logger.info( "The absolute upper limit of the motor is {} mm".format(self.upper_limit) ) time.sleep(0.05) # If the bounds are not specified, set them to the limit values self.lower_bound = self.lower_limit if lower_bound == None else lower_bound self.upper_bound = self.upper_limit if upper_bound == None else upper_bound # Initialize direction for move_sample method 1 = positive # stepsize, -1= negative stepsize self.direction = 1 # Setup attributes for continuously moving micrometer screw # within bounds Setup threading event for continuously moving up # and down screw self.continuous_move_stop = threading.Event() # Setup indicator for whether continuous movement of micrometer # screw is on/off (at the beginning it is always off) self.continuously_moving = False # Read out velocity value for later calculations self.get_velocity() # Read out position value for later calculations self.get_position()
[docs] def get_position(self): """ Returns current position of the motor. """ # Read out position command = "1TP" self.send_command(command) self.position = float(self.read(command)) time.sleep(0.05) return self.position
[docs] def get_velocity(self): """ Returns current velocity of the motor. """ get_velocity = "1VA?" self.send_command(get_velocity) self.velocity = float(self.read(get_velocity)) logger.info("Velocity of the motor is set to {} mm/s.".format(self.velocity)) time.sleep(0.05) return self.velocity
[docs] def set_velocity(self, velocity: int): """ Set the velocity of the micrometer-screw. Args: velocity (int): Velocity value in units/s. Maximum value is at 1 mm/s """ # Check if velocity is within limits #! This depends on the #! configuration setting which should ! not have been changed and #! on the properties of the screw itself if velocity > 1: velocity = 1.0 logger.warning("Maximum velocity exceeded 1 mm/s. It was set to 1 mm/s.") elif velocity <= 0: velocity = 0.01 # It's smoll because else it hurts. Set to small value to prevent motor injury. logger.warning( "Velocity is negative or zero. It was set to a small value (0.01 mm/s)." ) # Set new velocity self.velocity = velocity set_velocity = "1VA{}".format(velocity) self.send_command(set_velocity) time.sleep(0.05)
[docs] def send_command(self, input_command: str): """ Creates necessary ascii format of the command which will be send to the controller Args: input_command (str): Command which should be send to the controller Returns: str: Command which is send to card. In ascii format. """ # .encode is used to transform command into byte ascii_command = (str(input_command) + "\r\n").encode() self.ser.write(ascii_command)
[docs] def read(self, command: str): """ Read out the return from controller. Args: ascii_command (str): Command which is send to card beforehand. In ascii format. Returns: str: Returns what controller sends back as a response """ # The command which was sent looks like this: "1XX?\r\n" or # "1XX4\r\n" (number "4" is arbitrary here) If the command # contains a ? sign it has to be removed (if not .replace does # not replace anything) command = command.replace("?", "") # The return typically looks like this: "1XX2.4\r\n" Decode the # bytes to string and remove \r\n readout = self.ser.readline().decode("utf-8").replace("\r\n", "") # Remove the string defined by command argument i.e. 1XX readout = readout.replace(command, "") return readout
[docs] def move(self, position: float, relative: bool = False, wait: bool = True): """ Move micrometer-screw to set position. This can be set to an absolute position or a relative position. The controller needs time to execute and send the command before it can be moved again. Therefore, it is necessary to check wheather the motor is still moving. The function can wait until a confirmation is received that the motor stopped the motion by halting the interpreter. Args: position (float): Absolute or relative target. E.g. move to 10 mm or move 5 mm more in positive direction (-5 for negative movement) relative (bool, optional): Set True for relative motion. Set False for absolute motion. Defaults to False. wait (bool, optional): Wait for motor to complete the motion by halting interpreter. Defaults to True. """ # Check if motor is still moving with __check_for_moving # function if self.__check_for_moving() == True: logger.error( "Motor is already moving. Therefore, a new move cannot be danced!" ) return # Check if the movement is within limits relative, position = self.check_if_movement_is_within_limits( position, relative=relative ) # Calculate the time which is necessary to complete the movement self.__calculate_movement_time(position, relative=relative) # Execute relative (1PRxx) or absolute (1PAxx) movement if relative == True: command = "1PR{}".format(position) self.send_command(command) logger.info("{} is moving {} mm".format(self.name, position)) else: command = "1PA{}".format(position) self.send_command(command) logger.info("{} is moving to {} mm".format(self.name, position)) # Wait until movement is done if specified in function argument if wait == True: time.sleep(self.waiting_time) self.wait() # Update position value self.get_position()
[docs] def move_sample(self, stepsize: float): """ Moves the sample within defined bounds which depend on the sample holder. Method moves to user defined lower bound if the position is outside of bounds. If the position is within bounds, the sample is moved with a user defined stepsize relative to its current position. If the move/ step goes beyond the sample holder boundaries, the rest of the step length is moved backwards in the opposite direction. Note: To avoid hitting the same spots it is necessary to choose the stepsize such that the length of the sample holder is not divisible by the stepsize. Optimally, the remainder should be half of the stepsize. E.g. sample holder length = 10 divmod(10,0.21) = (47.0, 0.13000000000000037) which is not perfect but #!it's what it's. Args: stepsize (float): Length of the relative movement in millimeters. """ # Check whether position is within sample (holder) boundaries We # add a 2% tolarance in the if statement, because the motor does # not perfectly moves to the bounds. if self.lower_bound * 0.98 <= self.position <= self.upper_bound * 1.02: # If stepsize is bigger the twice the sample length, we can # divide that part out and obtain the remainder: That is the # effective stepsize we need to move. sample_length = abs(self.upper_bound - self.lower_bound) # This ensures that the stepsize is never greater than twice # the sample length stepsize = stepsize % (2 * sample_length) # %: Calculating remainder absolute_target = (self.direction * stepsize) + self.position if self.lower_bound <= absolute_target <= self.upper_bound: # If the absolute target postion is inside of the # bounds, we can simply move with the chosen stepsize in # the corresponding direction self.move(stepsize * self.direction, relative=True) else: if self.direction == 1: # If we are moving towards the upper bound, we need # to subtract the distance to the bound to # 'invert'/'mirror' the movement distance_to_bound = self.upper_bound - self.position if stepsize - distance_to_bound > sample_length: # If the remaining stepsize is (still) greater # than the length of the sample we are # effectiveley starting from the lower bound. We # can now calculate the effective target target = self.lower_bound + ( stepsize - distance_to_bound - sample_length ) self.move(target, relative=False) # Note that we do not need to change the # direction of movement else: # If the remaining stepsize is smaller than the # length of the sample we start from the upper # bound inverting our direction. target = self.upper_bound - (stepsize - distance_to_bound) self.move(target, relative=False) self.direction = -1 elif self.direction == -1: # If we are moving towards the lower bound, we need # to subtract the distance to the bound to # 'invert'/'mirror' the movement distance_to_bound = self.position - self.lower_bound if stepsize - distance_to_bound > sample_length: # If the remaining stepsize is (still) greater # than the length of the sample we are # effectiveley starting from the upper bound. We # can now calculate the effective target target = self.upper_bound - ( stepsize - distance_to_bound - sample_length ) self.move(target, relative=False) # Note that we do not need to change the # direction of movement else: # If the remaining stepsize is smaller than the # length of the sample we start from the lower # bound inverting our direction. target = self.lower_bound + (stepsize - distance_to_bound) self.move(target, relative=False) self.direction = 1 else: # If we are not within bounds, move to top of sample holder # This corresponds to the lower bound because the scale on # the micrometer-screw has 0 (lowest) on top and 25 # (highest) on bottom self.move(self.lower_bound, relative=False)
[docs] def move_continuosly(self): """ Method that continuously moves the mircometer screw back and forth between lower and upper bound until continuous_move_stop Event is set. This method is intentended to be run in a separate thread using for example toggle_continuous_movement. """ while not self.continuous_move_stop.is_set(): self.move(self.lower_bound, relative=False, wait=True) self.move(self.upper_bound, relative=False, wait=True) # Reset event set self.continuous_move_stop.clear()
[docs] def toggle_continuous_movement(self): """ Either sets up or stops continuous back and forth movement running in a different thread. """ # If continuous movement is not running start it if not self.continuously_moving: logging.info("Starting continuous movement for {}".format(self.name)) self.continuous_move_thread = threading.Thread(target=self.move_continuosly) self.continuous_move_thread.daemon = True self.continuously_moving = True # Start thread self.continuous_move_thread.start() # If continuous movement is running stop it elif self.continuously_moving: logging.info("Stopping continuous movement for {}".format(self.name)) self.continuous_move_stop.set() # Wait for thread to stop logging.info( "Waiting for continuous movement to stop for {}".format(self.name) ) self.continuous_move_thread.join() self.continuously_moving = False
[docs] def check_if_movement_is_within_limits( self, position: float, relative: bool = True ): """ Checks if movement is within lower and upper limit. If not, it returns lower or upper limit position respectively. Args: absolute_position (float): Targeted position relative (bool, optional): Set True for relative motion. Set False for absolute motion. Defaults to True. Returns: float: relative or absolute position that is within limits """ # For relative movement which goes out of bound, we have to # correct the relative motion For this we first calculate the # corresponding absolute position E.g. current position = 22; # relative move = 10 -> absolute position = 32 (Oh no, thats too # high) if relative == True: absolute_position = position + self.get_position() else: absolute_position = position # Correct absolute position if necessary (e.g. 30 -> 25; -4 -> # 0) Also, if the relative movement is too high/ low return that # it should go to the limit as an absolute motion Therefore, the # "relative" is also returned if absolute_position > self.upper_limit: logger.warning( "Requested position is beyond upper limit. Setting to {} instead.".format( self.upper_limit ) ) return False, self.upper_limit elif absolute_position < self.lower_limit: logger.warning( "Requested position is beyond lower limit. Setting to {} instead.".format( self.lower_limit ) ) return False, self.lower_limit else: return relative, position
# Concerning relative motion we have to calculate the new target # if boundaries are exceeded From the absolute position subtract # current position As in the example above: 32 -> 25, then 25 - # 22 = 3 (move +3 mm to 25 mm) bravoo!! def __calculate_movement_time(self, target: float, relative: bool = True): """ Calculates the time which is necessary to complete the motion to a set target. The function differentiates between relative and absolute movement. Args: target (float): Target position or relative movement range. absolute (bool, optional): Set True for relative motion. Set False for absolute motion. Defaults to True. Defaults to True. """ # The value must be absolute since it is possible to set # negative relative moves if relative == True: # relative motion relative_move_range = abs(target) elif relative == False: relative_move_range = abs(float(self.position) - float(target)) self.waiting_time = float(relative_move_range) / float(self.velocity)
[docs] def wait(self): """ Performs a check via while loop for Error "000028" where 28 indicates if motor/ screw is moving. """ # 1TS is the command for returning the positioner error and # controller state command = "1TS" while True: self.send_command(command) error_message = self.read(command) if error_message[-2:] != "28": self.get_position() break else: time.sleep(0.05)
def __check_for_moving(self): """ Checks if the motor is still moving by checking the error message (error 000028). Errors can be read out from the manual """ command = "1TS" self.send_command(command) error_message = self.read(command) return ( error_message[-2:] == "28" ) # True if moving, False if in a different state
[docs] def get_configuration(self): """ Read out all parameters which were/are preset in configuration mode. """ command = "1ZT" self.send_command(command) # The return contains many bytes. Just take sufficiently large # amount of bytes (i.e. 1000). config_information = self.ser.read(1000).decode() time.sleep(0.05) return config_information
[docs] def end(self): """ Sets the velocity back to 1mm/s for next use. Moves the screw to 0 to reduce referencing time at next initialization. Closes the Serial connection. """ # If screw is moving continuously # stop moving if self.continuously_moving: self.toggle_continuous_movement() # Set initial velocity to 1 mm/s self.set_velocity(1) self.move(0, relative=False) self.ser.close()
[docs] def __str__(self): """ Read out all relevant parameters. """ config_information = """ Identifier: {} Lower limit (physical): {} mm Upper limit (physical): {} mm Lower bound: {} mm Upper bound: {} mm Position: {} in mm Velocity: {} in mm/s Is moving: {} """.format( self.name, self.lower_limit, self.upper_limit, self.lower_bound, self.upper_bound, self.position, self.velocity, self.__check_for_moving(), ) return config_information
def __enter__(self): return self def __exit__(self, type, value, tb): self.end() # ? should work fine
if __name__ == "__main__": logging.basicConfig(level=logging.INFO) port = "COM5" lower_bound = 8 upper_bound = 12 controller = NewportControl( port, lower_bound=lower_bound, upper_bound=upper_bound, reference_time=3 ) controller.toggle_continuous_movement() import time print("MÄÄÄÄHMÄÄÄH") time.sleep(40) controller.toggle_continuous_movement() controller.toggle_continuous_movement() time.sleep(40) # controller.move(2,relative=False) # for i in range(5): # controller.move_sample(23) print("done") controller.end()