/ adafruit_motor / servo.py
servo.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2017 Scott Shawcroft for Adafruit Industries LLC
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # The above copyright notice and this permission notice shall be included in
 13  # all copies or substantial portions of the Software.
 14  #
 15  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 16  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 17  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 18  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 19  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 20  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 21  # THE SOFTWARE.
 22  """
 23  `adafruit_motor.servo`
 24  ====================================================
 25  
 26  Servos are motor based actuators that incorporate a feedback loop into the design. These feedback
 27  loops enable pulse width modulated control to determine position or rotational speed.
 28  
 29  * Author(s): Scott Shawcroft
 30  """
 31  
 32  __version__ = "0.0.0-auto.0"
 33  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Motor.git"
 34  
 35  # We disable the too few public methods check because this is a private base class for the two types
 36  # of servos.
 37  class _BaseServo:  # pylint: disable-msg=too-few-public-methods
 38      """Shared base class that handles pulse output based on a value between 0 and 1.0
 39  
 40         :param ~pulseio.PWMOut pwm_out: PWM output object.
 41         :param int min_pulse: The minimum pulse length of the servo in microseconds.
 42         :param int max_pulse: The maximum pulse length of the servo in microseconds."""
 43  
 44      def __init__(self, pwm_out, *, min_pulse=750, max_pulse=2250):
 45          self._pwm_out = pwm_out
 46          self.set_pulse_width_range(min_pulse, max_pulse)
 47  
 48      def set_pulse_width_range(self, min_pulse=750, max_pulse=2250):
 49          """Change min and max pulse widths."""
 50          self._min_duty = int((min_pulse * self._pwm_out.frequency) / 1000000 * 0xFFFF)
 51          max_duty = (max_pulse * self._pwm_out.frequency) / 1000000 * 0xFFFF
 52          self._duty_range = int(max_duty - self._min_duty)
 53  
 54      @property
 55      def fraction(self):
 56          """Pulse width expressed as fraction between 0.0 (`min_pulse`) and 1.0 (`max_pulse`).
 57          For conventional servos, corresponds to the servo position as a fraction
 58          of the actuation range. Is None when servo is diabled (pulsewidth of 0ms).
 59          """
 60          if self._pwm_out.duty_cycle == 0:  # Special case for disabled servos
 61              return None
 62          return (self._pwm_out.duty_cycle - self._min_duty) / self._duty_range
 63  
 64      @fraction.setter
 65      def fraction(self, value):
 66          if value is None:
 67              self._pwm_out.duty_cycle = 0  # disable the motor
 68              return
 69          if not 0.0 <= value <= 1.0:
 70              raise ValueError("Must be 0.0 to 1.0")
 71          duty_cycle = self._min_duty + int(value * self._duty_range)
 72          self._pwm_out.duty_cycle = duty_cycle
 73  
 74  
 75  class Servo(_BaseServo):
 76      """Control the position of a servo.
 77  
 78         :param ~pulseio.PWMOut pwm_out: PWM output object.
 79         :param int actuation_range: The physical range of motion of the servo in degrees, \
 80             for the given ``min_pulse`` and ``max_pulse`` values.
 81         :param int min_pulse: The minimum pulse width of the servo in microseconds.
 82         :param int max_pulse: The maximum pulse width of the servo in microseconds.
 83  
 84         ``actuation_range`` is an exposed property and can be changed at any time:
 85  
 86          .. code-block:: python
 87  
 88            servo = Servo(pwm)
 89            servo.actuation_range = 135
 90  
 91         The specified pulse width range of a servo has historically been 1000-2000us,
 92         for a 90 degree range of motion. But nearly all modern servos have a 170-180
 93         degree range, and the pulse widths can go well out of the range to achieve this
 94         extended motion. The default values here of ``750`` and ``2250`` typically give
 95         135 degrees of motion. You can set ``actuation_range`` to correspond to the
 96         actual range of motion you observe with your given ``min_pulse`` and ``max_pulse``
 97         values.
 98  
 99         .. warning:: You can extend the pulse width above and below these limits to
100           get a wider range of movement. But if you go too low or too high,
101           the servo mechanism may hit the end stops, buzz, and draw extra current as it stalls.
102           Test carefully to find the safe minimum and maximum.
103      """
104  
105      def __init__(self, pwm_out, *, actuation_range=180, min_pulse=750, max_pulse=2250):
106          super().__init__(pwm_out, min_pulse=min_pulse, max_pulse=max_pulse)
107          self.actuation_range = actuation_range
108          """The physical range of motion of the servo in degrees."""
109          self._pwm = pwm_out
110  
111      @property
112      def angle(self):
113          """The servo angle in degrees. Must be in the range ``0`` to ``actuation_range``.
114          Is None when servo is disabled."""
115          if self.fraction is None:  # special case for disabled servos
116              return None
117          return self.actuation_range * self.fraction
118  
119      @angle.setter
120      def angle(self, new_angle):
121          if new_angle is None:  # disable the servo by sending 0 signal
122              self.fraction = None
123              return
124          if new_angle < 0 or new_angle > self.actuation_range:
125              raise ValueError("Angle out of range")
126          self.fraction = new_angle / self.actuation_range
127  
128  
129  class ContinuousServo(_BaseServo):
130      """Control a continuous rotation servo.
131  
132         :param int min_pulse: The minimum pulse width of the servo in microseconds.
133         :param int max_pulse: The maximum pulse width of the servo in microseconds."""
134  
135      @property
136      def throttle(self):
137          """How much power is being delivered to the motor. Values range from ``-1.0`` (full
138             throttle reverse) to ``1.0`` (full throttle forwards.) ``0`` will stop the motor from
139             spinning."""
140          return self.fraction * 2 - 1
141  
142      @throttle.setter
143      def throttle(self, value):
144          if value > 1.0 or value < -1.0:
145              raise ValueError("Throttle must be between -1.0 and 1.0")
146          if value is None:
147              raise ValueError("Continuous servos cannot spin freely")
148          self.fraction = (value + 1) / 2
149  
150      def __enter__(self):
151          return self
152  
153      def __exit__(self, exception_type, exception_value, traceback):
154          self.throttle = 0