Skip to content

Commit d31993e

Browse files
authored
Merge pull request #10 from tgs/use-monotonic-ns
Switch to time.monotonic_ns() when it's available
2 parents 793f545 + 481d615 commit d31993e

File tree

2 files changed

+199
-17
lines changed

2 files changed

+199
-17
lines changed

adafruit_debouncer.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
`adafruit_debouncer`
2424
====================================================
2525
26-
Debounces an arbitrary predicate function (typically created as a lambda) of 0 arguments.
27-
Since a very common use is debouncing a digital input pin, the initializer accepts a pin number
28-
instead of a lambda.
26+
Debounces an arbitrary predicate function (typically created as a lambda) of 0
27+
arguments. Since a very common use is debouncing a digital input pin, the
28+
initializer accepts a DigitalInOut object instead of a lambda.
2929
3030
* Author(s): Dave Astels
3131
@@ -34,6 +34,16 @@
3434
3535
**Hardware:**
3636
37+
Not all hardware / CircuitPython combinations are capable of running the
38+
debouncer correctly for an extended length of time. If this line works
39+
on your microcontroller, then the debouncer should work forever:
40+
41+
``from time import monotonic_ns``
42+
43+
If it gives an ImportError, then the time values available in Python become
44+
less accurate over the days, and the debouncer will take longer to react to
45+
button presses.
46+
3747
**Software and Dependencies:**
3848
3949
* Adafruit CircuitPython firmware for the supported boards:
@@ -52,6 +62,15 @@
5262
_UNSTABLE_STATE = const(0x02)
5363
_CHANGED_STATE = const(0x04)
5464

65+
# Find out whether the current CircuitPython supports time.monotonic_ns(),
66+
# which doesn't have the accuracy limitation.
67+
if hasattr(time, "monotonic_ns"):
68+
TICKS_PER_SEC = 1_000_000_000
69+
MONOTONIC_TICKS = time.monotonic_ns
70+
else:
71+
TICKS_PER_SEC = 1
72+
MONOTONIC_TICKS = time.monotonic
73+
5574

5675
class Debouncer:
5776
"""Debounce an input pin or an arbitrary predicate"""
@@ -68,10 +87,13 @@ def __init__(self, io_or_predicate, interval=0.010):
6887
self.function = io_or_predicate
6988
if self.function():
7089
self._set_state(_DEBOUNCED_STATE | _UNSTABLE_STATE)
71-
self.previous_time = 0
72-
self.interval = interval
73-
self._previous_state_duration = 0
74-
self._state_changed_time = 0
90+
self._last_bounce_ticks = 0
91+
self._last_duration_ticks = 0
92+
self._state_changed_ticks = 0
93+
94+
# Could use the .interval setter, but pylint prefers that we explicitly
95+
# set the real underlying attribute:
96+
self._interval_ticks = interval * TICKS_PER_SEC
7597

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

88110
def update(self):
89111
"""Update the debouncer state. MUST be called frequently"""
90-
now = time.monotonic()
112+
now_ticks = MONOTONIC_TICKS()
91113
self._unset_state(_CHANGED_STATE)
92114
current_state = self.function()
93115
if current_state != self._get_state(_UNSTABLE_STATE):
94-
self.previous_time = now
116+
self._last_bounce_ticks = now_ticks
95117
self._toggle_state(_UNSTABLE_STATE)
96118
else:
97-
if now - self.previous_time >= self.interval:
119+
if now_ticks - self._last_bounce_ticks >= self._interval_ticks:
98120
if current_state != self._get_state(_DEBOUNCED_STATE):
99-
self.previous_time = now
121+
self._last_bounce_ticks = now_ticks
100122
self._toggle_state(_DEBOUNCED_STATE)
101123
self._set_state(_CHANGED_STATE)
102-
self._previous_state_duration = now - self._state_changed_time
103-
self._state_changed_time = now
124+
self._last_duration_ticks = now_ticks - self._state_changed_ticks
125+
self._state_changed_ticks = now_ticks
126+
127+
@property
128+
def interval(self):
129+
"""The debounce delay, in seconds"""
130+
return self._interval_ticks / TICKS_PER_SEC
131+
132+
@interval.setter
133+
def interval(self, new_interval_s):
134+
self._interval_ticks = new_interval_s * TICKS_PER_SEC
104135

105136
@property
106137
def value(self):
@@ -121,10 +152,10 @@ def fell(self):
121152

122153
@property
123154
def last_duration(self):
124-
"""Return the amount of time the state was stable prior to the most recent transition."""
125-
return self._previous_state_duration
155+
"""Return the number of seconds the state was stable prior to the most recent transition."""
156+
return self._last_duration_ticks / TICKS_PER_SEC
126157

127158
@property
128159
def current_duration(self):
129-
"""Return the time since the most recent transition."""
130-
return time.monotonic() - self._state_changed_time
160+
"""Return the number of seconds since the most recent transition."""
161+
return (MONOTONIC_TICKS() - self._state_changed_ticks) / TICKS_PER_SEC

tests/tests.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
How to use this test file:
3+
4+
Copy adafruit_debouncer's dependencies to lib/ on your circuitpython device.
5+
Copy adafruit_debouncer.py to / on the device
6+
Copy this tests.py file to /main.py on the device
7+
Connect to the serial terminal (e.g. sudo screen /dev/ttyACM0 115200)
8+
Press Ctrl-D, if needed to start the tests running
9+
"""
10+
import sys
11+
import time
12+
import adafruit_debouncer
13+
14+
15+
def _true():
16+
return True
17+
18+
19+
def _false():
20+
return False
21+
22+
23+
def assertEqual(a, b):
24+
assert a == b, "Want %r, got %r" % (a, b)
25+
26+
27+
def test_back_and_forth():
28+
# Start false
29+
db = adafruit_debouncer.Debouncer(_false)
30+
assertEqual(db.value, False)
31+
32+
# Set the raw state to true, update, and make sure the debounced
33+
# state has not changed yet:
34+
db.function = _true
35+
db.update()
36+
assertEqual(db.value, False)
37+
assert not db.last_duration, "There was no previous interval??"
38+
39+
# Sleep longer than the debounce interval, so state can change:
40+
time.sleep(0.02)
41+
db.update()
42+
assert db.last_duration # is actually duration between powerup and now
43+
assertEqual(db.value, True)
44+
assertEqual(db.rose, True)
45+
assertEqual(db.fell, False)
46+
# Duration since last change has only been long enough to run these
47+
# asserts, which should be well under 1/10 second
48+
assert db.current_duration < 0.1, "Unit error? %d" % db.current_duration
49+
50+
# Set raw state back to false, make sure it's not instantly reflected,
51+
# then wait and make sure it IS reflected after the interval has passed.
52+
db.function = _false
53+
db.update()
54+
assertEqual(db.value, True)
55+
assertEqual(db.fell, False)
56+
assertEqual(db.rose, False)
57+
time.sleep(0.02)
58+
assert 0.019 < db.current_duration <= 1, (
59+
"Unit error? sleep .02 -> duration %d" % db.current_duration
60+
)
61+
db.update()
62+
assertEqual(db.value, False)
63+
assertEqual(db.rose, False)
64+
assertEqual(db.fell, True)
65+
66+
assert 0 < db.current_duration <= 0.1, (
67+
"Unit error? time to run asserts %d" % db.current_duration
68+
)
69+
assert 0 < db.last_duration < 0.1, (
70+
"Unit error? Last dur should be ~.02, is %d" % db.last_duration
71+
)
72+
73+
74+
def test_interval_is_the_same():
75+
db = adafruit_debouncer.Debouncer(_false, interval=0.25)
76+
assertEqual(db.value, False)
77+
db.update()
78+
db.function = _true
79+
db.update()
80+
81+
time.sleep(0.1) # longer than default interval
82+
db.update()
83+
assertEqual(db.value, False)
84+
85+
time.sleep(0.2) # 0.1 + 0.2 > 0.25
86+
db.update()
87+
assertEqual(db.value, True)
88+
assertEqual(db.rose, True)
89+
assertEqual(db.interval, 0.25)
90+
91+
92+
def test_setting_interval():
93+
# Check that setting the interval does change the time the debouncer waits
94+
db = adafruit_debouncer.Debouncer(_false, interval=0.01)
95+
db.update()
96+
97+
# set the interval to a longer time, sleep for a time between
98+
# the two interval settings, and assert that the value hasn't changed.
99+
100+
db.function = _true
101+
db.interval = 0.2
102+
db.update()
103+
assert db.interval - 0.2 < 0.00001, "interval is not consistent"
104+
time.sleep(0.11)
105+
db.update()
106+
107+
assertEqual(db.value, False)
108+
assertEqual(db.rose, False)
109+
assertEqual(db.fell, False)
110+
111+
# and then once the whole time has passed make sure it did change
112+
time.sleep(0.11)
113+
db.update()
114+
assertEqual(db.value, True)
115+
assertEqual(db.rose, True)
116+
assertEqual(db.fell, False)
117+
118+
119+
def run():
120+
passes = 0
121+
fails = 0
122+
for name, test in locals().items():
123+
if name.startswith("test_") and callable(test):
124+
try:
125+
print()
126+
print(name)
127+
test()
128+
print("PASS")
129+
passes += 1
130+
except Exception as e:
131+
sys.print_exception(e)
132+
print("FAIL")
133+
fails += 1
134+
135+
print(passes, "passed,", fails, "failed")
136+
if passes and not fails:
137+
print(
138+
r"""
139+
________
140+
< YATTA! >
141+
--------
142+
\ ^__^
143+
\ (oo)\_______
144+
(__)\ )\/\
145+
||----w |
146+
|| ||"""
147+
)
148+
149+
150+
if __name__ == "__main__":
151+
run()

0 commit comments

Comments
 (0)