# 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()