Skip to content

Switch to time.monotonic_ns() when it's available #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 16, 2020
65 changes: 48 additions & 17 deletions adafruit_debouncer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
`adafruit_debouncer`
====================================================

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
instead of a lambda.
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 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 @@ -52,6 +62,15 @@
_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"):
TICKS_PER_SEC = 1_000_000_000
MONOTONIC_TICKS = time.monotonic_ns
else:
TICKS_PER_SEC = 1
MONOTONIC_TICKS = time.monotonic


class Debouncer:
"""Debounce an input pin or an arbitrary predicate"""
Expand All @@ -68,10 +87,13 @@ def __init__(self, io_or_predicate, interval=0.010):
self.function = io_or_predicate
if self.function():
self._set_state(_DEBOUNCED_STATE | _UNSTABLE_STATE)
self.previous_time = 0
self.interval = interval
self._previous_state_duration = 0
self._state_changed_time = 0
self._last_bounce_ticks = 0
self._last_duration_ticks = 0
self._state_changed_ticks = 0

# Could use the .interval setter, but pylint prefers that we explicitly
# set the real underlying attribute:
self._interval_ticks = interval * TICKS_PER_SEC

def _set_state(self, bits):
self.state |= bits
Expand All @@ -87,20 +109,29 @@ def _get_state(self, bits):

def update(self):
"""Update the debouncer state. MUST be called frequently"""
now = time.monotonic()
now_ticks = MONOTONIC_TICKS()
self._unset_state(_CHANGED_STATE)
current_state = self.function()
if current_state != self._get_state(_UNSTABLE_STATE):
self.previous_time = now
self._last_bounce_ticks = now_ticks
self._toggle_state(_UNSTABLE_STATE)
else:
if now - self.previous_time >= self.interval:
if now_ticks - self._last_bounce_ticks >= self._interval_ticks:
if current_state != self._get_state(_DEBOUNCED_STATE):
self.previous_time = now
self._last_bounce_ticks = now_ticks
self._toggle_state(_DEBOUNCED_STATE)
self._set_state(_CHANGED_STATE)
self._previous_state_duration = now - self._state_changed_time
self._state_changed_time = now
self._last_duration_ticks = now_ticks - self._state_changed_ticks
self._state_changed_ticks = now_ticks

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

@interval.setter
def interval(self, new_interval_s):
self._interval_ticks = new_interval_s * TICKS_PER_SEC

@property
def value(self):
Expand All @@ -121,10 +152,10 @@ def fell(self):

@property
def last_duration(self):
"""Return the amount of time the state was stable prior to the most recent transition."""
return self._previous_state_duration
"""Return the number of seconds the state was stable prior to the most recent transition."""
return self._last_duration_ticks / TICKS_PER_SEC

@property
def current_duration(self):
"""Return the time since the most recent transition."""
return time.monotonic() - self._state_changed_time
"""Return the number of seconds since the most recent transition."""
return (MONOTONIC_TICKS() - self._state_changed_ticks) / TICKS_PER_SEC
151 changes: 151 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
How to use this test file:

Copy adafruit_debouncer's dependencies to lib/ on your circuitpython device.
Copy adafruit_debouncer.py to / on the device
Copy this tests.py file to /main.py on the device
Connect to the serial terminal (e.g. sudo screen /dev/ttyACM0 115200)
Press Ctrl-D, if needed to start the tests running
"""
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_back_and_forth():
# Start false
db = adafruit_debouncer.Debouncer(_false)
assertEqual(db.value, False)

# Set the raw state to true, update, and make sure the debounced
# state has not changed yet:
db.function = _true
db.update()
assertEqual(db.value, False)
assert not db.last_duration, "There was no previous interval??"

# Sleep longer than the debounce interval, so state can change:
time.sleep(0.02)
db.update()
assert db.last_duration # is actually duration between powerup and now
assertEqual(db.value, True)
assertEqual(db.rose, True)
assertEqual(db.fell, False)
# Duration since last change has only been long enough to run these
# asserts, which should be well under 1/10 second
assert db.current_duration < 0.1, "Unit error? %d" % db.current_duration

# Set raw state back to false, make sure it's not instantly reflected,
# then wait and make sure it IS reflected after the interval has passed.
db.function = _false
db.update()
assertEqual(db.value, True)
assertEqual(db.fell, False)
assertEqual(db.rose, False)
time.sleep(0.02)
assert 0.019 < db.current_duration <= 1, (
"Unit error? sleep .02 -> duration %d" % db.current_duration
)
db.update()
assertEqual(db.value, False)
assertEqual(db.rose, False)
assertEqual(db.fell, True)

assert 0 < db.current_duration <= 0.1, (
"Unit error? time to run asserts %d" % db.current_duration
)
assert 0 < db.last_duration < 0.1, (
"Unit error? Last dur should be ~.02, is %d" % db.last_duration
)


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()