From ef6951fec1ceac7783b31634be01611f73f26f76 Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Sat, 10 Aug 2019 00:02:28 -0400 Subject: [PATCH 01/10] Add some tests --- tests.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests.py diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..496e5be --- /dev/null +++ b/tests.py @@ -0,0 +1,113 @@ +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(): + 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() + time.sleep(0.11) + db.update() + + assertEqual(db.value, False) + assertEqual(db.rose, False) + assertEqual(db.fell, False) + + 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() From daea74352eebe4a7811dca658f3fe41d03bf1595 Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Mon, 12 Aug 2019 21:38:20 -0400 Subject: [PATCH 02/10] Keep internal interval in a convenient unit #9 --- adafruit_debouncer.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index b4702ff..23778df 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -53,6 +53,14 @@ _UNSTABLE_STATE = const(0x02) _CHANGED_STATE = const(0x04) +if hasattr(time, 'monotonic_ns'): + INTERVAL_FACTOR = 1_000_000_000 + MONOTONIC_TIME = time.monotonic_ns +else: + INTERVAL_FACTOR = 1 + MONOTONIC_TIME = time.monotonic + + class Debouncer(object): """Debounce an input pin or an arbitrary predicate""" @@ -90,19 +98,28 @@ 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): + return self._interval / INTERVAL_FACTOR + + + @interval.setter + def interval(self, new_interval_s): + self._interval = new_interval_s * INTERVAL_FACTOR + @property def value(self): From 596708b09025bdfec5a0adb50fb1b78ffde5a24a Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Mon, 12 Aug 2019 22:12:36 -0400 Subject: [PATCH 03/10] Correct wording of the intro --- adafruit_debouncer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index 23778df..57448d6 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -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 From 741932cb93ce7d7b3b9bc7f27242692e40b61913 Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Mon, 12 Aug 2019 22:12:54 -0400 Subject: [PATCH 04/10] Add doc section about whether the debouncer will work forever --- adafruit_debouncer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index 57448d6..2978874 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -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: From 33ba756ac45155b628dc78fbb930c0d93161aa9d Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Mon, 12 Aug 2019 22:13:07 -0400 Subject: [PATCH 05/10] Nicer names and some comments --- adafruit_debouncer.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index 2978874..5b36945 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -63,11 +63,14 @@ _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'): - INTERVAL_FACTOR = 1_000_000_000 + MONOTONIC_UNITS_PER_SEC = 1_000_000_000 MONOTONIC_TIME = time.monotonic_ns else: - INTERVAL_FACTOR = 1 + MONOTONIC_UNITS_PER_SEC = 1 MONOTONIC_TIME = time.monotonic @@ -123,12 +126,13 @@ def update(self): @property def interval(self): - return self._interval / INTERVAL_FACTOR + """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 * INTERVAL_FACTOR + self._interval = new_interval_s * MONOTONIC_UNITS_PER_SEC @property From 5e099acd2f3b356b83dd611a86eaf2f28f7bc308 Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Mon, 12 Aug 2019 22:13:24 -0400 Subject: [PATCH 06/10] Comments and add test of interval getter --- tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests.py b/tests.py index 496e5be..d1d1140 100644 --- a/tests.py +++ b/tests.py @@ -57,6 +57,7 @@ def test_interval_is_the_same(): 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() @@ -66,6 +67,7 @@ def test_setting_interval(): 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() @@ -73,6 +75,7 @@ def test_setting_interval(): 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) From e9b861429400a4a4520aa8f254e257a42b82921c Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Thu, 9 Jan 2020 21:45:29 -0500 Subject: [PATCH 07/10] Update and also rename duration/time to ticks when measured in ticks --- adafruit_debouncer.py | 43 ++++++++++++++++++++------------------ tests.py => tests/tests.py | 21 ++++++++++++++++++- 2 files changed, 43 insertions(+), 21 deletions(-) rename tests.py => tests/tests.py (71%) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index 69a2d89..e3381ff 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -67,11 +67,11 @@ # 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 + TICKS_PER_SEC = 1_000_000_000 + MONOTONIC_TICKS = time.monotonic_ns else: - MONOTONIC_UNITS_PER_SEC = 1 - MONOTONIC_TIME = time.monotonic + TICKS_PER_SEC = 1 + MONOTONIC_TICKS = time.monotonic class Debouncer(object): @@ -89,10 +89,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): @@ -113,30 +116,30 @@ def _get_state(self, bits): def update(self): """Update the debouncer state. MUST be called frequently""" - now = MONOTONIC_TIME() + 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 / MONOTONIC_UNITS_PER_SEC + return self._interval_ticks / TICKS_PER_SEC @interval.setter def interval(self, new_interval_s): - self._interval = new_interval_s * MONOTONIC_UNITS_PER_SEC + self._interval_ticks = new_interval_s * TICKS_PER_SEC @property @@ -158,10 +161,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 diff --git a/tests.py b/tests/tests.py similarity index 71% rename from tests.py rename to tests/tests.py index d1d1140..42f89ba 100644 --- a/tests.py +++ b/tests/tests.py @@ -13,30 +13,49 @@ def assertEqual(a, b): assert a == b, "Want %r, got %r" % (a, b) -def test_simple(): +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) From 71f24d99c427565a7bba65f2ca1f0f409fe6180c Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Thu, 9 Jan 2020 21:49:49 -0500 Subject: [PATCH 08/10] Fix line length issue --- adafruit_debouncer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index e3381ff..37a41e9 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -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 DigitalInOut object -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 From f060e9a4fb40bcc55b116ea7db6c5741e2260b1b Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Thu, 9 Jan 2020 21:49:58 -0500 Subject: [PATCH 09/10] Add instructions for using the tests --- tests/tests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/tests.py b/tests/tests.py index 42f89ba..ef1343f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,3 +1,12 @@ +""" +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 From 481d6159200692828202e9c4cfa7c7dcafef6743 Mon Sep 17 00:00:00 2001 From: Thomas Grenfell Smith Date: Thu, 16 Apr 2020 15:28:33 -0400 Subject: [PATCH 10/10] black --- adafruit_debouncer.py | 3 +-- tests/tests.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/adafruit_debouncer.py b/adafruit_debouncer.py index 68c6506..8ff911a 100644 --- a/adafruit_debouncer.py +++ b/adafruit_debouncer.py @@ -64,7 +64,7 @@ # Find out whether the current CircuitPython supports time.monotonic_ns(), # which doesn't have the accuracy limitation. -if hasattr(time, 'monotonic_ns'): +if hasattr(time, "monotonic_ns"): TICKS_PER_SEC = 1_000_000_000 MONOTONIC_TICKS = time.monotonic_ns else: @@ -129,7 +129,6 @@ 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 diff --git a/tests/tests.py b/tests/tests.py index ef1343f..d90d56e 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -14,6 +14,8 @@ def _true(): return True + + def _false(): return False @@ -53,17 +55,20 @@ def test_back_and_forth(): assertEqual(db.fell, False) assertEqual(db.rose, False) time.sleep(0.02) - assert 0.019 < db.current_duration <= 1, \ + 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, \ + assert 0 < db.current_duration <= 0.1, ( "Unit error? time to run asserts %d" % db.current_duration - assert 0 < db.last_duration < 0.1, \ + ) + 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(): @@ -115,7 +120,7 @@ def run(): passes = 0 fails = 0 for name, test in locals().items(): - if name.startswith('test_') and callable(test): + if name.startswith("test_") and callable(test): try: print() print(name) @@ -129,7 +134,8 @@ def run(): print(passes, "passed,", fails, "failed") if passes and not fails: - print(r""" + print( + r""" ________ < YATTA! > -------- @@ -137,8 +143,9 @@ def run(): \ (oo)\_______ (__)\ )\/\ ||----w | - || ||""") + || ||""" + ) -if __name__ == '__main__': +if __name__ == "__main__": run()