Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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