Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3efd522

Browse files
committed
Merge pull request gpiozero#184 from waveform80/ultrasonics
Fix gpiozero#114
2 parents 81123f8 + 83fb6ae commit 3efd522

File tree

8 files changed

+287
-4
lines changed

8 files changed

+287
-4
lines changed

docs/api_exc.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ so you can still do::
4848

4949
.. autoexception:: GPIOBadQueueLen
5050

51+
.. autoexception:: GPIOBadSampleWait
52+
5153
.. autoexception:: InputDeviceError
5254

5355
.. autoexception:: OutputDeviceError

docs/api_input.rst

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ Button
2020
:members: wait_for_press, wait_for_release, pin, is_pressed, pull_up, when_pressed, when_released
2121

2222

23-
Motion Sensor (PIR)
24-
===================
23+
Motion Sensor (D-SUN PIR)
24+
=========================
2525

2626
.. autoclass:: MotionSensor(pin, queue_len=1, sample_rate=10, threshold=0.5, partial=False)
2727
:members: wait_for_motion, wait_for_no_motion, pin, motion_detected, when_motion, when_no_motion
@@ -33,6 +33,14 @@ Light Sensor (LDR)
3333
.. autoclass:: LightSensor(pin, queue_len=5, charge_time_limit=0.01, threshold=0.1, partial=False)
3434
:members: wait_for_light, wait_for_dark, pin, light_detected, when_light, when_dark
3535

36+
37+
Distance Sensor (HC-SR04)
38+
=========================
39+
40+
.. autoclass:: DistanceSensor(echo, trigger, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False)
41+
:members: wait_for_in_range, wait_for_out_of_range, trigger, echo, when_in_range, when_out_of_range, max_distance, distance, threshold_distance
42+
43+
3644
Analog to Digital Converters (ADC)
3745
==================================
3846

docs/recipes.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,36 @@ level::
443443
pause()
444444

445445

446+
Distance sensor
447+
===============
448+
449+
.. IMAGE TBD
450+
451+
Have a :class:`DistanceSensor` detect the distance to the nearest object::
452+
453+
from gpiozero import DistanceSensor
454+
from time import sleep
455+
456+
sensor = DistanceSensor(23, 24)
457+
458+
while True:
459+
print('Distance to nearest object is', sensor.distance, 'm')
460+
sleep(1)
461+
462+
Run a function when something gets near the sensor::
463+
464+
from gpiozero import DistanceSensor, LED
465+
from signal import pause
466+
467+
sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2)
468+
led = LED(16)
469+
470+
sensor.when_in_range = led.on
471+
sensor.when_out_of_range = led.off
472+
473+
pause()
474+
475+
446476
Motors
447477
======
448478

@@ -480,6 +510,19 @@ Make a :class:`Robot` drive around in (roughly) a square::
480510
robot.right()
481511
sleep(1)
482512

513+
Make a robot with a distance sensor that runs away when things get within
514+
20cm of it::
515+
516+
from gpiozero import Robot, DistanceSensor
517+
from signal import pause
518+
519+
sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2)
520+
robot = Robot(left=(4, 14), right=(17, 18))
521+
522+
sensor.when_in_range = robot.backward
523+
sensor.when_out_of_range = robot.stop
524+
pause()
525+
483526

484527
Button controlled robot
485528
=======================

gpiozero/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
GPIOPinInUse,
1717
GPIOPinMissing,
1818
GPIOBadQueueLen,
19+
GPIOBadSampleWait,
1920
InputDeviceError,
2021
OutputDeviceError,
2122
OutputDeviceBadValue,
@@ -48,6 +49,7 @@
4849
LineSensor,
4950
MotionSensor,
5051
LightSensor,
52+
DistanceSensor,
5153
AnalogInputDevice,
5254
MCP3008,
5355
MCP3004,

gpiozero/compat.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# vim: set fileencoding=utf-8:
2+
13
from __future__ import (
24
unicode_literals,
35
absolute_import,
@@ -25,3 +27,27 @@ def isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
2527
(diff <= abs(rel_tol * a)) or
2628
(diff <= abs_tol)
2729
)
30+
31+
32+
# Backported from py3.4
33+
def mean(data):
34+
if iter(data) is data:
35+
data = list(data)
36+
n = len(data)
37+
if not n:
38+
raise ValueError('cannot calculate mean of empty data')
39+
return sum(data) / n
40+
41+
42+
# Backported from py3.4
43+
def median(data):
44+
data = sorted(data)
45+
n = len(data)
46+
if not n:
47+
raise ValueError('cannot calculate median of empty data')
48+
elif n % 2:
49+
return data[n // 2]
50+
else:
51+
i = n // 2
52+
return (data[n - 1] + data[n]) / 2
53+

gpiozero/devices.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@
1212
from threading import Thread, Event, RLock
1313
from collections import deque
1414
from types import FunctionType
15+
try:
16+
from statistics import median, mean
17+
except ImportError:
18+
from .compat import median, mean
1519

1620
from .exc import (
1721
GPIOPinMissing,
1822
GPIOPinInUse,
1923
GPIODeviceClosed,
2024
GPIOBadQueueLen,
25+
GPIOBadSampleWait,
2126
)
2227

2328
# Get a pin implementation to use as the default; we prefer RPi.GPIO's here
@@ -344,23 +349,29 @@ def join(self):
344349

345350

346351
class GPIOQueue(GPIOThread):
347-
def __init__(self, parent, queue_len=5, sample_wait=0.0, partial=False):
352+
def __init__(
353+
self, parent, queue_len=5, sample_wait=0.0, partial=False,
354+
average=median):
348355
assert isinstance(parent, GPIODevice)
356+
assert callable(average)
349357
super(GPIOQueue, self).__init__(target=self.fill)
350358
if queue_len < 1:
351359
raise GPIOBadQueueLen('queue_len must be at least one')
360+
if sample_wait < 0:
361+
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
352362
self.queue = deque(maxlen=queue_len)
353363
self.partial = partial
354364
self.sample_wait = sample_wait
355365
self.full = Event()
356366
self.parent = weakref.proxy(parent)
367+
self.average = average
357368

358369
@property
359370
def value(self):
360371
if not self.partial:
361372
self.full.wait()
362373
try:
363-
return sum(self.queue) / len(self.queue)
374+
return self.average(self.queue)
364375
except ZeroDivisionError:
365376
# No data == inactive value
366377
return 0.0

gpiozero/exc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class GPIOPinMissing(GPIODeviceError, ValueError):
2828
class GPIOBadQueueLen(GPIODeviceError, ValueError):
2929
"Error raised when non-positive queue length is specified"
3030

31+
class GPIOBadSampleWait(GPIODeviceError, ValueError):
32+
"Error raised when a negative sampling wait period is specified"
33+
3134
class InputDeviceError(GPIODeviceError):
3235
"Base class for errors specific to the InputDevice hierarchy"
3336

gpiozero/input_devices.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,194 @@ def _read(self):
572572
LightSensor.wait_for_dark = LightSensor.wait_for_inactive
573573

574574

575+
class DistanceSensor(SmoothedInputDevice):
576+
"""
577+
Extends :class:`SmoothedInputDevice` and represents an HC-SR04 ultrasonic
578+
distance sensor, as found in the `CamJam #3 EduKit`_.
579+
580+
The distance sensor requires two GPIO pins: one for the *trigger* (marked
581+
TRIG on the sensor) and another for the *echo* (marked ECHO on the sensor).
582+
However, a voltage divider is required to ensure the 5V from the ECHO pin
583+
doesn't damage the Pi. Wire your sensor according to the following
584+
instructions:
585+
586+
1. Connect the GND pin of the sensor to a ground pin on the Pi.
587+
588+
2. Connect the TRIG pin of the sensor a GPIO pin.
589+
590+
3. Connect a 330Ω resistor from the ECHO pin of the sensor to a different
591+
GPIO pin.
592+
593+
4. Connect a 470Ω resistor from ground to the ECHO GPIO pin. This forms
594+
the required voltage divider.
595+
596+
5. Finally, connect the VCC pin of the sensor to a 5V pin on the Pi.
597+
598+
The following code will periodically report the distance measured by the
599+
sensor in cm assuming the TRIG pin is connected to GPIO17, and the ECHO
600+
pin to GPIO18::
601+
602+
from gpiozero import DistanceSensor
603+
from time import sleep
604+
605+
sensor = DistanceSensor(18, 17)
606+
while True:
607+
print('Distance: ', sensor.distance * 100)
608+
sleep(1)
609+
610+
:param int echo:
611+
The GPIO pin which the ECHO pin is attached to. See :doc:`notes` for
612+
valid pin numbers.
613+
614+
:param int trigger:
615+
The GPIO pin which the TRIG pin is attached to. See :doc:`notes` for
616+
valid pin numbers.
617+
618+
:param int queue_len:
619+
The length of the queue used to store values read from the sensor.
620+
This defaults to 30.
621+
622+
:param float max_distance:
623+
The :attr:`value` attribute reports a normalized value between 0 (too
624+
close to measure) and 1 (maximum distance). This parameter specifies
625+
the maximum distance expected in meters. This defaults to 1.
626+
627+
:param float threshold_distance:
628+
Defaults to 0.3. This is the distance (in meters) that will trigger the
629+
``in_range`` and ``out_of_range`` events when crossed.
630+
631+
:param bool partial:
632+
When ``False`` (the default), the object will not return a value for
633+
:attr:`~SmoothedInputDevice.is_active` until the internal queue has
634+
filled with values. Only set this to ``True`` if you require values
635+
immediately after object construction.
636+
637+
.. _CamJam #3 EduKit: http://camjam.me/?page_id=1035
638+
"""
639+
def __init__(
640+
self, echo=None, trigger=None, queue_len=30, max_distance=1,
641+
threshold_distance=0.3, partial=False):
642+
if not (max_distance > 0):
643+
raise ValueError('invalid maximum distance (must be positive)')
644+
self._trigger = None
645+
super(DistanceSensor, self).__init__(
646+
echo, pull_up=False, threshold=threshold_distance / max_distance,
647+
queue_len=queue_len, sample_wait=0.0, partial=partial
648+
)
649+
try:
650+
self.speed_of_sound = 343.26 # m/s
651+
self._max_distance = max_distance
652+
self._trigger = GPIODevice(trigger)
653+
self._echo = Event()
654+
self._trigger.pin.function = 'output'
655+
self._trigger.pin.state = False
656+
self.pin.edges = 'both'
657+
self.pin.bounce = None
658+
self.pin.when_changed = self._echo.set
659+
self._queue.start()
660+
except:
661+
self.close()
662+
raise
663+
664+
def close(self):
665+
try:
666+
self._trigger.close()
667+
except AttributeError:
668+
if self._trigger is not None:
669+
raise
670+
else:
671+
self._trigger = None
672+
super(DistanceSensor, self).close()
673+
674+
@property
675+
def max_distance(self):
676+
"""
677+
The maximum distance that the sensor will measure in meters. This value
678+
is specified in the constructor and is used to provide the scaling
679+
for the :attr:`value` attribute. When :attr:`distance` is equal to
680+
:attr:`max_distance`, :attr:`value` will be 1.
681+
"""
682+
return self._max_distance
683+
684+
@max_distance.setter
685+
def max_distance(self, value):
686+
if not (value > 0):
687+
raise ValueError('invalid maximum distance (must be positive)')
688+
t = self.threshold_distance
689+
self._max_distance = value
690+
self.threshold_distance = t
691+
692+
@property
693+
def threshold_distance(self):
694+
"""
695+
The distance, measured in meters, that will trigger the
696+
:attr:`when_in_range` and :attr:`when_out_of_range` events when
697+
crossed. This is simply a meter-scaled variant of the usual
698+
:attr:`threshold` attribute.
699+
"""
700+
return self.threshold * self.max_distance
701+
702+
@threshold_distance.setter
703+
def threshold_distance(self, value):
704+
self.threshold = value / self.max_distance
705+
706+
@property
707+
def distance(self):
708+
"""
709+
Returns the current distance measured by the sensor in meters. Note
710+
that this property will have a value between 0 and
711+
:attr:`max_distance`.
712+
"""
713+
return self.value * self._max_distance
714+
715+
@property
716+
def trigger(self):
717+
"""
718+
Returns the :class:`Pin` that the sensor's trigger is connected to.
719+
"""
720+
return self._trigger.pin
721+
722+
@property
723+
def echo(self):
724+
"""
725+
Returns the :class:`Pin` that the sensor's echo is connected to. This
726+
is simply an alias for the usual :attr:`pin` attribute.
727+
"""
728+
return self.pin
729+
730+
def _read(self):
731+
# Make sure the echo event is clear
732+
self._echo.clear()
733+
# Fire the trigger
734+
self._trigger.pin.state = True
735+
sleep(0.00001)
736+
self._trigger.pin.state = False
737+
# Wait up to 1 second for the echo pin to rise
738+
if self._echo.wait(1):
739+
start = time()
740+
self._echo.clear()
741+
# Wait up to 40ms for the echo pin to fall (35ms is maximum pulse
742+
# time so any longer means something's gone wrong). Calculate
743+
# distance as time for echo multiplied by speed of sound divided by
744+
# two to compensate for travel to and from the reflector
745+
if self._echo.wait(0.04):
746+
distance = (time() - start) * self.speed_of_sound / 2.0
747+
return min(1.0, distance / self._max_distance)
748+
else:
749+
# If we only saw one edge it means we missed the echo because
750+
# it was too fast; report minimum distance
751+
return 0.0
752+
else:
753+
# The echo pin never rose or fell; something's gone horribly
754+
# wrong (XXX raise a warning?)
755+
return 1.0
756+
757+
DistanceSensor.when_out_of_range = DistanceSensor.when_activated
758+
DistanceSensor.when_in_range = DistanceSensor.when_deactivated
759+
DistanceSensor.wait_for_out_of_range = DistanceSensor.wait_for_active
760+
DistanceSensor.wait_for_in_range = DistanceSensor.wait_for_inactive
761+
762+
575763
class AnalogInputDevice(CompositeDevice):
576764
"""
577765
Represents an analog input device connected to SPI (serial interface).

0 commit comments

Comments
 (0)