mirror of
				https://github.com/KevinMidboe/python-gpiozero.git
				synced 2025-10-29 17:50:37 +00:00 
			
		
		
		
	Fix #248
Add Servo and AngularServo implementation along with docs and tests. This is a deliberately minimal implementation designed to be added to as we agree on new extensions (better than making an all-singing, all-dancing version in which I get things wrong and then wind up making backward incompatible changes to get it right :)
This commit is contained in:
		| @@ -43,6 +43,20 @@ Motor | ||||
| .. autoclass:: Motor(forward, backward, pwm=True) | ||||
|     :members: forward, backward, stop | ||||
|  | ||||
| Servo | ||||
| ===== | ||||
|  | ||||
| .. autoclass:: Servo(pin, initial_value=0, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000) | ||||
|     :inherited-members: | ||||
|     :members: | ||||
|  | ||||
| AngularServo | ||||
| ============ | ||||
|  | ||||
| .. autoclass:: AngularServo(pin, initial_angle=0, min_angle=-90, max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000) | ||||
|     :inherited-members: | ||||
|     :members: | ||||
|  | ||||
| Base Classes | ||||
| ============ | ||||
|  | ||||
|   | ||||
| @@ -32,5 +32,7 @@ digraph classes { | ||||
|     RyanteckRobot->Robot; | ||||
|     CamJamKitRobot->Robot; | ||||
|     Motor->CompositeDevice; | ||||
|     Servo->CompositeDevice; | ||||
|     AngularServo->Servo; | ||||
|     Energenie->Device; | ||||
| } | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 52 KiB | 
| @@ -4,25 +4,25 @@ | ||||
| <!-- Generated by graphviz version 2.38.0 (20140413.2041) | ||||
|  --> | ||||
| <!-- Title: classes Pages: 1 --> | ||||
| <svg width="587pt" height="476pt" | ||||
|  viewBox="0.00 0.00 587.00 476.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||||
| <svg width="687pt" height="476pt" | ||||
|  viewBox="0.00 0.00 686.50 476.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||||
| <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 472)"> | ||||
| <title>classes</title> | ||||
| <polygon fill="white" stroke="none" points="-4,4 -4,-472 583,-472 583,4 -4,4"/> | ||||
| <polygon fill="white" stroke="none" points="-4,4 -4,-472 682.5,-472 682.5,4 -4,4"/> | ||||
| <!-- Device --> | ||||
| <g id="node1" class="node"><title>Device</title> | ||||
| <polygon fill="#9ec6e0" stroke="#9ec6e0" points="499,-468 445,-468 445,-432 499,-432 499,-468"/> | ||||
| <text text-anchor="middle" x="472" y="-447.5" font-family="Sans" font-size="10.00" fill="#000000">Device</text> | ||||
| <polygon fill="#9ec6e0" stroke="#9ec6e0" points="553,-468 499,-468 499,-432 553,-432 553,-468"/> | ||||
| <text text-anchor="middle" x="526" y="-447.5" font-family="Sans" font-size="10.00" fill="#000000">Device</text> | ||||
| </g> | ||||
| <!-- CompositeDevice --> | ||||
| <g id="node2" class="node"><title>CompositeDevice</title> | ||||
| <polygon fill="#9ec6e0" stroke="#9ec6e0" points="472.5,-396 371.5,-396 371.5,-360 472.5,-360 472.5,-396"/> | ||||
| <text text-anchor="middle" x="422" y="-375.5" font-family="Sans" font-size="10.00" fill="#000000">CompositeDevice</text> | ||||
| <polygon fill="#9ec6e0" stroke="#9ec6e0" points="526.5,-396 425.5,-396 425.5,-360 526.5,-360 526.5,-396"/> | ||||
| <text text-anchor="middle" x="476" y="-375.5" font-family="Sans" font-size="10.00" fill="#000000">CompositeDevice</text> | ||||
| </g> | ||||
| <!-- CompositeDevice->Device --> | ||||
| <g id="edge1" class="edge"><title>CompositeDevice->Device</title> | ||||
| <path fill="none" stroke="black" d="M434.36,-396.303C440.233,-404.526 447.369,-414.517 453.842,-423.579"/> | ||||
| <polygon fill="black" stroke="black" points="451.122,-425.793 459.783,-431.896 456.818,-421.724 451.122,-425.793"/> | ||||
| <path fill="none" stroke="black" d="M488.36,-396.303C494.233,-404.526 501.369,-414.517 507.842,-423.579"/> | ||||
| <polygon fill="black" stroke="black" points="505.122,-425.793 513.783,-431.896 510.818,-421.724 505.122,-425.793"/> | ||||
| </g> | ||||
| <!-- CompositeOutputDevice --> | ||||
| <g id="node3" class="node"><title>CompositeOutputDevice</title> | ||||
| @@ -31,8 +31,8 @@ | ||||
| </g> | ||||
| <!-- CompositeOutputDevice->CompositeDevice --> | ||||
| <g id="edge2" class="edge"><title>CompositeOutputDevice->CompositeDevice</title> | ||||
| <path fill="none" stroke="black" d="M335.888,-324.124C350.793,-333.276 369.329,-344.658 385.382,-354.515"/> | ||||
| <polygon fill="black" stroke="black" points="383.652,-357.56 394.005,-359.81 387.315,-351.595 383.652,-357.56"/> | ||||
| <path fill="none" stroke="black" d="M349.097,-324.124C372.166,-333.736 401.136,-345.807 425.57,-355.987"/> | ||||
| <polygon fill="black" stroke="black" points="424.459,-359.316 435.036,-359.932 427.151,-352.855 424.459,-359.316"/> | ||||
| </g> | ||||
| <!-- LEDCollection --> | ||||
| <g id="node4" class="node"><title>LEDCollection</title> | ||||
| @@ -136,13 +136,13 @@ | ||||
| </g> | ||||
| <!-- Robot --> | ||||
| <g id="node14" class="node"><title>Robot</title> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="449,-324 395,-324 395,-288 449,-288 449,-324"/> | ||||
| <text text-anchor="middle" x="422" y="-303.5" font-family="Sans" font-size="10.00" fill="#ffffff">Robot</text> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="467,-324 413,-324 413,-288 467,-288 467,-324"/> | ||||
| <text text-anchor="middle" x="440" y="-303.5" font-family="Sans" font-size="10.00" fill="#ffffff">Robot</text> | ||||
| </g> | ||||
| <!-- Robot->CompositeDevice --> | ||||
| <g id="edge13" class="edge"><title>Robot->CompositeDevice</title> | ||||
| <path fill="none" stroke="black" d="M422,-324.303C422,-332.017 422,-341.288 422,-349.888"/> | ||||
| <polygon fill="black" stroke="black" points="418.5,-349.896 422,-359.896 425.5,-349.896 418.5,-349.896"/> | ||||
| <path fill="none" stroke="black" d="M448.899,-324.303C452.997,-332.272 457.949,-341.9 462.493,-350.736"/> | ||||
| <polygon fill="black" stroke="black" points="459.517,-352.604 467.203,-359.896 465.742,-349.402 459.517,-352.604"/> | ||||
| </g> | ||||
| <!-- RyanteckRobot --> | ||||
| <g id="node15" class="node"><title>RyanteckRobot</title> | ||||
| @@ -151,8 +151,8 @@ | ||||
| </g> | ||||
| <!-- RyanteckRobot->Robot --> | ||||
| <g id="edge14" class="edge"><title>RyanteckRobot->Robot</title> | ||||
| <path fill="none" stroke="black" d="M422,-252.303C422,-260.017 422,-269.288 422,-277.888"/> | ||||
| <polygon fill="black" stroke="black" points="418.5,-277.896 422,-287.896 425.5,-277.896 418.5,-277.896"/> | ||||
| <path fill="none" stroke="black" d="M426.449,-252.303C428.455,-260.102 430.869,-269.491 433.101,-278.171"/> | ||||
| <polygon fill="black" stroke="black" points="429.722,-279.082 435.602,-287.896 436.501,-277.339 429.722,-279.082"/> | ||||
| </g> | ||||
| <!-- CamJamKitRobot --> | ||||
| <g id="node16" class="node"><title>CamJamKitRobot</title> | ||||
| @@ -161,28 +161,48 @@ | ||||
| </g> | ||||
| <!-- CamJamKitRobot->Robot --> | ||||
| <g id="edge15" class="edge"><title>CamJamKitRobot->Robot</title> | ||||
| <path fill="none" stroke="black" d="M504.336,-252.124C490.216,-261.192 472.689,-272.448 457.439,-282.241"/> | ||||
| <polygon fill="black" stroke="black" points="455.29,-279.462 448.767,-287.81 459.073,-285.352 455.29,-279.462"/> | ||||
| <path fill="none" stroke="black" d="M508.506,-252.303C497.045,-261.119 482.942,-271.968 470.516,-281.526"/> | ||||
| <polygon fill="black" stroke="black" points="468.028,-279.024 462.236,-287.896 472.296,-284.573 468.028,-279.024"/> | ||||
| </g> | ||||
| <!-- Motor --> | ||||
| <g id="node17" class="node"><title>Motor</title> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="521,-324 467,-324 467,-288 521,-288 521,-324"/> | ||||
| <text text-anchor="middle" x="494" y="-303.5" font-family="Sans" font-size="10.00" fill="#ffffff">Motor</text> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="539,-324 485,-324 485,-288 539,-288 539,-324"/> | ||||
| <text text-anchor="middle" x="512" y="-303.5" font-family="Sans" font-size="10.00" fill="#ffffff">Motor</text> | ||||
| </g> | ||||
| <!-- Motor->CompositeDevice --> | ||||
| <g id="edge16" class="edge"><title>Motor->CompositeDevice</title> | ||||
| <path fill="none" stroke="black" d="M476.202,-324.303C467.396,-332.865 456.618,-343.344 446.999,-352.696"/> | ||||
| <polygon fill="black" stroke="black" points="444.323,-350.415 439.593,-359.896 449.203,-355.434 444.323,-350.415"/> | ||||
| <path fill="none" stroke="black" d="M503.101,-324.303C499.003,-332.272 494.051,-341.9 489.507,-350.736"/> | ||||
| <polygon fill="black" stroke="black" points="486.258,-349.402 484.797,-359.896 492.483,-352.604 486.258,-349.402"/> | ||||
| </g> | ||||
| <!-- Servo --> | ||||
| <g id="node18" class="node"><title>Servo</title> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="647,-324 593,-324 593,-288 647,-288 647,-324"/> | ||||
| <text text-anchor="middle" x="620" y="-303.5" font-family="Sans" font-size="10.00" fill="#ffffff">Servo</text> | ||||
| </g> | ||||
| <!-- Servo->CompositeDevice --> | ||||
| <g id="edge17" class="edge"><title>Servo->CompositeDevice</title> | ||||
| <path fill="none" stroke="black" d="M592.915,-320.166C572.488,-330.096 544.032,-343.929 520.308,-355.462"/> | ||||
| <polygon fill="black" stroke="black" points="518.604,-352.398 511.14,-359.918 521.664,-358.694 518.604,-352.398"/> | ||||
| </g> | ||||
| <!-- AngularServo --> | ||||
| <g id="node19" class="node"><title>AngularServo</title> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="678.5,-252 597.5,-252 597.5,-216 678.5,-216 678.5,-252"/> | ||||
| <text text-anchor="middle" x="638" y="-231.5" font-family="Sans" font-size="10.00" fill="#ffffff">AngularServo</text> | ||||
| </g> | ||||
| <!-- AngularServo->Servo --> | ||||
| <g id="edge18" class="edge"><title>AngularServo->Servo</title> | ||||
| <path fill="none" stroke="black" d="M633.551,-252.303C631.545,-260.102 629.131,-269.491 626.899,-278.171"/> | ||||
| <polygon fill="black" stroke="black" points="623.499,-277.339 624.398,-287.896 630.278,-279.082 623.499,-277.339"/> | ||||
| </g> | ||||
| <!-- Energenie --> | ||||
| <g id="node18" class="node"><title>Energenie</title> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="555.5,-396 490.5,-396 490.5,-360 555.5,-360 555.5,-396"/> | ||||
| <text text-anchor="middle" x="523" y="-375.5" font-family="Sans" font-size="10.00" fill="#ffffff">Energenie</text> | ||||
| <g id="node20" class="node"><title>Energenie</title> | ||||
| <polygon fill="#2980b9" stroke="#2980b9" points="609.5,-396 544.5,-396 544.5,-360 609.5,-360 609.5,-396"/> | ||||
| <text text-anchor="middle" x="577" y="-375.5" font-family="Sans" font-size="10.00" fill="#ffffff">Energenie</text> | ||||
| </g> | ||||
| <!-- Energenie->Device --> | ||||
| <g id="edge17" class="edge"><title>Energenie->Device</title> | ||||
| <path fill="none" stroke="black" d="M510.393,-396.303C504.403,-404.526 497.124,-414.517 490.521,-423.579"/> | ||||
| <polygon fill="black" stroke="black" points="487.522,-421.752 484.462,-431.896 493.179,-425.874 487.522,-421.752"/> | ||||
| <g id="edge19" class="edge"><title>Energenie->Device</title> | ||||
| <path fill="none" stroke="black" d="M564.393,-396.303C558.403,-404.526 551.124,-414.517 544.521,-423.579"/> | ||||
| <polygon fill="black" stroke="black" points="541.522,-421.752 538.462,-431.896 547.179,-425.874 541.522,-421.752"/> | ||||
| </g> | ||||
| </g> | ||||
| </svg> | ||||
|   | ||||
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB | 
| @@ -100,6 +100,8 @@ from .output_devices import ( | ||||
|     LED, | ||||
|     Buzzer, | ||||
|     Motor, | ||||
|     Servo, | ||||
|     AngularServo, | ||||
|     RGBLED, | ||||
| ) | ||||
| from .boards import ( | ||||
|   | ||||
| @@ -898,3 +898,313 @@ class Motor(SourceMixin, CompositeDevice): | ||||
|         self.forward_device.off() | ||||
|         self.backward_device.off() | ||||
|  | ||||
|  | ||||
| class Servo(SourceMixin, CompositeDevice): | ||||
|     """ | ||||
|     Extends :class:`CompositeDevice` and represents a PWM-controlled servo | ||||
|     motor connected to a GPIO pin. | ||||
|  | ||||
|     Connect a power source (e.g. a battery pack or the 5V pin) to the power | ||||
|     cable of the servo (this is typically colored red); connect the ground | ||||
|     cable of the servo (typically colored black or brown) to the negative of | ||||
|     your battery pack, or a GND pin; connect the final cable (typically colored | ||||
|     white or orange) to the GPIO pin you wish to use for controlling the servo. | ||||
|  | ||||
|     The following code will make the servo move between its minimum, maximum, | ||||
|     and mid-point positions with a pause between each:: | ||||
|  | ||||
|         from gpiozero import Servo | ||||
|         from time import sleep | ||||
|  | ||||
|         servo = Servo(17) | ||||
|         while True: | ||||
|             servo.min() | ||||
|             sleep(1) | ||||
|             servo.mid() | ||||
|             sleep(1) | ||||
|             servo.max() | ||||
|             sleep(1) | ||||
|  | ||||
|     :param int pin: | ||||
|         The GPIO pin which the device is attached to. See :ref:`pin_numbering` | ||||
|         for valid pin numbers. | ||||
|  | ||||
|     :param float initial_value: | ||||
|         If ``0`` (the default), the device's mid-point will be set | ||||
|         initially.  Other values between -1 and +1 can be specified as an | ||||
|         initial position. ``None`` means to start the servo un-controlled (see | ||||
|         :attr:`value`). | ||||
|  | ||||
|     :param float min_pulse_width: | ||||
|         The pulse width corresponding to the servo's minimum position. This | ||||
|         defaults to 1ms. | ||||
|  | ||||
|     :param float max_pulse_width: | ||||
|         The pulse width corresponding to the servo's maximum position. This | ||||
|         defaults to 2ms. | ||||
|  | ||||
|     :param float frame_width: | ||||
|         The length of time between servo control pulses measured in seconds. | ||||
|         This defaults to 20ms which is a common value for servos. | ||||
|     """ | ||||
|     def __init__( | ||||
|             self, pin=None, initial_value=0.0, | ||||
|             min_pulse_width=1/1000, max_pulse_width=2/1000, | ||||
|             frame_width=20/1000): | ||||
|         if min_pulse_width >= max_pulse_width: | ||||
|             raise ValueError('min_pulse_width must be less than max_pulse_width') | ||||
|         if max_pulse_width >= frame_width: | ||||
|             raise ValueError('max_pulse_width must be less than frame_width') | ||||
|         self._frame_width = frame_width | ||||
|         self._min_dc = min_pulse_width / frame_width | ||||
|         self._dc_range = (max_pulse_width - min_pulse_width) / frame_width | ||||
|         self._min_value = -1 | ||||
|         self._value_range = 2 | ||||
|         super(Servo, self).__init__( | ||||
|             pwm_device=PWMOutputDevice(pin, frequency=int(1 / frame_width))) | ||||
|         try: | ||||
|             self.value = initial_value | ||||
|         except: | ||||
|             self.close() | ||||
|             raise | ||||
|  | ||||
|     @property | ||||
|     def frame_width(self): | ||||
|         """ | ||||
|         The time between control pulses, measured in seconds. | ||||
|         """ | ||||
|         return self._frame_width | ||||
|  | ||||
|     @property | ||||
|     def min_pulse_width(self): | ||||
|         """ | ||||
|         The control pulse width corresponding to the servo's minimum position, | ||||
|         measured in seconds. | ||||
|         """ | ||||
|         return self._min_dc * self.frame_width | ||||
|  | ||||
|     @property | ||||
|     def max_pulse_width(self): | ||||
|         """ | ||||
|         The control pulse width corresponding to the servo's maximum position, | ||||
|         measured in seconds. | ||||
|         """ | ||||
|         return (self._dc_range * self.frame_width) + self.min_pulse_width | ||||
|  | ||||
|     @property | ||||
|     def pulse_width(self): | ||||
|         """ | ||||
|         Returns the current pulse width controlling the servo. | ||||
|         """ | ||||
|         if self.pwm_device.pin.frequency is None: | ||||
|             return None | ||||
|         else: | ||||
|             return self.pwm_device.pin.state * self.frame_width | ||||
|  | ||||
|     def min(self): | ||||
|         """ | ||||
|         Set the servo to its minimum position. | ||||
|         """ | ||||
|         self.value = -1 | ||||
|  | ||||
|     def mid(self): | ||||
|         """ | ||||
|         Set the servo to its mid-point position. | ||||
|         """ | ||||
|         self.value = 0 | ||||
|  | ||||
|     def max(self): | ||||
|         """ | ||||
|         Set the servo to its maximum position. | ||||
|         """ | ||||
|         self.value = 1 | ||||
|  | ||||
|     def detach(self): | ||||
|         """ | ||||
|         Temporarily disable control of the servo. This is equivalent to | ||||
|         setting :attr:`value` to ``None``. | ||||
|         """ | ||||
|         self.value = None | ||||
|  | ||||
|     def _get_value(self): | ||||
|         if self.pwm_device.pin.frequency is None: | ||||
|             return None | ||||
|         else: | ||||
|             return ( | ||||
|                 ((self.pwm_device.pin.state - self._min_dc) / self._dc_range) * | ||||
|                 self._value_range + self._min_value) | ||||
|  | ||||
|     @property | ||||
|     def value(self): | ||||
|         """ | ||||
|         Represents the position of the servo as a value between -1 (the minimum | ||||
|         position) and +1 (the maximum position). This can also be the special | ||||
|         value ``None`` indicating that the servo is currently "uncontrolled", | ||||
|         i.e. that no control signal is being sent. Typically this means the | ||||
|         servo's position remains unchanged, but that it can be moved by hand. | ||||
|         """ | ||||
|         result = self._get_value() | ||||
|         if result is None: | ||||
|             return result | ||||
|         else: | ||||
|             # NOTE: This round() only exists to ensure we don't confuse people | ||||
|             # by returning 2.220446049250313e-16 as the default initial value | ||||
|             # instead of 0. The reason _get_value and _set_value are split | ||||
|             # out is for descendents that require the un-rounded values for | ||||
|             # accuracy | ||||
|             return round(result, 14) | ||||
|  | ||||
|     @value.setter | ||||
|     def value(self, value): | ||||
|         if value is None: | ||||
|             self.pwm_device.pin.frequency = None | ||||
|         elif -1 <= value <= 1: | ||||
|             self.pwm_device.pin.frequency = int(1 / self.frame_width) | ||||
|             self.pwm_device.pin.state = ( | ||||
|                 self._min_dc + self._dc_range * | ||||
|                 ((value - self._min_value) / self._value_range) | ||||
|                 ) | ||||
|         else: | ||||
|             raise OutputDeviceBadValue( | ||||
|                 "Servo value must be between -1 and 1, or None") | ||||
|  | ||||
|     @property | ||||
|     def is_active(self): | ||||
|         return self.value is not None | ||||
|  | ||||
|  | ||||
| class AngularServo(Servo): | ||||
|     """ | ||||
|     Extends :class:`Servo` and represents a rotational PWM-controlled servo | ||||
|     motor which can be set to particular angles (assuming valid minimum and | ||||
|     maximum angles are provided to the constructor). | ||||
|  | ||||
|     Connect a power source (e.g. a battery pack or the 5V pin) to the power | ||||
|     cable of the servo (this is typically colored red); connect the ground | ||||
|     cable of the servo (typically colored black or brown) to the negative of | ||||
|     your battery pack, or a GND pin; connect the final cable (typically colored | ||||
|     white or orange) to the GPIO pin you wish to use for controlling the servo. | ||||
|  | ||||
|     Next, calibrate the angles that the servo can rotate to. In an interactive | ||||
|     Python session, construct a :class:`Servo` instance. The servo should move | ||||
|     to its mid-point by default. Set the servo to its minimum value, and | ||||
|     measure the angle from the mid-point. Set the servo to its maximum value, | ||||
|     and again measure the angle:: | ||||
|  | ||||
|         >>> from gpiozero import Servo | ||||
|         >>> s = Servo(17) | ||||
|         >>> s.min() # measure the angle | ||||
|         >>> s.max() # measure the angle | ||||
|  | ||||
|     You should now be able to construct an :class:`AngularServo` instance | ||||
|     with the correct bounds:: | ||||
|  | ||||
|         >>> from gpiozero import AngularServo | ||||
|         >>> s = AngularServo(17, min_angle=-42, max_angle=44) | ||||
|         >>> s.angle = 0.0 | ||||
|         >>> s.angle | ||||
|         0.0 | ||||
|         >>> s.angle = 15 | ||||
|         >>> s.angle | ||||
|         15.0 | ||||
|  | ||||
|     .. note:: | ||||
|  | ||||
|         You can set *min_angle* greater than *max_angle* if you wish to reverse | ||||
|         the sense of the angles (e.g. ``min_angle=45, max_angle=-45``). This | ||||
|         can be useful with servos that rotate in the opposite direction to your | ||||
|         expectations of minimum and maximum. | ||||
|  | ||||
|     :param int pin: | ||||
|         The GPIO pin which the device is attached to. See :ref:`pin_numbering` | ||||
|         for valid pin numbers. | ||||
|  | ||||
|     :param float initial_angle: | ||||
|         Sets the servo's initial angle to the specified value. The default is | ||||
|         0. The value specified must be between *min_angle* and *max_angle* | ||||
|         inclusive. ``None`` means to start the servo un-controlled (see | ||||
|         :attr:`value`). | ||||
|  | ||||
|     :param float min_angle: | ||||
|         Sets the minimum angle that the servo can rotate to. This defaults to | ||||
|         -90, but should be set to whatever you measure from your servo during | ||||
|         calibration. | ||||
|  | ||||
|     :param float max_angle: | ||||
|         Sets the maximum angle that the servo can rotate to. This defaults to | ||||
|         90, but should be set to whatever you measure from your servo during | ||||
|         calibration. | ||||
|  | ||||
|     :param float min_pulse_width: | ||||
|         The pulse width corresponding to the servo's minimum position. This | ||||
|         defaults to 1ms. | ||||
|  | ||||
|     :param float max_pulse_width: | ||||
|         The pulse width corresponding to the servo's maximum position. This | ||||
|         defaults to 2ms. | ||||
|  | ||||
|     :param float frame_width: | ||||
|         The length of time between servo control pulses measured in seconds. | ||||
|         This defaults to 20ms which is a common value for servos. | ||||
|     """ | ||||
|     def __init__( | ||||
|             self, pin=None, initial_angle=0.0, | ||||
|             min_angle=-90, max_angle=90, | ||||
|             min_pulse_width=1/1000, max_pulse_width=2/1000, | ||||
|             frame_width=20/1000): | ||||
|         self._min_angle = min_angle | ||||
|         self._angular_range = max_angle - min_angle | ||||
|         initial_value = 2 * ((initial_angle - min_angle) / self._angular_range) - 1 | ||||
|         super(AngularServo, self).__init__( | ||||
|             pin, initial_value, min_pulse_width, max_pulse_width, frame_width) | ||||
|  | ||||
|     @property | ||||
|     def min_angle(self): | ||||
|         """ | ||||
|         The minimum angle that the servo will rotate to when :meth:`min` is | ||||
|         called. | ||||
|         """ | ||||
|         return self._min_angle | ||||
|  | ||||
|     @property | ||||
|     def max_angle(self): | ||||
|         """ | ||||
|         The maximum angle that the servo will rotate to when :meth:`max` is | ||||
|         called. | ||||
|         """ | ||||
|         return self._min_angle + self._angular_range | ||||
|  | ||||
|     @property | ||||
|     def angle(self): | ||||
|         """ | ||||
|         The position of the servo as an angle measured in degrees. This will | ||||
|         only be accurate if *min_angle* and *max_angle* have been set | ||||
|         appropriately in the constructor. | ||||
|  | ||||
|         This can also be the special value ``None`` indicating that the servo | ||||
|         is currently "uncontrolled", i.e. that no control signal is being sent. | ||||
|         Typically this means the servo's position remains unchanged, but that | ||||
|         it can be moved by hand. | ||||
|         """ | ||||
|         result = self._get_value() | ||||
|         if result is None: | ||||
|             return None | ||||
|         else: | ||||
|             # NOTE: Why round(n, 12) here instead of 14? Angle ranges can be | ||||
|             # much larger than -1..1 so we need a little more rounding to | ||||
|             # smooth off the rough corners! | ||||
|             return round( | ||||
|                 self._angular_range * | ||||
|                 ((result - self._min_value) / self._value_range) + | ||||
|                 self._min_angle, 12) | ||||
|  | ||||
|     @angle.setter | ||||
|     def angle(self, value): | ||||
|         if value is None: | ||||
|             self.value = None | ||||
|         else: | ||||
|             self.value = ( | ||||
|                 self._value_range * | ||||
|                 ((value - self._min_angle) / self._angular_range) + | ||||
|                 self._min_value) | ||||
|  | ||||
|   | ||||
| @@ -920,3 +920,115 @@ def test_motor_reverse_nonpwm(): | ||||
|         device.reverse() | ||||
|         assert device.value == -1 | ||||
|         assert b.state == 1 and f.state == 0 | ||||
|  | ||||
| def test_servo_pins(): | ||||
|     p = MockPWMPin(1) | ||||
|     with Servo(p) as device: | ||||
|         assert device.pwm_device.pin is p | ||||
|         assert isinstance(device.pwm_device, PWMOutputDevice) | ||||
|  | ||||
| def test_servo_bad_value(): | ||||
|     p = MockPWMPin(1) | ||||
|     with pytest.raises(ValueError): | ||||
|         Servo(p, initial_value=2) | ||||
|     with pytest.raises(ValueError): | ||||
|         Servo(p, min_pulse_width=30/1000) | ||||
|     with pytest.raises(ValueError): | ||||
|         Servo(p, max_pulse_width=30/1000) | ||||
|  | ||||
| def test_servo_pins_nonpwm(): | ||||
|     p = MockPin(2) | ||||
|     with pytest.raises(PinPWMUnsupported): | ||||
|         Servo(p) | ||||
|  | ||||
| def test_servo_close(): | ||||
|     p = MockPWMPin(2) | ||||
|     with Servo(p) as device: | ||||
|         device.close() | ||||
|         assert device.closed | ||||
|         assert device.pwm_device.pin is None | ||||
|         device.close() | ||||
|         assert device.closed | ||||
|  | ||||
| def test_servo_pulse_width(): | ||||
|     p = MockPWMPin(2) | ||||
|     with Servo(p, min_pulse_width=5/10000, max_pulse_width=25/10000) as device: | ||||
|         assert isclose(device.min_pulse_width, 5/10000) | ||||
|         assert isclose(device.max_pulse_width, 25/10000) | ||||
|         assert isclose(device.frame_width, 20/1000) | ||||
|         assert isclose(device.pulse_width, 15/10000) | ||||
|         device.value = -1 | ||||
|         assert isclose(device.pulse_width, 5/10000) | ||||
|         device.value = 1 | ||||
|         assert isclose(device.pulse_width, 25/10000) | ||||
|         device.value = None | ||||
|         assert device.pulse_width is None | ||||
|  | ||||
| def test_servo_values(): | ||||
|     p = MockPWMPin(1) | ||||
|     with Servo(p) as device: | ||||
|         device.min() | ||||
|         assert device.is_active | ||||
|         assert device.value == -1 | ||||
|         assert isclose(p.state, 0.05) | ||||
|         device.max() | ||||
|         assert device.is_active | ||||
|         assert device.value == 1 | ||||
|         assert isclose(p.state, 0.1) | ||||
|         device.mid() | ||||
|         assert device.is_active | ||||
|         assert device.value == 0.0 | ||||
|         assert isclose(p.state, 0.075) | ||||
|         device.value = 0.5 | ||||
|         assert device.is_active | ||||
|         assert device.value == 0.5 | ||||
|         assert isclose(p.state, 0.0875) | ||||
|         device.detach() | ||||
|         assert not device.is_active | ||||
|         assert device.value is None | ||||
|         device.value = 0 | ||||
|         assert device.value == 0 | ||||
|         device.value = None | ||||
|         assert device.value is None | ||||
|  | ||||
| def test_angular_servo_range(): | ||||
|     p = MockPWMPin(1) | ||||
|     with AngularServo(p, initial_angle=15, min_angle=0, max_angle=90) as device: | ||||
|         assert device.min_angle == 0 | ||||
|         assert device.max_angle == 90 | ||||
|  | ||||
| def test_angular_servo_angles(): | ||||
|     p = MockPWMPin(1) | ||||
|     with AngularServo(p) as device: | ||||
|         device.angle = 0 | ||||
|         assert device.angle == 0 | ||||
|         assert isclose(device.value, 0) | ||||
|         device.max() | ||||
|         assert device.angle == 90 | ||||
|         assert isclose(device.value, 1) | ||||
|         device.min() | ||||
|         assert device.angle == -90 | ||||
|         assert isclose(device.value, -1) | ||||
|         device.detach() | ||||
|         assert device.angle is None | ||||
|     with AngularServo(p, initial_angle=15, min_angle=0, max_angle=90) as device: | ||||
|         assert device.angle == 15 | ||||
|         assert isclose(device.value, -2/3) | ||||
|         device.angle = 0 | ||||
|         assert device.angle == 0 | ||||
|         assert isclose(device.value, -1) | ||||
|         device.angle = 90 | ||||
|         assert device.angle == 90 | ||||
|         assert isclose(device.value, 1) | ||||
|         device.angle = None | ||||
|         assert device.angle is None | ||||
|     with AngularServo(p, min_angle=45, max_angle=-45) as device: | ||||
|         assert device.angle == 0 | ||||
|         assert isclose(device.value, 0) | ||||
|         device.angle = -45 | ||||
|         assert device.angle == -45 | ||||
|         assert isclose(device.value, 1) | ||||
|         device.angle = -15 | ||||
|         assert device.angle == -15 | ||||
|         assert isclose(device.value, 1/3) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user