herosdevices.hardware.newport.picomotor - Heros Devices 0.11.1 documentation

Skip to content
Back to top

# 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

    class MotorType(IntEnum):
        """Motor types supported by Picomotor controller 8742."""

        UNCONNECTED = 0
        UNKNOWN = 1
        TINY = 2
        STANDARD = 3

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

        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)

        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

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

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

        def reset(self) -> None:
            """Soft reset the controller.

            Note:
                Connection will be interrupted.
            """
            self.connection.write("RS")

        def abort_motion(self) -> None:
            """Stop any motion immediately without deceleration."""
            self.connection.write("AB")

    @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

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

        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)

        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)

        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)