#!/usr/bin/env python
"""
Provides support for the Newport ESP-301 motor controller.
Due to the complexity of this piece of equipment, and relative lack of
documentation and following of normal SCPI guidelines, this file more than
likely contains bugs and non-complete behaviour.
"""
# IMPORTS #####################################################################
from contextlib import contextmanager
from enum import IntEnum
from functools import reduce
import time
from instruments.abstract_instruments import Instrument
from instruments.newport.errors import NewportError
from instruments.units import ureg as u
from instruments.util_fns import assume_units, ProxyList
# CLASSES #####################################################################
# pylint: disable=too-many-lines
[docs]
class NewportESP301(Instrument):
"""
Handles communication with the Newport ESP-301 multiple-axis motor
controller using the protocol documented in the `user's guide`_.
Due to the complexity of this piece of equipment, and relative lack of
documentation and following of normal SCPI guidelines, this class more than
likely contains bugs and non-complete behaviour.
.. _user's guide: http://assets.newport.com/webDocuments-EN/images/14294.pdf
"""
def __init__(self, filelike):
super().__init__(filelike)
self._execute_immediately = True
self._command_list = []
self._bulk_query_resp = ""
self.terminator = "\r"
[docs]
class Axis:
"""
Encapsulates communication concerning a single axis
of an ESP-301 controller. This class should not be
instantiated by the user directly, but is
returned by `NewportESP301.axis`.
"""
# quantities micro inch
# micro_inch = u.UnitQuantity('micro-inch', u.inch / 1e6, symbol='uin')
micro_inch = u.uinch
# Some more work might need to be done here to make
# the encoder_step and motor_step functional
# I really don't have a concrete idea how I'm
# going to do this until I have a physical device
_unit_dict = {
0: u.count,
1: u.count,
2: u.mm,
3: u.um,
4: u.inch,
5: u.mil,
6: micro_inch, # compound unit for micro-inch
7: u.deg,
8: u.grad,
9: u.rad,
10: u.mrad,
11: u.urad,
}
def __init__(self, controller, axis_id):
if not isinstance(controller, NewportESP301):
raise TypeError(
"Axis must be controlled by a Newport ESP-301 " "motor controller."
)
self._controller = controller
self._axis_id = axis_id + 1
self._units = self.units
# CONTEXT MANAGERS ##
@contextmanager
def _units_of(self, units):
"""
Sets the units for the corresponding axis to a those given by an integer
label (see `NewportESP301.Units`), ensuring that the units are properly
reset at the completion of the context manager.
"""
old_units = self._get_units()
self._set_units(units)
yield
self._set_units(old_units)
# PRIVATE METHODS ##
def _get_units(self):
"""
Returns the integer label for the current units set for this axis.
.. seealso::
NewportESP301.Units
"""
return self._controller.Units(
int(self._newport_cmd("SN?", target=self.axis_id))
)
def _set_units(self, new_units):
return self._newport_cmd("SN", target=self.axis_id, params=[int(new_units)])
# PROPERTIES ##
@property
def axis_id(self):
"""
Get axis number of Newport Controller
:type: `int`
"""
return self._axis_id
@property
def is_motion_done(self):
"""
`True` if and only if all motion commands have
completed. This method can be used to wait for
a motion command to complete before sending the next
command.
:type: `bool`
"""
return bool(int(self._newport_cmd("MD?", target=self.axis_id)))
@property
def acceleration(self):
"""
Gets/sets the axis acceleration
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport unit
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("AC?", target=self.axis_id)),
self._units / (u.s**2),
)
@acceleration.setter
def acceleration(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / (u.s**2))
.to(self._units / (u.s**2))
.magnitude
)
self._newport_cmd("AC", target=self.axis_id, params=[newval])
@property
def deceleration(self):
"""
Gets/sets the axis deceleration
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s^2}`
:type: `~pint.Quantity` or float
"""
return assume_units(
float(self._newport_cmd("AG?", target=self.axis_id)),
self._units / (u.s**2),
)
@deceleration.setter
def deceleration(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / (u.s**2))
.to(self._units / (u.s**2))
.magnitude
)
self._newport_cmd("AG", target=self.axis_id, params=[newval])
@property
def estop_deceleration(self):
"""
Gets/sets the axis estop deceleration
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s^2}`
:type: `~pint.Quantity` or float
"""
return assume_units(
float(self._newport_cmd("AE?", target=self.axis_id)),
self._units / (u.s**2),
)
@estop_deceleration.setter
def estop_deceleration(self, decel):
decel = float(
assume_units(decel, self._units / (u.s**2))
.to(self._units / (u.s**2))
.magnitude
)
self._newport_cmd("AE", target=self.axis_id, params=[decel])
@property
def jerk(self):
"""
Gets/sets the jerk rate for the controller
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport unit
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("JK?", target=self.axis_id)),
self._units / (u.s**3),
)
@jerk.setter
def jerk(self, jerk):
jerk = float(
assume_units(jerk, self._units / (u.s**3))
.to(self._units / (u.s**3))
.magnitude
)
self._newport_cmd("JK", target=self.axis_id, params=[jerk])
@property
def velocity(self):
"""
Gets/sets the axis velocity
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("VA?", target=self.axis_id)), self._units / u.s
)
@velocity.setter
def velocity(self, velocity):
velocity = float(
assume_units(velocity, self._units / (u.s))
.to(self._units / u.s)
.magnitude
)
self._newport_cmd("VA", target=self.axis_id, params=[velocity])
@property
def max_velocity(self):
"""
Gets/sets the axis maximum velocity
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("VU?", target=self.axis_id)), self._units / u.s
)
@max_velocity.setter
def max_velocity(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude
)
self._newport_cmd("VU", target=self.axis_id, params=[newval])
@property
def max_base_velocity(self):
"""
Gets/sets the maximum base velocity for stepper motors
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("VB?", target=self.axis_id)), self._units / u.s
)
@max_base_velocity.setter
def max_base_velocity(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude
)
self._newport_cmd("VB", target=self.axis_id, params=[newval])
@property
def jog_high_velocity(self):
"""
Gets/sets the axis jog high velocity
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("JH?", target=self.axis_id)), self._units / u.s
)
@jog_high_velocity.setter
def jog_high_velocity(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude
)
self._newport_cmd("JH", target=self.axis_id, params=[newval])
@property
def jog_low_velocity(self):
"""
Gets/sets the axis jog low velocity
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("JW?", target=self.axis_id)), self._units / u.s
)
@jog_low_velocity.setter
def jog_low_velocity(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude
)
self._newport_cmd("JW", target=self.axis_id, params=[newval])
@property
def homing_velocity(self):
"""
Gets/sets the axis homing velocity
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("OH?", target=self.axis_id)), self._units / u.s
)
@homing_velocity.setter
def homing_velocity(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / u.s).to(self._units / u.s).magnitude
)
self._newport_cmd("OH", target=self.axis_id, params=[newval])
@property
def max_acceleration(self):
"""
Gets/sets the axis max acceleration
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s^2}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("AU?", target=self.axis_id)),
self._units / (u.s**2),
)
@max_acceleration.setter
def max_acceleration(self, newval):
if newval is None:
return
newval = float(
assume_units(newval, self._units / (u.s**2))
.to(self._units / (u.s**2))
.magnitude
)
self._newport_cmd("AU", target=self.axis_id, params=[newval])
@property
def max_deceleration(self):
"""
Gets/sets the axis max decceleration.
Max deaceleration is always the same as acceleration.
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\frac{unit}{s^2}`
:type: `~pint.Quantity` or `float`
"""
return self.max_acceleration
@max_deceleration.setter
def max_deceleration(self, decel):
decel = float(
assume_units(decel, self._units / (u.s**2))
.to(self._units / (u.s**2))
.magnitude
)
self.max_acceleration = decel
@property
def position(self):
"""
Gets real position on axis in units
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport unit
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("TP?", target=self.axis_id)), self._units
)
@property
def desired_position(self):
"""
Gets desired position on axis in units
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport unit
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("DP?", target=self.axis_id)), self._units
)
@property
def desired_velocity(self):
"""
Gets the axis desired velocity in unit/s
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport unit/s
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("DV?", target=self.axis_id)), self._units / u.s
)
@property
def home(self):
"""
Gets/sets the axis home position.
Default should be 0 as that sets current position as home
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport unit
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("DH?", target=self.axis_id)), self._units
)
@home.setter
def home(self, newval):
if newval is None:
return
newval = float(assume_units(newval, self._units).to(self._units).magnitude)
self._newport_cmd("DH", target=self.axis_id, params=[newval])
@property
def units(self):
"""
Get the units that all commands are in reference to.
:type: `~pint.Unit` corresponding to units of axis connected or
int which corresponds to Newport unit number
"""
self._units = self._get_pq_unit(self._get_units())
return self._units
@units.setter
def units(self, newval):
if newval is None:
return
if isinstance(newval, int):
self._units = self._get_pq_unit(self._controller.Units(int(newval)))
elif isinstance(newval, u.Unit):
self._units = newval
newval = self._get_unit_num(newval)
self._set_units(newval)
@property
def encoder_resolution(self):
"""
Gets/sets the resolution of the encode. The minimum number of units
per step. Encoder functionality must be enabled.
:units: The number of units per encoder step
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("SU?", target=self.axis_id)), self._units
)
@encoder_resolution.setter
def encoder_resolution(self, newval):
if newval is None:
return
newval = float(assume_units(newval, self._units).to(self._units).magnitude)
self._newport_cmd("SU", target=self.axis_id, params=[newval])
@property
def full_step_resolution(self):
"""
Gets/sets the axis resolution of the encode. The minimum number of
units per step. Encoder functionality must be enabled.
:units: The number of units per encoder step
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("FR?", target=self.axis_id)), self._units
)
@full_step_resolution.setter
def full_step_resolution(self, newval):
if newval is None:
return
newval = float(assume_units(newval, self._units).to(self._units).magnitude)
self._newport_cmd("FR", target=self.axis_id, params=[newval])
@property
def left_limit(self):
"""
Gets/sets the axis left travel limit
:units: The limit in units
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("SL?", target=self.axis_id)), self._units
)
@left_limit.setter
def left_limit(self, limit):
limit = float(assume_units(limit, self._units).to(self._units).magnitude)
self._newport_cmd("SL", target=self.axis_id, params=[limit])
@property
def right_limit(self):
"""
Gets/sets the axis right travel limit
:units: units
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("SR?", target=self.axis_id)), self._units
)
@right_limit.setter
def right_limit(self, limit):
limit = float(assume_units(limit, self._units).to(self._units).magnitude)
self._newport_cmd("SR", target=self.axis_id, params=[limit])
@property
def error_threshold(self):
"""
Gets/sets the axis error threshold
:units: units
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("FE?", target=self.axis_id)), self._units
)
@error_threshold.setter
def error_threshold(self, newval):
if newval is None:
return
newval = float(assume_units(newval, self._units).to(self._units).magnitude)
self._newport_cmd("FE", target=self.axis_id, params=[newval])
@property
def current(self):
"""
Gets/sets the axis current (amps)
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\text{A}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("QI?", target=self.axis_id)), u.A
)
@current.setter
def current(self, newval):
if newval is None:
return
current = float(assume_units(newval, u.A).to(u.A).magnitude)
self._newport_cmd("QI", target=self.axis_id, params=[current])
@property
def voltage(self):
"""
Gets/sets the axis voltage
:units: As specified (if a `~pint.Quantity`) or assumed to be
of current newport :math:`\\text{V}`
:type: `~pint.Quantity` or `float`
"""
return assume_units(
float(self._newport_cmd("QV?", target=self.axis_id)), u.V
)
@voltage.setter
def voltage(self, newval):
if newval is None:
return
voltage = float(assume_units(newval, u.V).to(u.V).magnitude)
self._newport_cmd("QV", target=self.axis_id, params=[voltage])
@property
def motor_type(self):
"""
Gets/sets the axis motor type
* 0 = undefined
* 1 = DC Servo
* 2 = Stepper motor
* 3 = commutated stepper motor
* 4 = commutated brushless servo motor
:type: `int`
:rtype: `NewportESP301.MotorType`
"""
return self._controller.MotorType(
int(self._newport_cmd("QM?", target=self._axis_id))
)
@motor_type.setter
def motor_type(self, newval):
if newval is None:
return
self._newport_cmd("QM", target=self._axis_id, params=[int(newval)])
@property
def feedback_configuration(self):
"""
Gets/sets the axis Feedback configuration
:type: `int`
"""
return int(self._newport_cmd("ZB?", target=self._axis_id)[:-2], 16)
@feedback_configuration.setter
def feedback_configuration(self, newval):
if newval is None:
return
self._newport_cmd("ZB", target=self._axis_id, params=[int(newval)])
@property
def position_display_resolution(self):
"""
Gets/sets the position display resolution
:type: `int`
"""
return int(self._newport_cmd("FP?", target=self._axis_id))
@position_display_resolution.setter
def position_display_resolution(self, newval):
if newval is None:
return
self._newport_cmd("FP", target=self._axis_id, params=[int(newval)])
@property
def trajectory(self):
"""
Gets/sets the axis trajectory
:type: `int`
"""
return int(self._newport_cmd("TJ?", target=self._axis_id))
@trajectory.setter
def trajectory(self, newval):
if newval is None:
return
self._newport_cmd("TJ", target=self._axis_id, params=[int(newval)])
@property
def microstep_factor(self):
"""
Gets/sets the axis microstep_factor
:type: `int`
"""
return int(self._newport_cmd("QS?", target=self._axis_id))
@microstep_factor.setter
def microstep_factor(self, newval):
if newval is None:
return
newval = int(newval)
if newval < 1 or newval > 250:
raise ValueError("Microstep factor must be between 1 and 250")
else:
self._newport_cmd("QS", target=self._axis_id, params=[newval])
@property
def hardware_limit_configuration(self):
"""
Gets/sets the axis hardware_limit_configuration
:type: `int`
"""
return int(self._newport_cmd("ZH?", target=self._axis_id)[:-2])
@hardware_limit_configuration.setter
def hardware_limit_configuration(self, newval):
if newval is None:
return
self._newport_cmd("ZH", target=self._axis_id, params=[int(newval)])
@property
def acceleration_feed_forward(self):
"""
Gets/sets the axis acceleration_feed_forward setting
:type: `int`
"""
return float(self._newport_cmd("AF?", target=self._axis_id))
@acceleration_feed_forward.setter
def acceleration_feed_forward(self, newval):
if newval is None:
return
self._newport_cmd("AF", target=self._axis_id, params=[float(newval)])
@property
def proportional_gain(self):
"""
Gets/sets the axis proportional_gain
:type: `float`
"""
return float(self._newport_cmd("KP?", target=self._axis_id)[:-1])
@proportional_gain.setter
def proportional_gain(self, newval):
if newval is None:
return
self._newport_cmd("KP", target=self._axis_id, params=[float(newval)])
@property
def derivative_gain(self):
"""
Gets/sets the axis derivative_gain
:type: `float`
"""
return float(self._newport_cmd("KD?", target=self._axis_id))
@derivative_gain.setter
def derivative_gain(self, newval):
if newval is None:
return
self._newport_cmd("KD", target=self._axis_id, params=[float(newval)])
@property
def integral_gain(self):
"""
Gets/sets the axis integral_gain
:type: `float`
"""
return float(self._newport_cmd("KI?", target=self._axis_id))
@integral_gain.setter
def integral_gain(self, newval):
if newval is None:
return
self._newport_cmd("KI", target=self._axis_id, params=[float(newval)])
@property
def integral_saturation_gain(self):
"""
Gets/sets the axis integral_saturation_gain
:type: `float`
"""
return float(self._newport_cmd("KS?", target=self._axis_id))
@integral_saturation_gain.setter
def integral_saturation_gain(self, newval):
if newval is None:
return
self._newport_cmd("KS", target=self._axis_id, params=[float(newval)])
@property
def encoder_position(self):
"""
Gets the encoder position
:type:
"""
with self._units_of(self._controller.Units.encoder_step):
return self.position
# MOVEMENT METHODS #
[docs]
def search_for_home(self, search_mode=None):
"""
Searches this axis only
for home using the method specified by ``search_mode``.
:param HomeSearchMode search_mode: Method to detect when
Home has been found. If None, the search mode is taken from
HomeSearchMode.
"""
if search_mode is None:
search_mode = self._controller.HomeSearchMode.zero_position_count.value
self._controller.search_for_home(axis=self.axis_id, search_mode=search_mode)
[docs]
def move(self, position, absolute=True, wait=False, block=False):
"""
:param position: Position to set move to along this axis.
:type position: `float` or :class:`~pint.Quantity`
:param bool absolute: If `True`, the position ``pos`` is
interpreted as relative to the zero-point of the encoder.
If `False`, ``pos`` is interpreted as relative to the current
position of this axis.
:param bool wait: If True, will tell axis to not execute other
commands until movement is finished
:param bool block: If True, will block code until movement is finished
"""
position = float(
assume_units(position, self._units).to(self._units).magnitude
)
if absolute:
self._newport_cmd("PA", params=[position], target=self.axis_id)
else:
self._newport_cmd("PR", params=[position], target=self.axis_id)
if wait:
self.wait_for_position(position)
if block:
time.sleep(0.003)
mot = self.is_motion_done
while not mot:
mot = self.is_motion_done
[docs]
def move_to_hardware_limit(self):
"""
move to hardware travel limit
"""
self._newport_cmd("MT", target=self.axis_id)
[docs]
def move_indefinitely(self):
"""
Move until told to stop
"""
self._newport_cmd("MV", target=self.axis_id)
[docs]
def abort_motion(self):
"""
Abort motion
"""
self._newport_cmd("AB", target=self.axis_id)
[docs]
def wait_for_stop(self):
"""
Waits for axis motion to stop before next command is executed
"""
self._newport_cmd("WS", target=self.axis_id)
[docs]
def stop_motion(self):
"""
Stop all motion on axis. With programmed deceleration rate
"""
self._newport_cmd("ST", target=self.axis_id)
[docs]
def wait_for_position(self, position):
"""
Wait for axis to reach position before executing next command
:param position: Position to wait for on axis
:type position: float or :class:`~pint.Quantity`
"""
position = float(
assume_units(position, self._units).to(self._units).magnitude
)
self._newport_cmd("WP", target=self.axis_id, params=[position])
[docs]
def wait_for_motion(self, poll_interval=0.01, max_wait=None):
"""
Blocks until all movement along this axis is complete, as reported
by `NewportESP301.Axis.is_motion_done`.
:param float poll_interval: How long (in seconds) to sleep between
checking if the motion is complete.
:param float max_wait: Maximum amount of time to wait before
raising a `IOError`. If `None`, this method will wait
indefinitely.
"""
# FIXME: make sure that the controller is not in
# programming mode, or else this might not work.
# In programming mode, the "WS" command should be
# sent instead, and the two parameters to this method should
# be ignored.
poll_interval = float(assume_units(poll_interval, u.s).to(u.s).magnitude)
if max_wait is not None:
max_wait = float(assume_units(max_wait, u.s).to(u.s).magnitude)
tic = time.time()
while True:
if self.is_motion_done:
return
else:
if max_wait is None or (time.time() - tic) < max_wait:
time.sleep(poll_interval)
else:
raise OSError("Timed out waiting for motion to finish.")
[docs]
def enable(self):
"""
Turns motor axis on.
"""
self._newport_cmd("MO", target=self._axis_id)
[docs]
def disable(self):
"""
Turns motor axis off.
"""
self._newport_cmd("MF", target=self._axis_id)
[docs]
def setup_axis(self, **kwargs):
"""
Setup a non-newport DC servo motor stage. Necessary parameters are.
* 'motor_type' = type of motor see 'QM' in Newport documentation
* 'current' = motor maximum current (A)
* 'voltage' = motor voltage (V)
* 'units' = set units (see NewportESP301.Units)(U)
* 'encoder_resolution' = value of encoder step in terms of (U)
* 'max_velocity' = maximum velocity (U/s)
* 'max_base_velocity' = maximum working velocity (U/s)
* 'homing_velocity' = homing speed (U/s)
* 'jog_high_velocity' = jog high speed (U/s)
* 'jog_low_velocity' = jog low speed (U/s)
* 'max_acceleration' = maximum acceleration (U/s^2)
* 'acceleration' = acceleration (U/s^2)
* 'velocity' = velocity (U/s)
* 'deceleration' = set deceleration (U/s^2)
* 'error_threshold' = set error threshold (U)
* 'estop_deceleration' = estop deceleration (U/s^2)
* 'jerk' = jerk rate (U/s^3)
* 'proportional_gain' = PID proportional gain (optional)
* 'derivative_gain' = PID derivative gain (optional)
* 'integral_gain' = PID internal gain (optional)
* 'integral_saturation_gain' = PID integral saturation (optional)
* 'trajectory' = trajectory mode (optional)
* 'position_display_resolution' (U per step)
* 'feedback_configuration'
* 'full_step_resolution' = (U per step)
* 'home' = (U)
* 'acceleration_feed_forward' = between 0 to 2e9
* 'microstep_factor' = axis microstep factor
* 'reduce_motor_torque_time' = time (ms) between 0 and 60000,
* 'reduce_motor_torque_percentage' = percentage between 0 and 100
"""
self.motor_type = kwargs.get("motor_type")
self.feedback_configuration = kwargs.get("feedback_configuration")
self.full_step_resolution = kwargs.get("full_step_resolution")
self.position_display_resolution = kwargs.get(
"position_display_" "resolution"
)
self.current = kwargs.get("current")
self.voltage = kwargs.get("voltage")
self.units = int(kwargs.get("units"))
self.encoder_resolution = kwargs.get("encoder_resolution")
self.max_acceleration = kwargs.get("max_acceleration")
self.max_velocity = kwargs.get("max_velocity")
self.max_base_velocity = kwargs.get("max_base_velocity")
self.homing_velocity = kwargs.get("homing_velocity")
self.jog_high_velocity = kwargs.get("jog_high_velocity")
self.jog_low_velocity = kwargs.get("jog_low_velocity")
self.acceleration = kwargs.get("acceleration")
self.velocity = kwargs.get("velocity")
self.deceleration = kwargs.get("deceleration")
self.estop_deceleration = kwargs.get("estop_deceleration")
self.jerk = kwargs.get("jerk")
self.error_threshold = kwargs.get("error_threshold")
self.proportional_gain = kwargs.get("proportional_gain")
self.derivative_gain = kwargs.get("derivative_gain")
self.integral_gain = kwargs.get("integral_gain")
self.integral_saturation_gain = kwargs.get("integral_saturation_gain")
self.home = kwargs.get("home")
self.microstep_factor = kwargs.get("microstep_factor")
self.acceleration_feed_forward = kwargs.get("acceleration_feed_forward")
self.trajectory = kwargs.get("trajectory")
self.hardware_limit_configuration = kwargs.get(
"hardware_limit_" "configuration"
)
if (
"reduce_motor_torque_time" in kwargs
and "reduce_motor_torque_percentage" in kwargs
):
motor_time = kwargs["reduce_motor_torque_time"]
motor_time = int(assume_units(motor_time, u.ms).to(u.ms).magnitude)
if motor_time < 0 or motor_time > 60000:
raise ValueError("Time must be between 0 and 60000 ms")
percentage = kwargs["reduce_motor_torque_percentage"]
percentage = int(
assume_units(percentage, u.percent).to(u.percent).magnitude
)
if percentage < 0 or percentage > 100:
raise ValueError(r"Percentage must be between 0 and 100%")
self._newport_cmd(
"QR", target=self._axis_id, params=[motor_time, percentage]
)
# update motor configuration
self._newport_cmd("UF", target=self._axis_id)
self._newport_cmd("QD", target=self._axis_id)
# save configuration
self._newport_cmd("SM")
return self.read_setup()
[docs]
def read_setup(self):
"""
Returns dictionary containing:
'units'
'motor_type'
'feedback_configuration'
'full_step_resolution'
'position_display_resolution'
'current'
'max_velocity'
'encoder_resolution'
'acceleration'
'deceleration'
'velocity'
'max_acceleration'
'homing_velocity'
'jog_high_velocity'
'jog_low_velocity'
'estop_deceleration'
'jerk'
'proportional_gain'
'derivative_gain'
'integral_gain'
'integral_saturation_gain'
'home'
'microstep_factor'
'acceleration_feed_forward'
'trajectory'
'hardware_limit_configuration'
:rtype: dict of `pint.Quantity`, float and int
"""
config = dict()
config["units"] = self.units
config["motor_type"] = self.motor_type
config["feedback_configuration"] = self.feedback_configuration
config["full_step_resolution"] = self.full_step_resolution
config["position_display_resolution"] = self.position_display_resolution
config["current"] = self.current
config["max_velocity"] = self.max_velocity
config["encoder_resolution"] = self.encoder_resolution
config["acceleration"] = self.acceleration
config["deceleration"] = self.deceleration
config["velocity"] = self.velocity
config["max_acceleration"] = self.max_acceleration
config["homing_velocity"] = self.homing_velocity
config["jog_high_velocity"] = self.jog_high_velocity
config["jog_low_velocity"] = self.jog_low_velocity
config["estop_deceleration"] = self.estop_deceleration
config["jerk"] = self.jerk
# config['error_threshold'] = self.error_threshold
config["proportional_gain"] = self.proportional_gain
config["derivative_gain"] = self.derivative_gain
config["integral_gain"] = self.integral_gain
config["integral_saturation_gain"] = self.integral_saturation_gain
config["home"] = self.home
config["microstep_factor"] = self.microstep_factor
config["acceleration_feed_forward"] = self.acceleration_feed_forward
config["trajectory"] = self.trajectory
config["hardware_limit_configuration"] = self.hardware_limit_configuration
return config
[docs]
def get_status(self):
"""
Returns Dictionary containing values:
'units'
'position'
'desired_position'
'desired_velocity'
'is_motion_done'
:rtype: dict
"""
status = dict()
status["units"] = self.units
status["position"] = self.position
status["desired_position"] = self.desired_position
status["desired_velocity"] = self.desired_velocity
status["is_motion_done"] = self.is_motion_done
return status
@staticmethod
def _get_pq_unit(num):
"""
Gets the units for the specified axis.
:units: The units for the attached axis
:type num: int
"""
return NewportESP301.Axis._unit_dict[num]
def _get_unit_num(self, quantity):
"""
Gets the integer label used by the Newport ESP 301 corresponding to a
given `~pint.Quantity`.
:param pint.Quantity quantity: Units to return a label for.
:return int:
"""
for num, quant in self._unit_dict.items():
if quant == quantity:
return num
raise KeyError(f"{quantity} is not a valid unit for Newport Axis")
# pylint: disable=protected-access
def _newport_cmd(self, cmd, **kwargs):
"""
Passes the newport command from the axis class to the parent controller
:param cmd:
:param kwargs:
:return:
"""
return self._controller._newport_cmd(cmd, **kwargs)
# ENUMS #
[docs]
class HomeSearchMode(IntEnum):
"""
Enum containing different search modes code
"""
#: Search along specified axes for the +0 position.
zero_position_count = 0
#: Search for combined Home and Index signals.
home_index_signals = 1
#: Search only for the Home signal.
home_signal_only = 2
#: Search for the positive limit signal.
pos_limit_signal = 3
#: Search for the negative limit signal.
neg_limit_signal = 4
#: Search for the positive limit and Index signals.
pos_index_signals = 5
#: Search for the negative limit and Index signals.
neg_index_signals = 6
[docs]
class MotorType(IntEnum):
"""
Enum for different motor types.
"""
undefined = 0
dc_servo = 1
stepper_motor = 2
commutated_stepper_motor = 3
commutated_brushless_servo = 4
[docs]
class Units(IntEnum):
"""
Enum containing what `units` return means.
"""
encoder_step = 0
motor_step = 1
millimeter = 2
micrometer = 3
inches = 4
milli_inches = 5
micro_inches = 6
degree = 7
gradian = 8
radian = 9
milliradian = 10
microradian = 11
# PROPERTIES #
@property
def axis(self):
"""
Gets the axes of the motor controller as a sequence. For instance,
to move along a given axis::
>>> controller = NewportESP301.open_serial("COM3")
>>> controller.axis[0].move(-0.001, absolute=False)
Note that the axes are numbered starting from zero, so that
Python idioms can be used more easily. This is not the same convention
used in the Newport ESP-301 user's manual, and so care must
be taken when converting examples.
:type: :class:`NewportESP301.Axis`
"""
return ProxyList(self, self.Axis, range(100))
# return _AxisList(self)
# LOW-LEVEL COMMAND METHODS ##
def _newport_cmd(self, cmd, params=tuple(), target=None, errcheck=True):
"""
The Newport ESP-301 command set supports checking for errors,
specifying different axes and allows for multiple parameters.
As such, it is convienent to wrap calls to the low-level
`~instruments.abstract_instruments.Instrument.sendcmd` method
in a method that is aware of the eccenticities of the controller.
This method sends a command, checks for errors on the device
and turns them into exceptions as needed.
:param bool errcheck: If `False`, suppresses the standard error
checking. Note that since error-checking is unsupported
during device programming, ``errcheck`` must be `False`
during ``PGM`` mode.
"""
query_resp = None
if isinstance(target, self.Axis):
target = target.axis_id
raw_cmd = "{target}{cmd}{params}".format(
target=target if target is not None else "",
cmd=cmd.upper(),
params=",".join(map(str, params)),
)
if self._execute_immediately:
query_resp = self._execute_cmd(raw_cmd, errcheck)
else:
self._command_list.append(raw_cmd)
# This works because "return None" is equivalent to "return".
return query_resp
def _execute_cmd(self, raw_cmd, errcheck=True):
"""
Takes a string command and executes it on Newport
:param str raw_cmd:
:param bool errcheck:
:return: response of device
:rtype: `str`
"""
query_resp = None
if "?" in raw_cmd:
query_resp = self.query(raw_cmd)
else:
self.sendcmd(raw_cmd)
if errcheck:
err_resp = self.query("TB?")
# pylint: disable=unused-variable
code, timestamp, msg = err_resp.split(",")
code = int(code)
if code != 0:
raise NewportError(code)
return query_resp
# SPECIFIC COMMANDS ##
def _home(self, axis, search_mode, errcheck=True):
"""
Private method for searching for home "OR", so that
the methods in this class and the axis class can both
point to the same thing.
"""
self._newport_cmd("OR", target=axis, params=[search_mode], errcheck=errcheck)
[docs]
def search_for_home(
self,
axis=1,
search_mode=HomeSearchMode.zero_position_count.value,
errcheck=True,
):
"""
Searches the specified axis for home using the method specified
by ``search_mode``.
:param int axis: Axis ID for which home should be searched for. This
value is 1-based indexing.
:param HomeSearchMode search_mode: Method to detect when
Home has been found.
:param bool errcheck: Boolean to check for errors after each command
that is sent to the instrument.
"""
self._home(axis=axis, search_mode=search_mode, errcheck=errcheck)
[docs]
def reset(self):
"""
Causes the device to perform a hardware reset. Note that
this method is only effective if the watchdog timer is enabled
by the physical jumpers on the ESP-301. Please see the `user's guide`_
for more information.
"""
self._newport_cmd("RS", errcheck=False)
# USER PROGRAMS ##
[docs]
@contextmanager
def define_program(self, program_id):
"""
Erases any existing programs with a given program ID
and instructs the device to record the commands within this
``with`` block to be saved as a program with that ID.
For instance:
>>> controller = NewportESP301.open_serial("COM3")
>>> with controller.define_program(15):
... controller.axis[0].move(0.001, absolute=False)
...
>>> controller.run_program(15)
:param int program_id: An integer label for the new program.
Must be in ``range(1, 101)``.
"""
if program_id not in range(1, 101):
raise ValueError(
"Invalid program ID. Must be an integer from " "1 to 100 (inclusive)."
)
self._newport_cmd("XX", target=program_id)
try:
self._newport_cmd("EP", target=program_id)
yield
finally:
self._newport_cmd("QP")
[docs]
@contextmanager
def execute_bulk_command(self, errcheck=True):
"""
Context manager to execute multiple commands in a single
communication with device
Example::
with self.execute_bulk_command():
execute commands as normal...
:param bool errcheck: Boolean to check for errors after each command
that is sent to the instrument.
"""
self._execute_immediately = False
yield
command_string = reduce(lambda x, y: x + " ; " + y + " ; ", self._command_list)
# TODO: is _bulk_query_resp getting back to user?
self._bulk_query_resp = self._execute_cmd(command_string, errcheck)
self._command_list = []
self._execute_immediately = True
[docs]
def run_program(self, program_id):
"""
Runs a previously defined user program with a given program ID.
:param int program_id: ID number for previously saved user program
"""
if program_id not in range(1, 101):
raise ValueError(
"Invalid program ID. Must be an integer from " "1 to 100 (inclusive)."
)
self._newport_cmd("EX", target=program_id)