Source code for herosdevices.hardware.newport.picomotor

"""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)