Merge pull request #10 from tgs/use-monotonic-ns
Switch to time.monotonic_ns() when it's available
This commit is contained in:
commit
d31993ec02
2 changed files with 199 additions and 17 deletions
|
|
@ -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
|
||||
|
||||
|
|
@ -34,6 +34,16 @@ Implementation Notes
|
|||
|
||||
**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:
|
||||
|
|
@ -52,6 +62,15 @@ _DEBOUNCED_STATE = const(0x01)
|
|||
_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"""
|
||||
|
|
@ -68,10 +87,13 @@ class Debouncer:
|
|||
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
|
||||
|
|
@ -87,20 +109,29 @@ class Debouncer:
|
|||
|
||||
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):
|
||||
|
|
@ -121,10 +152,10 @@ class Debouncer:
|
|||
|
||||
@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
tests/tests.py
Normal file
151
tests/tests.py
Normal file
|
|
@ -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()
|
||||
Loading…
Reference in a new issue