diff --git a/docs/api.rst b/docs/api.rst
index 9fe63d8..1bdab82 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -56,6 +56,21 @@ Speaker
:inherited-members:
:members:
+Motor
+-----
+
+.. autoclass:: Motor
+ :show-inheritance:
+ :inherited-members:
+ :members:
+
+Robot
+-----
+
+.. autoclass:: Robot
+ :show-inheritance:
+ :inherited-members:
+ :members:
DigitalOutputDevice
-------------------
@@ -103,6 +118,14 @@ TemperatureSensor
:inherited-members:
:members:
+DistanceSensor
+--------------
+
+.. autoclass:: DistanceSensor
+ :show-inheritance:
+ :inherited-members:
+ :members:
+
DigitalInputDevice
------------------
diff --git a/docs/examples/motor_move.py b/docs/examples/motor_move.py
new file mode 100644
index 0000000..1a97ac0
--- /dev/null
+++ b/docs/examples/motor_move.py
@@ -0,0 +1,8 @@
+from picozero import Motor
+from time import sleep
+
+motor = Motor(14, 15)
+
+motor.move()
+sleep(1)
+motor.stop()
\ No newline at end of file
diff --git a/docs/examples/robot_rover_forward.py b/docs/examples/robot_rover_forward.py
new file mode 100644
index 0000000..8944ecc
--- /dev/null
+++ b/docs/examples/robot_rover_forward.py
@@ -0,0 +1,9 @@
+from picozero import Robot
+from time import sleep
+
+robot_rover = Robot(left=(14,15), right=(12,13))
+
+# move forward
+robot_rover.forward()
+sleep(1)
+robot_rover.stop()
diff --git a/docs/examples/robot_rover_square.py b/docs/examples/robot_rover_square.py
new file mode 100644
index 0000000..0b16ba7
--- /dev/null
+++ b/docs/examples/robot_rover_square.py
@@ -0,0 +1,10 @@
+from picozero import Robot
+from time import sleep
+
+robot_rover = Robot(left=(14,15), right=(12,13))
+
+for i in range(4):
+ # move forward for 1 second
+ robot_rover.forward(t=1, wait=True)
+ # rotate to the left for 1 second
+ robot_rover.left(t=1, wait=True)
diff --git a/docs/examples/ultrasonic_distance_sensor.py b/docs/examples/ultrasonic_distance_sensor.py
index accecc5..b2190df 100644
--- a/docs/examples/ultrasonic_distance_sensor.py
+++ b/docs/examples/ultrasonic_distance_sensor.py
@@ -1,8 +1,8 @@
from picozero import DistanceSensor
from time import sleep
-ds = DistanceSensor(echo=5, trigger=4)
+ds = DistanceSensor(echo=4, trigger=5)
while True:
print(ds.distance)
- sleep(0.1)
\ No newline at end of file
+ sleep(0.1)
diff --git a/docs/images/distance_sensor_bb.svg b/docs/images/distance_sensor_bb.svg
new file mode 100644
index 0000000..9aab5aa
--- /dev/null
+++ b/docs/images/distance_sensor_bb.svg
@@ -0,0 +1,817 @@
+
+
+
\ No newline at end of file
diff --git a/docs/images/robot_bb.svg b/docs/images/robot_bb.svg
new file mode 100644
index 0000000..b41b7e4
--- /dev/null
+++ b/docs/images/robot_bb.svg
@@ -0,0 +1,8150 @@
+
+
+
\ No newline at end of file
diff --git a/docs/recipes.rst b/docs/recipes.rst
index 5e5429e..4ceab28 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -154,7 +154,6 @@ Print the value, voltage and percent reported by a potentiometer:
In the Thonny Python editor, choose View->Plotter to plot the output of :meth:`print`.
-
Buzzer
------
@@ -169,6 +168,29 @@ Control a passive buzzer or speaker that can play different tones or frequencies
.. literalinclude:: examples/speaker.py
+Motor
+-----
+
+Move a motor which is connected via 2 pins (forward / backward) and a motor controller board
+
+.. literalinclude:: examples/motor_move.py
+
+Robot rover
+-------------
+
+Make a simple 2 wheeled robot rover.
+
+.. image:: images/robot_bb.svg
+ :alt: A diagram of the Raspberry Pi Pico connected to 2 motors via a motor controller board powered by a battery pack
+
+Move the rover forward for 1 second and stop.
+
+.. literalinclude:: examples/robot_rover_forward.py
+
+Move the rover *roughly* in a square:
+
+.. literalinclude:: examples/robot_rover_square.py
+
Play a tune
~~~~~~~~~~~
@@ -193,6 +215,9 @@ Check the internal temperature of the Raspberry Pi Pico in degrees Celcius:
Ultrasonic Distance Sensor
--------------------------
-Get the distance in metres from an ultrasonic distance sensor (HC-SR04):
+Get the distance in metres from an ultrasonic distance sensor (HC-SR04).
+
+.. image:: images/distance_sensor_bb.svg
+ :alt: A diagram of the Raspberry Pi Pico connected to HC-SR04 distance sensor
.. literalinclude:: examples/ultrasonic_distance_sensor.py
\ No newline at end of file
diff --git a/docs/sketches/distance_sensor.fzz b/docs/sketches/distance_sensor.fzz
new file mode 100644
index 0000000..6e26c36
Binary files /dev/null and b/docs/sketches/distance_sensor.fzz differ
diff --git a/docs/images/pico_led_14.fzz b/docs/sketches/pico_led_14.fzz
similarity index 100%
rename from docs/images/pico_led_14.fzz
rename to docs/sketches/pico_led_14.fzz
diff --git a/docs/sketches/robot.fzz b/docs/sketches/robot.fzz
new file mode 100644
index 0000000..7546c4d
Binary files /dev/null and b/docs/sketches/robot.fzz differ
diff --git a/examples/motor.py b/examples/motor.py
new file mode 100644
index 0000000..2116b54
--- /dev/null
+++ b/examples/motor.py
@@ -0,0 +1,14 @@
+from picozero import Motor
+from time import sleep
+
+m = Motor(12, 13)
+
+# turn the motor for 1 second
+print("the motor is turning")
+m.move()
+sleep(1)
+m.stop()
+
+# or alternatively turn it at half speed for 2 seconds
+m.move(speed=0.5, t=2)
+print("the motor will turn at half speed and stop after 2 seconds")
\ No newline at end of file
diff --git a/picozero/__init__.py b/picozero/__init__.py
index b1b525c..52e38f5 100644
--- a/picozero/__init__.py
+++ b/picozero/__init__.py
@@ -19,6 +19,8 @@
Speaker,
RGBLED,
+ Motor,
+ Robot,
DigitalInputDevice,
Switch,
diff --git a/picozero/picozero.py b/picozero/picozero.py
index c98ec6b..1d08deb 100644
--- a/picozero/picozero.py
+++ b/picozero/picozero.py
@@ -170,11 +170,27 @@ def value(self, value):
self._stop_change()
self._write(value)
- def on(self):
+ def on(self, value=1, t=None, wait=False):
"""
Turns the device on.
+
+ :param float value:
+ The value to set when turning on. Defaults to 1.
+
+ :param float t:
+ The time in seconds the device should be on. If None is
+ specified, the device will stay on. The default is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the device will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
"""
- self.value = 1
+ if t is None:
+ self.value = value
+ else:
+ self._start_change(lambda : iter([(value, t), ]), 1, wait)
def off(self):
"""
@@ -1093,6 +1109,296 @@ def close(self):
RGBLED.colour = RGBLED.color
+class Motor(PinsMixin):
+ """
+ Represents a motor connected to a motor controller which has a 2 pin
+ input. One pin drives the motor "forward", the other drives the motor
+ "backward".
+
+ :type forward: int
+ :param forward:
+ The GP pin that controls the "forward" motion of the motor.
+
+ :type backward: int
+ :param backward:
+ The GP pin that controls the "backward" motion of the motor.
+
+ :param bool pwm:
+ If :data:`True` (the default), PWM pins are used to drive the motor.
+ When using PWM pins values between 0 and 1 can be used to set the
+ speed.
+
+ """
+ def __init__(self, forward, backward, pwm=True):
+ self._pin_nums = (forward, backward)
+ self._forward = PWMOutputDevice(forward) if pwm else DigitalOutputDevice(forward)
+ self._backward = PWMOutputDevice(backward) if pwm else DigitalOutputDevice(backward)
+
+ def on(self, speed=1, t=None, wait=False):
+ """
+ Turns the motor on and makes it turn.
+
+ :param float speed:
+ The speed as a value between -1 and 1. 1 turns the motor at
+ full speed direction, -1 turns the motor at full speed in
+ the opposite direction. Defaults to 1.
+
+ :param float t:
+ The time in seconds the motor should run for. If None is
+ specified, the motor will stay on. The default is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ if speed > 0:
+ self._backward.off()
+ self._forward.on(speed, t, wait)
+
+ elif speed < 0:
+ self._forward.off()
+ self._backward.on(-speed, t, wait)
+
+ else:
+ self.off()
+
+ def off(self):
+ """
+ Stops the motor turning.
+ """
+ self._backward.off()
+ self._forward.off()
+
+ @property
+ def value(self):
+ """
+ Sets or returns the motor speed as a value between -1 and 1. -1 is full
+ speed "backward", 1 is full speed "forward", 0 is stopped.
+ """
+ return self._forward.value + (-self._backward.value)
+
+ @value.setter
+ def value(self, value):
+ if value != 0:
+ self.on(value)
+ else:
+ self.stop()
+
+ def forward(self, speed=1, t=None, wait=False):
+ """
+ Makes the motor turn "forward".
+
+ :param float speed:
+ The speed as a value between 0 and 1. 1 is full speed. Defaults to 1.
+
+ :param float t:
+ The time in seconds the motor should turn for. If None is
+ specified, the motor will stay on. The default is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ self.on(speed, t, wait)
+
+ def backward(self, speed=1, t=None, wait=False):
+ """
+ Makes the motor turn "backward".
+
+ :param float speed:
+ The speed as a value between 0 and 1. 1 is full speed. Defaults to 1.
+
+ :param float t:
+ The time in seconds the motor should turn for. If None is
+ specified, the motor will stay on. The default is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ self.on(-speed, t, wait)
+
+ def close(self):
+ """
+ Closes the device and releases any resources. Once closed, the device
+ can no longer be used.
+ """
+ self._forward.close()
+ self._backward.close()
+
+Motor.start = Motor.on
+Motor.stop = Motor.off
+
+class Robot:
+ """
+ Represent a generic dual-motor robot / rover / buggy.
+
+ This class is constructed with two tuples representing the forward and
+ backward pins of the left and right controllers respectively. For example,
+ if the left motor's controller is connected to pins 12 and 13, while the
+ right motor's controller is connected to pins 14 and 15 then the following
+ example will drive the robot forward::
+
+ from picozero import Robot
+
+ robot = Robot(left=(12, 13), right=(14, 15))
+ robot.forward()
+
+ :param tuple left:
+ A tuple of two pins representing the forward and backward inputs of the
+ left motor's controller.
+
+ :param tuple right:
+ A tuple of two pins representing the forward and backward inputs of the
+ left motor's controller.
+
+ :param bool pwm:
+ If :data:`True` (the default), pwm pins will be used, allowing ariable
+ speed control.
+
+ """
+ def __init__(self, left, right, pwm=True):
+ self._left = Motor(left[0], left[1], pwm)
+ self._right = Motor(right[0], right[1], pwm)
+
+ @property
+ def left_motor(self):
+ """
+ Returns the left :class:`Motor`.
+ """
+ return self._left
+
+ @property
+ def right_motor(self):
+ """
+ Returns the right :class:`Motor`.
+ """
+ return self._right
+
+ @property
+ def value(self):
+ """
+ Represents the motion of the robot as a tuple of (left_motor_speed,
+ right_motor_speed) with ``(-1, -1)`` representing full speed backwards,
+ ``(1, 1)`` representing full speed forwards, and ``(0, 0)``
+ representing stopped.
+ """
+ return (self._left.value, self._right.value)
+
+ @value.setter
+ def value(self, value):
+ self._left.value, self._right.value = value
+
+ def forward(self, speed=1, t=None, wait=False):
+ """
+ Makes the robot move "forward".
+
+ :param float speed:
+ The speed as a value between 0 and 1. 1 is full speed. Defaults to 1.
+
+ :param float t:
+ The time in seconds the robot should move for. If None is
+ specified, the robot will continue to move until stopped. The default
+ is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ self._left.forward(speed, t, False)
+ self._right.forward(speed, t, wait)
+
+ def backward(self, speed=1, t=None, wait=False):
+ """
+ Makes the robot move "backward".
+
+ :param float speed:
+ The speed as a value between 0 and 1. 1 is full speed. Defaults to 1.
+
+ :param float t:
+ The time in seconds the robot should move for. If None is
+ specified, the robot will continue to move until stopped. The default
+ is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ self._left.backward(speed, t, False)
+ self._right.backward(speed, t, wait)
+
+ def left(self, speed=1, t=None, wait=False):
+ """
+ Makes the robot turn "left" by turning the left motor backward and the
+ right motor forward.
+
+ :param float speed:
+ The speed as a value between 0 and 1. 1 is full speed. Defaults to 1.
+
+ :param float t:
+ The time in seconds the robot should turn for. If None is
+ specified, the robot will continue to turn until stopped. The default
+ is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ self._left.backward(speed, t, False)
+ self._right.forward(speed, t, wait)
+
+ def right(self, speed=1, t=None, wait=False):
+ """
+ Makes the robot turn "right" by turning the left motor forward and the
+ right motor backward.
+
+ :param float speed:
+ The speed as a value between 0 and 1. 1 is full speed. Defaults to 1.
+
+ :param float t:
+ The time in seconds the robot should turn for. If None is
+ specified, the robot will continue to turn until stopped. The default
+ is None.
+
+ :param bool wait:
+ If True the method will block until the time `t` has expired.
+ If False the method will return and the motor will turn on in
+ the background. Defaults to False. Only effective if `t` is not
+ None.
+ """
+ self._left.forward(speed, t, False)
+ self._right.backward(speed, t, wait)
+
+ def stop(self):
+ """
+ Stops the robot.
+ """
+ self._left.stop()
+ self._right.stop()
+
+ def close(self):
+ """
+ Closes the device and releases any resources. Once closed, the device
+ can no longer be used.
+ """
+ self._left.close()
+ self._right.close()
+
+Rover = Robot
+
+
###############################################################################
# INPUT DEVICES
###############################################################################
@@ -1555,3 +1861,4 @@ def max_distance(self):
Returns the maximum distance that the sensor will measure in metres.
"""
return self._max_distance
+
diff --git a/tests/test_picozero.py b/tests/test_picozero.py
index a99ddc0..2619dee 100644
--- a/tests/test_picozero.py
+++ b/tests/test_picozero.py
@@ -63,6 +63,10 @@ def reset(self):
class Testpicozero(unittest.TestCase):
+ ###########################################################################
+ # OUTPUT DEVICES
+ ###########################################################################
+
def test_digital_output_device_default_values(self):
d = DigitalOutputDevice(1)
@@ -221,6 +225,76 @@ def test_pwm_output_device_pulse(self):
self.fail(f"{len(values)} values were generated, {len(expected)} were expected.")
d.close()
+
+ def test_motor_default_values(self):
+ d = Motor(1,2)
+
+ self.assertEqual(d.value, 0)
+
+ d.on()
+ self.assertEqual(d.value, 1)
+
+ d.stop()
+ self.assertEqual(d.value, 0)
+
+ d.forward()
+ self.assertEqual(d.value, 1)
+
+ d.backward()
+ self.assertEqual(d.value, -1)
+
+ d.value = 0.5
+ self.assertAlmostEqual(d.value, 0.5, places=2)
+
+ d.value = -0.5
+ self.assertAlmostEqual(d.value, -0.5, places=2)
+
+ d.forward(1, t=0.5)
+ values = log_device_values(d, 0.6)
+ self.assertEqual(values, [1,0])
+ self.assertEqual(d.value, 0)
+
+ d.backward(1, t=0.5)
+ values = log_device_values(d, 0.6)
+ self.assertEqual(values, [-1,0])
+ self.assertEqual(d.value, 0)
+
+ d.close()
+
+ def test_motor_alt_values(self):
+ d = Motor(1,2,pwm=False)
+
+ d.value = 0.5
+ self.assertEqual(d.value, 1)
+
+ d.value = -0.5
+ self.assertEqual(d.value, -1)
+
+ d.value = 0
+ self.assertEqual(d.value, 0)
+
+ d.close()
+
+ def test_robot(self):
+ d = Robot(left=(1,2), right=(3,4))
+
+ d.forward()
+ self.assertEqual(d.value, (1,1))
+
+ d.left()
+ self.assertEqual(d.value, (-1,1))
+
+ d.right()
+ self.assertEqual(d.value, (1,-1))
+
+ d.value = (0.5, -0.5)
+ self.assertAlmostEqual(d.left_motor.value, 0.5, places=2)
+ self.assertAlmostEqual(d.right_motor.value, -0.5, places=2)
+
+ d.stop()
+ self.assertEqual(d.value, (0,0))
+
+ d.close()
def test_LED_factory(self):
d = LED(1)
@@ -242,6 +316,10 @@ 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)
@@ -456,6 +534,6 @@ def test_pico_temp_sensor(self):
self.assertIsInstance(pico_temp_sensor, TemperatureSensor)
self.assertEqual(pico_temp_sensor.pin, 4)
self.assertIsNotNone(pico_temp_sensor.temp)
-
+
unittest.main()