mirror of
https://github.com/KevinMidboe/python-gpiozero.git
synced 2025-10-29 17:50:37 +00:00
@@ -16,6 +16,7 @@ from .exc import (
|
||||
GPIOPinInUse,
|
||||
GPIOPinMissing,
|
||||
GPIOBadQueueLen,
|
||||
GPIOBadSampleWait,
|
||||
InputDeviceError,
|
||||
OutputDeviceError,
|
||||
OutputDeviceBadValue,
|
||||
@@ -48,6 +49,7 @@ from .input_devices import (
|
||||
LineSensor,
|
||||
MotionSensor,
|
||||
LightSensor,
|
||||
DistanceSensor,
|
||||
AnalogInputDevice,
|
||||
MCP3008,
|
||||
MCP3004,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# vim: set fileencoding=utf-8:
|
||||
|
||||
from __future__ import (
|
||||
unicode_literals,
|
||||
absolute_import,
|
||||
@@ -25,3 +27,27 @@ def isclose(a, b, rel_tol=1e-9, abs_tol=0.0):
|
||||
(diff <= abs(rel_tol * a)) or
|
||||
(diff <= abs_tol)
|
||||
)
|
||||
|
||||
|
||||
# Backported from py3.4
|
||||
def mean(data):
|
||||
if iter(data) is data:
|
||||
data = list(data)
|
||||
n = len(data)
|
||||
if not n:
|
||||
raise ValueError('cannot calculate mean of empty data')
|
||||
return sum(data) / n
|
||||
|
||||
|
||||
# Backported from py3.4
|
||||
def median(data):
|
||||
data = sorted(data)
|
||||
n = len(data)
|
||||
if not n:
|
||||
raise ValueError('cannot calculate median of empty data')
|
||||
elif n % 2:
|
||||
return data[n // 2]
|
||||
else:
|
||||
i = n // 2
|
||||
return (data[n - 1] + data[n]) / 2
|
||||
|
||||
|
||||
@@ -12,12 +12,17 @@ import weakref
|
||||
from threading import Thread, Event, RLock
|
||||
from collections import deque
|
||||
from types import FunctionType
|
||||
try:
|
||||
from statistics import median, mean
|
||||
except ImportError:
|
||||
from .compat import median, mean
|
||||
|
||||
from .exc import (
|
||||
GPIOPinMissing,
|
||||
GPIOPinInUse,
|
||||
GPIODeviceClosed,
|
||||
GPIOBadQueueLen,
|
||||
GPIOBadSampleWait,
|
||||
)
|
||||
|
||||
# Get a pin implementation to use as the default; we prefer RPi.GPIO's here
|
||||
@@ -344,23 +349,29 @@ class GPIOThread(Thread):
|
||||
|
||||
|
||||
class GPIOQueue(GPIOThread):
|
||||
def __init__(self, parent, queue_len=5, sample_wait=0.0, partial=False):
|
||||
def __init__(
|
||||
self, parent, queue_len=5, sample_wait=0.0, partial=False,
|
||||
average=median):
|
||||
assert isinstance(parent, GPIODevice)
|
||||
assert callable(average)
|
||||
super(GPIOQueue, self).__init__(target=self.fill)
|
||||
if queue_len < 1:
|
||||
raise GPIOBadQueueLen('queue_len must be at least one')
|
||||
if sample_wait < 0:
|
||||
raise GPIOBadSampleWait('sample_wait must be 0 or greater')
|
||||
self.queue = deque(maxlen=queue_len)
|
||||
self.partial = partial
|
||||
self.sample_wait = sample_wait
|
||||
self.full = Event()
|
||||
self.parent = weakref.proxy(parent)
|
||||
self.average = average
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
if not self.partial:
|
||||
self.full.wait()
|
||||
try:
|
||||
return sum(self.queue) / len(self.queue)
|
||||
return self.average(self.queue)
|
||||
except ZeroDivisionError:
|
||||
# No data == inactive value
|
||||
return 0.0
|
||||
|
||||
@@ -28,6 +28,9 @@ class GPIOPinMissing(GPIODeviceError, ValueError):
|
||||
class GPIOBadQueueLen(GPIODeviceError, ValueError):
|
||||
"Error raised when non-positive queue length is specified"
|
||||
|
||||
class GPIOBadSampleWait(GPIODeviceError, ValueError):
|
||||
"Error raised when a negative sampling wait period is specified"
|
||||
|
||||
class InputDeviceError(GPIODeviceError):
|
||||
"Base class for errors specific to the InputDevice hierarchy"
|
||||
|
||||
|
||||
@@ -572,6 +572,194 @@ LightSensor.wait_for_light = LightSensor.wait_for_active
|
||||
LightSensor.wait_for_dark = LightSensor.wait_for_inactive
|
||||
|
||||
|
||||
class DistanceSensor(SmoothedInputDevice):
|
||||
"""
|
||||
Extends :class:`SmoothedInputDevice` and represents an HC-SR04 ultrasonic
|
||||
distance sensor, as found in the `CamJam #3 EduKit`_.
|
||||
|
||||
The distance sensor requires two GPIO pins: one for the *trigger* (marked
|
||||
TRIG on the sensor) and another for the *echo* (marked ECHO on the sensor).
|
||||
However, a voltage divider is required to ensure the 5V from the ECHO pin
|
||||
doesn't damage the Pi. Wire your sensor according to the following
|
||||
instructions:
|
||||
|
||||
1. Connect the GND pin of the sensor to a ground pin on the Pi.
|
||||
|
||||
2. Connect the TRIG pin of the sensor a GPIO pin.
|
||||
|
||||
3. Connect a 330Ω resistor from the ECHO pin of the sensor to a different
|
||||
GPIO pin.
|
||||
|
||||
4. Connect a 470Ω resistor from ground to the ECHO GPIO pin. This forms
|
||||
the required voltage divider.
|
||||
|
||||
5. Finally, connect the VCC pin of the sensor to a 5V pin on the Pi.
|
||||
|
||||
The following code will periodically report the distance measured by the
|
||||
sensor in cm assuming the TRIG pin is connected to GPIO17, and the ECHO
|
||||
pin to GPIO18::
|
||||
|
||||
from gpiozero import DistanceSensor
|
||||
from time import sleep
|
||||
|
||||
sensor = DistanceSensor(18, 17)
|
||||
while True:
|
||||
print('Distance: ', sensor.distance * 100)
|
||||
sleep(1)
|
||||
|
||||
:param int echo:
|
||||
The GPIO pin which the ECHO pin is attached to. See :doc:`notes` for
|
||||
valid pin numbers.
|
||||
|
||||
:param int trigger:
|
||||
The GPIO pin which the TRIG pin is attached to. See :doc:`notes` for
|
||||
valid pin numbers.
|
||||
|
||||
:param int queue_len:
|
||||
The length of the queue used to store values read from the sensor.
|
||||
This defaults to 30.
|
||||
|
||||
:param float max_distance:
|
||||
The :attr:`value` attribute reports a normalized value between 0 (too
|
||||
close to measure) and 1 (maximum distance). This parameter specifies
|
||||
the maximum distance expected in meters. This defaults to 1.
|
||||
|
||||
:param float threshold_distance:
|
||||
Defaults to 0.3. This is the distance (in meters) that will trigger the
|
||||
``in_range`` and ``out_of_range`` events when crossed.
|
||||
|
||||
:param bool partial:
|
||||
When ``False`` (the default), the object will not return a value for
|
||||
:attr:`~SmoothedInputDevice.is_active` until the internal queue has
|
||||
filled with values. Only set this to ``True`` if you require values
|
||||
immediately after object construction.
|
||||
|
||||
.. _CamJam #3 EduKit: http://camjam.me/?page_id=1035
|
||||
"""
|
||||
def __init__(
|
||||
self, echo=None, trigger=None, queue_len=30, max_distance=1,
|
||||
threshold_distance=0.3, partial=False):
|
||||
if not (max_distance > 0):
|
||||
raise ValueError('invalid maximum distance (must be positive)')
|
||||
self._trigger = None
|
||||
super(DistanceSensor, self).__init__(
|
||||
echo, pull_up=False, threshold=threshold_distance / max_distance,
|
||||
queue_len=queue_len, sample_wait=0.0, partial=partial
|
||||
)
|
||||
try:
|
||||
self.speed_of_sound = 343.26 # m/s
|
||||
self._max_distance = max_distance
|
||||
self._trigger = GPIODevice(trigger)
|
||||
self._echo = Event()
|
||||
self._trigger.pin.function = 'output'
|
||||
self._trigger.pin.state = False
|
||||
self.pin.edges = 'both'
|
||||
self.pin.bounce = None
|
||||
self.pin.when_changed = self._echo.set
|
||||
self._queue.start()
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._trigger.close()
|
||||
except AttributeError:
|
||||
if self._trigger is not None:
|
||||
raise
|
||||
else:
|
||||
self._trigger = None
|
||||
super(DistanceSensor, self).close()
|
||||
|
||||
@property
|
||||
def max_distance(self):
|
||||
"""
|
||||
The maximum distance that the sensor will measure in meters. This value
|
||||
is specified in the constructor and is used to provide the scaling
|
||||
for the :attr:`value` attribute. When :attr:`distance` is equal to
|
||||
:attr:`max_distance`, :attr:`value` will be 1.
|
||||
"""
|
||||
return self._max_distance
|
||||
|
||||
@max_distance.setter
|
||||
def max_distance(self, value):
|
||||
if not (value > 0):
|
||||
raise ValueError('invalid maximum distance (must be positive)')
|
||||
t = self.threshold_distance
|
||||
self._max_distance = value
|
||||
self.threshold_distance = t
|
||||
|
||||
@property
|
||||
def threshold_distance(self):
|
||||
"""
|
||||
The distance, measured in meters, that will trigger the
|
||||
:attr:`when_in_range` and :attr:`when_out_of_range` events when
|
||||
crossed. This is simply a meter-scaled variant of the usual
|
||||
:attr:`threshold` attribute.
|
||||
"""
|
||||
return self.threshold * self.max_distance
|
||||
|
||||
@threshold_distance.setter
|
||||
def threshold_distance(self, value):
|
||||
self.threshold = value / self.max_distance
|
||||
|
||||
@property
|
||||
def distance(self):
|
||||
"""
|
||||
Returns the current distance measured by the sensor in meters. Note
|
||||
that this property will have a value between 0 and
|
||||
:attr:`max_distance`.
|
||||
"""
|
||||
return self.value * self._max_distance
|
||||
|
||||
@property
|
||||
def trigger(self):
|
||||
"""
|
||||
Returns the :class:`Pin` that the sensor's trigger is connected to.
|
||||
"""
|
||||
return self._trigger.pin
|
||||
|
||||
@property
|
||||
def echo(self):
|
||||
"""
|
||||
Returns the :class:`Pin` that the sensor's echo is connected to. This
|
||||
is simply an alias for the usual :attr:`pin` attribute.
|
||||
"""
|
||||
return self.pin
|
||||
|
||||
def _read(self):
|
||||
# Make sure the echo event is clear
|
||||
self._echo.clear()
|
||||
# Fire the trigger
|
||||
self._trigger.pin.state = True
|
||||
sleep(0.00001)
|
||||
self._trigger.pin.state = False
|
||||
# Wait up to 1 second for the echo pin to rise
|
||||
if self._echo.wait(1):
|
||||
start = time()
|
||||
self._echo.clear()
|
||||
# Wait up to 40ms for the echo pin to fall (35ms is maximum pulse
|
||||
# time so any longer means something's gone wrong). Calculate
|
||||
# distance as time for echo multiplied by speed of sound divided by
|
||||
# two to compensate for travel to and from the reflector
|
||||
if self._echo.wait(0.04):
|
||||
distance = (time() - start) * self.speed_of_sound / 2.0
|
||||
return min(1.0, distance / self._max_distance)
|
||||
else:
|
||||
# If we only saw one edge it means we missed the echo because
|
||||
# it was too fast; report minimum distance
|
||||
return 0.0
|
||||
else:
|
||||
# The echo pin never rose or fell; something's gone horribly
|
||||
# wrong (XXX raise a warning?)
|
||||
return 1.0
|
||||
|
||||
DistanceSensor.when_out_of_range = DistanceSensor.when_activated
|
||||
DistanceSensor.when_in_range = DistanceSensor.when_deactivated
|
||||
DistanceSensor.wait_for_out_of_range = DistanceSensor.wait_for_active
|
||||
DistanceSensor.wait_for_in_range = DistanceSensor.wait_for_inactive
|
||||
|
||||
|
||||
class AnalogInputDevice(CompositeDevice):
|
||||
"""
|
||||
Represents an analog input device connected to SPI (serial interface).
|
||||
|
||||
Reference in New Issue
Block a user