Skip to content
37 changes: 34 additions & 3 deletions adafruit_debouncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
====================================================

Debounces an arbitrary predicate function (typically created as a lambda) of 0 arguments.
Since a very common use is debouncing a digital input pin, the initializer accepts a pin number
Since a very common use is debouncing a digital input pin, the initializer accepts a DigitalInOut object
instead of a lambda.

* Author(s): Dave Astels
Expand All @@ -34,6 +34,16 @@

**Hardware:**

Not all hardware / CircuitPython combinations are capable of running the
debouncer correctly for an extended length of time. If this line works
on your microcontroller, then the debouncer should work forever:

``from time import monotonic_ns``

If it gives an ImportError, then the time values available in Python become
less accurate over the days, and the debouncer will take longer to react to
button presses.

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
Expand All @@ -53,6 +63,17 @@
_UNSTABLE_STATE = const(0x02)
_CHANGED_STATE = const(0x04)


# Find out whether the current CircuitPython supports time.monotonic_ns(),
# which doesn't have the accuracy limitation.
if hasattr(time, 'monotonic_ns'):
MONOTONIC_UNITS_PER_SEC = 1_000_000_000
MONOTONIC_TIME = time.monotonic_ns
else:
MONOTONIC_UNITS_PER_SEC = 1
MONOTONIC_TIME = time.monotonic


class Debouncer(object):
"""Debounce an input pin or an arbitrary predicate"""

Expand Down Expand Up @@ -90,19 +111,29 @@ def _get_state(self, bits):

def update(self):
"""Update the debouncer state. MUST be called frequently"""
now = time.monotonic()
now = MONOTONIC_TIME()
self._unset_state(_CHANGED_STATE)
current_state = self.function()
if current_state != self._get_state(_UNSTABLE_STATE):
self.previous_time = now
self._toggle_state(_UNSTABLE_STATE)
else:
if now - self.previous_time >= self.interval:
if now - self.previous_time >= self._interval:
if current_state != self._get_state(_DEBOUNCED_STATE):
self.previous_time = now
self._toggle_state(_DEBOUNCED_STATE)
self._set_state(_CHANGED_STATE)

@property
def interval(self):
"""The debounce delay, in seconds"""
return self._interval / MONOTONIC_UNITS_PER_SEC


@interval.setter
def interval(self, new_interval_s):
self._interval = new_interval_s * MONOTONIC_UNITS_PER_SEC


@property
def value(self):
Expand Down
116 changes: 116 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import sys
import time
import adafruit_debouncer


def _true():
return True
def _false():
return False


def assertEqual(a, b):
assert a == b, "Want %r, got %r" % (a, b)


def test_simple():
db = adafruit_debouncer.Debouncer(_false)
assertEqual(db.value, False)

db.function = _true
db.update()
assertEqual(db.value, False)
time.sleep(0.02)
db.update()
assertEqual(db.value, True)
assertEqual(db.rose, True)
assertEqual(db.fell, False)

db.function = _false
db.update()
assertEqual(db.value, True)
assertEqual(db.fell, False)
assertEqual(db.rose, False)
time.sleep(0.02)
db.update()
assertEqual(db.value, False)
assertEqual(db.rose, False)
assertEqual(db.fell, True)


def test_interval_is_the_same():
db = adafruit_debouncer.Debouncer(_false, interval=0.25)
assertEqual(db.value, False)
db.update()
db.function = _true
db.update()

time.sleep(0.1) # longer than default interval
db.update()
assertEqual(db.value, False)

time.sleep(0.2) # 0.1 + 0.2 > 0.25
db.update()
assertEqual(db.value, True)
assertEqual(db.rose, True)
assertEqual(db.interval, 0.25)


def test_setting_interval():
# Check that setting the interval does change the time the debouncer waits
db = adafruit_debouncer.Debouncer(_false, interval=0.01)
db.update()

# set the interval to a longer time, sleep for a time between
# the two interval settings, and assert that the value hasn't changed.

db.function = _true
db.interval = 0.2
db.update()
assert db.interval - 0.2 < 0.00001, "interval is not consistent"
time.sleep(0.11)
db.update()

assertEqual(db.value, False)
assertEqual(db.rose, False)
assertEqual(db.fell, False)

# and then once the whole time has passed make sure it did change
time.sleep(0.11)
db.update()
assertEqual(db.value, True)
assertEqual(db.rose, True)
assertEqual(db.fell, False)


def run():
passes = 0
fails = 0
for name, test in locals().items():
if name.startswith('test_') and callable(test):
try:
print()
print(name)
test()
print("PASS")
passes += 1
except Exception as e:
sys.print_exception(e)
print("FAIL")
fails += 1

print(passes, "passed,", fails, "failed")
if passes and not fails:
print(r"""
________
< YATTA! >
--------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||""")


if __name__ == '__main__':
run()