"""HEROS driver for newport picomotors."""
from enum import IntEnum
from herosdevices.core import DeviceCommandQuantity
from herosdevices.core.templates import SerialDeviceTemplate
from herosdevices.helper import mark_driver
[docs]
class MotorType(IntEnum):
"""Motor types supported by Picomotor controller 8742."""
UNCONNECTED = 0
UNKNOWN = 1
TINY = 2
STANDARD = 3
[docs]
@mark_driver(
state="alpha",
info="4 channel controller for picomotors",
product_page="https://www.newport.com/p/8742"
)
class PicomotorController8742(SerialDeviceTemplate):
"""HEROS driver for 4-channel picomotor controller 8742.
Args:
host: IP address of the controller, e.g. "192.168.1.42".
port: listening port, typically 23.
"""
MIN_AXIS = 1
MAX_AXIS = 4
identifier: str = DeviceCommandQuantity(command_get="*IDN?", dtype=str)
def __init__(self, host: str, port: int = 23, timeout: int = 5000) -> None:
SerialDeviceTemplate.__init__(
self,
f"socket://{host}:{port}",
baudrate=None,
timeout=timeout,
read_line_termination=b"\r",
write_line_termination=b"\r",
)
[docs]
def send_command(self, command: str, axis: int | None = None, argument: int | str | None = None) -> None:
"""Send a command to the controller.
Args:
command: the command string.
axis: the axis the command is to be executed on. Not required for global commands.
argument: arguments to the command if required.
"""
_cmd = f"{axis if axis is not None else ''}{command}{argument if argument is not None else ''}"
self.connection.write(_cmd)
[docs]
def query_for_int(self, query: str, axis: int | None = None) -> int | None:
"""Query an integer from the controller.
Args:
query: the query command.
axis: the axis the command is to be executed on. Not required for global queries.
"""
self.connection.write(f"{axis if axis is not None else ''}{query}")
resp = self.connection.read_line()
return int(resp) if resp is not None else None
[docs]
def query_for_string(self, query: str, axis: int | None = None) -> str:
"""Query a string from the controller.
Args:
query: the query command.
axis: the axis the command is to be executed on. Not required for global queries.
"""
self.send_command(query, axis)
resp = self.device.readline()
return resp.decode("utf-8").strip()
[docs]
def query_error(self) -> tuple[int, str]:
"""Query the latest error from the controller.
Upon reading, errors are cleared from the controller's buffer and thus can only be read once.
Returns:
tuple (error code, error message). The first digit of the error code indicates the axis the error has
occured on.
"""
self.send_command("TB?")
resp = self.connection.readline()
err_code, err_msg = resp.strip().split(",")
return int(err_code), err_msg.strip()
[docs]
def reset(self) -> None:
"""Soft reset the controller.
Note:
Connection will be interrupted.
"""
self.connection.write("RS")
[docs]
def abort_motion(self) -> None:
"""Stop any motion immediately without deceleration."""
self.connection.write("AB")
[docs]
@mark_driver(
state="alpha",
info="picomotor linear actuator",
product_page="https://www.newport.com/p/8301NF"
)
class Picomotor:
"""HEROS driver for a single Picomotor.
Args:
controller: RemoteHERO of the controller the motor is attached to.
axis: port of the controller. Available axes are 1,2,3,4.
"""
MIN_VELOCITY = 1
MAX_VELOCITY = 2_000
MIN_ACCELERATION = 1
MAX_ACCELERATION = 200_000
MIN_STEP_COUNT = -2_147_483_648
MAX_STEP_COUNT = 2_147_483_647
def __init__(self, controller: PicomotorController8742, axis: int) -> None:
self.controller = controller
if not PicomotorController8742.MIN_AXIS <= axis <= PicomotorController8742.MAX_AXIS:
raise RuntimeError("Invalid axis: " + str(axis))
self.axis = axis
@property
def velocity(self) -> int:
"""Query the velocity.
Returns:
velocity in steps/second.
"""
return self.controller.query_for_int("VA?", self.axis)
@velocity.setter
def velocity(self, velocity: int) -> None:
"""Set the velocity.
Args:
velocity: velocity in steps/second. Must be within [1, 2_000].
"""
if not self.MIN_VELOCITY <= velocity <= self.MAX_VELOCITY:
raise RuntimeError("Invalid velocity: " + str(velocity))
self.controller.send_command("VA", self.axis, velocity)
@property
def acceleration(self) -> int:
"""Query the acceleration.
Returns:
acceleration in steps/second².
"""
return self.controller.query_for_int("AC?", self.axis)
@acceleration.setter
def acceleration(self, acceleration: int) -> None:
"""Set the acceleration.
Args:
acceleration: acceleration in steps/second². Must be within [1, 200_000].
"""
if not self.MIN_ACCELERATION <= acceleration <= self.MAX_ACCELERATION:
raise RuntimeError("Invelid acceleration: " + str(acceleration))
self.controller.send_command("AC", self.axis, acceleration)
@property
def home_position(self) -> int:
"""Query the home position.
All absolute moves are relative to "home".
Returns:
step count considered home.
"""
return self.controller.query_for_int("DH?", self.axis)
@home_position.setter
def home_position(self, home_position: int | None) -> None:
"""Set the home position.
Set the step count considered "home". When `home_position` is not given, home is set to 0. All absolute moves
are relative to "home".
Args:
home_position: step count considered home. Must be within [-2_147_483_648, 2_147_483_647].
"""
if not self.MIN_STEP_COUNT <= home_position <= self.MAX_STEP_COUNT:
raise RuntimeError("Invalid home_position: " + str(home_position))
self.controller.send_command("DH", self.axis, home_position)
@property
def motion_done(self) -> bool:
"""Check if a move is in progress.
Returns:
True when no movement is in progrogress.
"""
movement_done = self.controller.query_for_int("MD?", self.axis)
return movement_done > 0
[docs]
def move_indefinite(self, direction: int) -> None:
"""Start a movement.
The motor will move indefinitely until a :meth:`stop_motion` command is issued.
Args:
direction: positive value for increasing step count, negative for decreasing.
"""
self.controller.send_command("MV", self.axis, "-" if direction < 0 else "+")
[docs]
def move_absolute(self, target_position: int) -> None:
"""Move to the desired step count.
Args:
target_position: absolute position relative to "home" (see :meth:`set_home_position`). Must be within
[-2_147_483_648, 2_147_483_647]
"""
if not self.MIN_STEP_COUNT <= target_position <= self.MAX_STEP_COUNT:
raise RuntimeError("Invalid target_position: " + str(target_position))
self.controller.send_command("PA", self.axis, target_position)
[docs]
def move_relative(self, steps: int) -> None:
"""Move relative by the desired step count.
Args:
steps: amount of steps to move by. Must be within [-2_147_483_648, 2_147_483_647].
"""
if not self.MIN_STEP_COUNT <= steps <= self.MAX_STEP_COUNT:
raise RuntimeError("Invalid steps: " + str(steps))
self.controller.send_command("PR", self.axis, steps)
@property
def target_position(self) -> int:
"""Query the target position of the current move.
Returns:
target step count of the current move.
"""
return self.controller.query_for_int("PR?", self.axis)
@property
def actual_position(self) -> int:
"""Query the position of the motor.
Returns:
current step count of the motor.
"""
return self.controller.query_for_int("PA?", self.axis)
[docs]
def stop_motion(self) -> None:
"""Stop the movement."""
self.controller.send_command("ST", self.axis)
@property
def motor_type(self) -> MotorType:
"""Query the type of Motor.
Returns:
motor type as specified in :class:`MotorType`
"""
motor_type = self.controller.query_for_int("QM?", self.axis)
return MotorType(motor_type)
@motor_type.setter
def motor_type(self, motor_type: MotorType) -> None:
"""Set the type of of motor.
This loads the default values for acceleration and velocity.
Args:
motor_type: See :class:`MotorType`.
"""
self.controller.send_command("QM", self.axis, motor_type.value)