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