Source code for pitop.pma.servo_motor

import atexit
from dataclasses import dataclass

from pitop.core.mixins import Recreatable, Stateful
from pitop.pma.servo_controller import ServoController, ServoHardwareSpecs


@dataclass
class ServoMotorSetting:
    angle: float = 0.0
    speed: float = 0.0


[docs] class ServoMotor(Stateful, Recreatable): """Represents a pi-top servo motor component. Note that pi-top servo motors use an open-loop control system. As such, the output of the device (e.g. the angle and speed of the servo horn) cannot be measured directly. This means that you can set a target angle or speed for the servo, but you cannot read the current angle or speed. :type port_name: str :param port_name: The ID for the port to which this component is connected. :type zero_point: int :param zero_point: A user-defined offset from 'true' zero. :type name: str :param name: Component name, defaults to `servo`. Used to access this component when added to a :class:`pitop.Pitop` object. """ __HARDWARE_MIN_ANGLE = -ServoHardwareSpecs.ANGLE_RANGE / 2 __HARDWARE_MAX_ANGLE = ServoHardwareSpecs.ANGLE_RANGE / 2 __DEFAULT_SPEED = 50.0 def __init__(self, port_name, zero_point=0, name="servo"): self._pma_port = port_name self.name = name self._controller = ServoController(self._pma_port) self.__target_angle = 0.0 self.__target_speed = self.__DEFAULT_SPEED self.__min_angle = self.__HARDWARE_MIN_ANGLE self.__max_angle = self.__HARDWARE_MAX_ANGLE self.__has_set_angle = False self.__zero_point = zero_point atexit.register(self.__cleanup) Stateful.__init__(self) Recreatable.__init__( self, config_dict={ "port_name": port_name, "name": name, "zero_point": lambda: self.zero_point, }, ) @property def own_state(self): return { "angle": self.current_angle, "speed": self.current_speed, } def __cleanup(self): if self.__has_set_angle and self.current_speed != 0.0: self.stop()
[docs] def stop(self): """Stop servo at its current position. :return: None """ self.target_angle = self.current_angle
@property def zero_point(self): """Represents the servo motor angle that the library treats as 'zero'. This value can be anywhere in the range of -90 to +90. For example, if the zero_point were set to be -30, then the valid range of values for setting the angle would be -60 to +120. .. warning:: Setting a zero point out of the range of -90 to 90 will cause the method to raise an exception. """ return self.__zero_point @zero_point.setter def zero_point(self, zero_position): if not ( self.__HARDWARE_MIN_ANGLE <= zero_position <= self.__HARDWARE_MAX_ANGLE ): raise ValueError( f"Value must be from {self.__HARDWARE_MIN_ANGLE} to {self.__HARDWARE_MAX_ANGLE} degrees " f"(inclusive)" ) self.__zero_point = zero_position self.__min_angle = self.__HARDWARE_MIN_ANGLE - self.__zero_point self.__max_angle = self.__HARDWARE_MAX_ANGLE - self.__zero_point @property def angle_range(self): """Returns a tuple with minimum and maximum possible angles where the servo horn can be moved to. If :class:`zero_point` is set to 0 (default), the angle range will be (-90, 90). """ return self.__min_angle, self.__max_angle @property def setting(self): """Returns the current state of the servo motor, giving current angle and current speed. :return: :class:'ServoMotorSetting` object that has angle and speed attributes. """ if not self.__has_set_angle: return None, None angle, speed = self._controller.get_current_angle_and_speed() current_state = ServoMotorSetting() current_state.angle = angle - self.zero_point current_state.speed = speed return current_state @setting.setter def setting(self, target_state: ServoMotorSetting): """Sets the target state of the servo horn, relative to the zero position. .. warning:: Using an :data:`target_state.angle` out of the valid angle range will cause the method to raise an exception. To determine the valid angle range, use :meth:`ServoMotor.get_angle_range`. .. warning:: Using a :data:`target_state.speed` out of the valid speed range will cause the method to raise an exception. :type target_state: :class:`ServoMotorSetting` :param target_state: Set the target servo state using the :class:`ServoMotorSetting` class, both :meth:`ServoMotorSetting.speed` and :meth:`ServoMotorSetting.angle` must be passed. Example usage: .. code-block:: python from pitop import ServoMotor, ServoMotorSetting servo = ServoMotor() target_setting = ServoMotorSetting() target_setting.angle = 45 target_setting.speed = 20 servo.setting = target_setting """ self._controller.set_target_angle( target_state.angle + self.__zero_point, target_state.speed ) self.__has_set_angle = True @property def current_angle(self): """Returns the current angle that the servo motor is at. .. note:: If you need synchronized angle and speed values, use :meth:`ServoMotor.state` instead, this will return both current angle and current speed at the same time. :return: float value of the current angle of the servo motor in degrees. """ if not self.__has_set_angle: raise RuntimeError( "Current angle is unknown. " "Please set a servo angle first to initialise the servo to an angle." ) angle, _ = self._controller.get_current_angle_and_speed() return angle - self.zero_point @property def current_speed(self): """Returns the current speed the servo motor is at. .. note:: If you need synchronized angle and speed values, use :meth:`ServoMotor.state` instead, this will return both current angle and current speed at the same time. :return: float value of the current speed of the servo motor in deg/s. """ _, speed = self._controller.get_current_angle_and_speed() return speed @property def smooth_acceleration(self): """Gets whether or not the servo is configured to use a linear acceleration profile to ramp speed at start and end of cycle. :return: boolean value of the acceleration mode """ return self._controller.get_acceleration_mode() == 1 @smooth_acceleration.setter def smooth_acceleration(self, enabled: bool): """Sets whether or not the servo is configured to use a linear acceleration profile to ramp speed at start and end of cycle. :type enabled: bool :param enabled: acceleration mode state """ self._controller.set_acceleration_mode(int(enabled)) @property def target_angle(self): """Returns the last target angle that has been set. :return: float value of the target angle of the servo motor in deg. """ return self.__target_angle @target_angle.setter def target_angle(self, angle): """Set the target angle you want the servo motor to go to. :type angle: float :param angle: target servo motor angle. """ if not (self.__min_angle <= angle <= self.__max_angle): raise ValueError( f"Angle value must be from {self.__min_angle} to {self.__max_angle} degrees (inclusive)" ) self.__target_angle = angle self._controller.set_target_angle( self.__target_angle + self.__zero_point, self.__target_speed ) self.__has_set_angle = True @property def target_speed(self): """Returns the last target speed that has been set. :return: float value of the target speed of the servo motor in deg/s. """ return self.__target_speed @target_speed.setter def target_speed(self, speed): """Sets the servo motor speed. The speed value must be a number from. -100.0 to 100.0 deg/s. .. warning:: Using a :data:`speed` out of the valid speed range will cause the method to raise an exception. :type speed: int or float :param speed: The target speed at which to move the servo horn, from -100 to 100 deg/s. """ if not ( -ServoHardwareSpecs.SPEED_RANGE <= speed <= ServoHardwareSpecs.SPEED_RANGE ): raise ValueError( f"Speed value must be from {ServoHardwareSpecs.SPEED_RANGE} to {ServoHardwareSpecs.SPEED_RANGE} deg/s (inclusive)" ) self.__target_speed = speed
[docs] def sweep(self, speed=None): """Moves the servo horn from the current position to one of the servo motor limits (maximum/minimum possible angle), moving at the specified speed. The speed value must be a number from -100.0 to 100.0 deg/s. The sweep direction is given by the speed. Setting a :data:`speed` value higher than zero will move the horn to the maximum angle (90 degrees by default), while a value less than zero will move it to the minimum angle (-90 degress by default). .. warning:: Using a :data:`speed` out of the valid speed range will cause the method to raise an exception. :type speed: int or float :param speed: The target speed at which to move the servo horn, from -100 to 100 deg/s. """ speed = self.target_speed if speed is None else speed if not ( -ServoHardwareSpecs.SPEED_RANGE <= speed <= ServoHardwareSpecs.SPEED_RANGE ): raise ValueError( f"Speed value must be from {ServoHardwareSpecs.SPEED_RANGE} to {ServoHardwareSpecs.SPEED_RANGE} deg/s (inclusive)" ) angle = self.__min_angle if speed < 0 else self.__max_angle self._controller.set_target_angle(angle + self.__zero_point, speed)