Skip to content

Commit

Permalink
Merge pull request #83 from RaspberryPiFoundation/servo
Browse files Browse the repository at this point in the history
merging servo into dev
  • Loading branch information
Martin O'Hanlon authored Nov 18, 2022
2 parents f76da5d + 4f9ecbf commit 531be66
Show file tree
Hide file tree
Showing 10 changed files with 835 additions and 7 deletions.
7 changes: 7 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ Speaker
:inherited-members:
:members:

Servo
-----
.. autoclass:: Servo
:show-inheritance:
:inherited-members:
:members:

Motor
-----

Expand Down
15 changes: 15 additions & 0 deletions docs/examples/servo_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from picozero import Servo
from time import sleep

servo = Servo(1)

servo.min()
sleep(1)

servo.mid()
sleep(1)

servo.max()
sleep(1)

servo.off()
5 changes: 5 additions & 0 deletions docs/examples/servo_pulse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from picozero import Servo

servo = Servo(1)

servo.pulse()
10 changes: 10 additions & 0 deletions docs/examples/servo_sweep.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from picozero import Servo
from time import sleep

servo = Servo(1)

for i in range(0, 100):
servo.value = i / 100
sleep(0.1)

servo.off()
648 changes: 648 additions & 0 deletions docs/images/servo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions docs/recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,26 @@ Play individual notes and control the timing or perform another action:

.. literalinclude:: examples/speaker_notes.py

Servo
-----

A servo motor connected to a single pin, 3.3v and ground.

.. image:: images/servo.svg
:alt: A diagram of the Raspberry Pi Pico connected to a servo motor

Move the servo to its minimum, mid and maximum positions.

.. literalinclude:: examples/servo_move.py

Pulse the servo between its minumum and maximum position.

.. literalinclude:: examples/servo_pulse.py

Move the servo gradually from its minimum to maximum position in 100 increments.

.. literalinclude:: examples/servo_sweep.py

Motor
-----

Expand Down
Binary file added docs/sketches/servo.fzz
Binary file not shown.
1 change: 1 addition & 0 deletions picozero/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
RGBLED,
Motor,
Robot,
Servo,

DigitalInputDevice,
Switch,
Expand Down
74 changes: 72 additions & 2 deletions picozero/picozero.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class EventFailedScheduleQueueFull(Exception):
# SUPPORTING CLASSES
###############################################################################

def clamp(n, low, high): return max(low, min(n, high))

class PinMixin:
"""
Mixin used by devices that have a single pin number.
Expand Down Expand Up @@ -358,8 +360,8 @@ class PWMOutputDevice(OutputDevice, PinMixin):
LOW (the :meth:`off` method always does the opposite).
:param bool initial_value:
If :data:`False` (the default), the LED will be off initially. If
:data:`True`, the LED will be switched on initially.
If :data:`0` (the default), the device will be off initially. If
:data:`1`, the device will be switched on initially.
"""

PIN_TO_PWM_CHANNEL = ["0A","0B","1A","1B","2A","2B","3A","3B","4A","4B","5A","5B","6A","6B","7A","7B","0A","0B","1A","1B","2A","2B","3A","3B","4A","4B","5A","5B","6A","6B"]
Expand Down Expand Up @@ -1393,6 +1395,74 @@ def close(self):

Rover = Robot

class Servo(PWMOutputDevice):
"""
Represents a PWM-controlled servo motor.
Setting the `value` to 0 will move the servo to its minimum position,
1 will move the servo to its maximum position. Setting the `value` to
:data:`None` will turn the servo "off" (i.e. no signal is sent).
:type pin: int
:param pin:
The pin the servo motor is connected to.
:param bool initial_value:
If :data:`0`, the servo will be set to its minimum position. If
:data:`1`, the servo will set to its maximum position. If :data:`None`
(the default), the position of the servo will not change.
: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.
:param int duty_factor:
The duty factor of the PWM signal. This is a value between 0 and 65535.
Defaults to 65535.
"""
def __init__(self, pin, initial_value=None, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000, duty_factor=65535):
self._min_duty = int((min_pulse_width / frame_width) * duty_factor)
self._max_duty = int((max_pulse_width / frame_width) * duty_factor)

super().__init__(pin, freq=int(1 / frame_width), duty_factor=duty_factor, initial_value=initial_value)

def _state_to_value(self, state):
return None if state == 0 else clamp((state - self._min_duty) / (self._max_duty - self._min_duty), 0, 1)

def _value_to_state(self, value):
return 0 if value is None else int(self._min_duty + ((self._max_duty - self._min_duty) * value))

def min(self):
"""
Set the servo to its minimum position.
"""
self.value = 0

def mid(self):
"""
Set the servo to its mid-point position.
"""
self.value = 0.5

def max(self):
"""
Set the servo to its maximum position.
"""
self.value = 1

def off(self):
"""
Turn the servo "off" by setting the value to `None`.
"""
self.value = None

###############################################################################
# INPUT DEVICES
Expand Down
62 changes: 57 additions & 5 deletions tests/test_picozero.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ def reset(self):
self._is_set = False

class Testpicozero(unittest.TestCase):


def assertInRange(self, value, lower, upper):
msg = "Expected %r to be in range {} to {}".format(lower, upper)
self.assertTrue(value <= upper, msg)
self.assertTrue(value >= lower, msg)

###########################################################################
# OUTPUT DEVICES
###########################################################################
Expand Down Expand Up @@ -317,10 +322,6 @@ def test_pico_led(self):
pico_led.off()
self.assertEqual(pico_led.value, 0)

###########################################################################
# INPUT DEVICES
###########################################################################

def test_rgb_led_default_values(self):
d = RGBLED(1,2,3)

Expand Down Expand Up @@ -366,6 +367,57 @@ def test_rgb_led_alt_values(self):
self.assertEqual(d.value, (0,1,1))

d.close()

def test_servo_default_value(self):
d = Servo(1)

self.assertEqual(d.value, None)

d.value = 0
self.assertAlmostEqual(d.value, 0, 2)
self.assertInRange(d._pwm.duty_u16(), int((0.001 / 0.02) * 65535) - 1, int((0.001 / 0.02) * 65535) + 1)

d.value = 1
self.assertAlmostEqual(d.value, 1, 2)
self.assertInRange(d._pwm.duty_u16(), int((0.002 / 0.02) * 65535) - 1, int((0.002 / 0.02) * 65535) + 1)

d.value = None
self.assertEqual(d.value, None)
self.assertEqual(d._pwm.duty_u16(), 0)

d.min()
self.assertAlmostEqual(d.value, 0, 2)

d.mid()
self.assertAlmostEqual(d.value, 0.5, 2)

d.max()
self.assertAlmostEqual(d.value, 1, 2)

d.off()
self.assertEqual(d._pwm.duty_u16(), 0)

d.close()

def test_servo_alt_values(self):
d = Servo(1, initial_value=1, min_pulse_width=0.9/1000, max_pulse_width=2.1/1000, frame_width=19/1000)

self.assertAlmostEqual(d.value, 1, 2)

d.value = 0
self.assertInRange(d._pwm.duty_u16(), int((0.0009 / 0.019) * 65535) - 1, int((0.0009 / 0.019) * 65535) + 1)

d.value = 1
self.assertInRange(d._pwm.duty_u16(), int((0.0021 / 0.019) * 65535) - 1, int((0.0021 / 0.019) * 65535) + 1)

d.value = None
self.assertEqual(d._pwm.duty_u16(), 0)

d.close()

###########################################################################
# INPUT DEVICES
###########################################################################

def test_digital_input_device_default_values(self):
d = DigitalInputDevice(1)
Expand Down

0 comments on commit 531be66

Please sign in to comment.