Skip to content

Commit 9cd11e9

Browse files
committed
use the timestamp from the event to determine clicks
This avoids problems with a) clicks happening during other longer running coroutines and b) gradual loss of accuracy of `time.monotonic` over longer periods. The implementation of keypad in Blinka does not use this field, so it will default to using the ticks library, and lose the benefit of a), but retain b). This is a requirement of the asyncio library anyway,
1 parent 9ac2d01 commit 9cd11e9

File tree

4 files changed

+44
-19
lines changed

4 files changed

+44
-19
lines changed

async_button.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,19 @@
2121
2222
* CircuitPython asyncio module:
2323
https://github.com/adafruit/Adafruit_CircuitPython_asyncio
24+
25+
* CircuitPython ticks module:
26+
https://github.com/adafruit/Adafruit_CircuitPython_Ticks
2427
"""
2528

26-
# imports
2729

2830
__version__ = "0.0.0+auto.0"
2931
__repo__ = "https://github.com/furbrain/CircuitPython_async_button.git"
3032

31-
import time
32-
3333
import asyncio
3434

35+
from adafruit_ticks import ticks_add, ticks_less, ticks_ms
36+
3537
try:
3638
from typing import Dict, Sequence, Awaitable
3739
except ImportError:
@@ -92,7 +94,6 @@ async def released(self):
9294

9395

9496
class Button:
95-
# pylint: disable
9697
"""
9798
This object will monitor the specified pin for changes and will report
9899
single, double, triple and long_clicks. It creates a background `asyncio` process
@@ -191,20 +192,27 @@ async def _monitor(self):
191192
This is the main background task that monitors key presses and releases
192193
"""
193194
evt = keypad.Event(0, False)
194-
last_click_tm = -100000
195-
long_click_due = None
195+
now = ticks_ms()
196+
long_click_due = ticks_add(now, int(self.long_click_min_duration * 1000))
197+
dbl_clk_expires = ticks_add(now, -100)
196198
while True:
197199
if self.keys.events.get_into(evt):
198200
if evt.pressed:
199201
self._trigger(self.PRESSED)
200-
now = time.monotonic()
202+
now = getattr(
203+
evt, "timestamp", ticks_ms()
204+
) # use now if timestamp not there
201205
# print(now, self.last_click_tm, self.double_click_max_duration)
202-
if now - last_click_tm < self.double_click_max_duration:
206+
if ticks_less(now, dbl_clk_expires):
203207
self._increase_clicks()
204208
else:
205209
self.last_click = self.SINGLE
206-
last_click_tm = now
207-
long_click_due = now + self.long_click_min_duration
210+
long_click_due = ticks_add(
211+
now, int(self.long_click_min_duration * 1000)
212+
)
213+
dbl_clk_expires = ticks_add(
214+
now, int(self.double_click_max_duration * 1000)
215+
)
208216
self.pressed = True
209217
else:
210218
self._trigger(self.RELEASED)
@@ -216,7 +224,7 @@ async def _monitor(self):
216224
else:
217225
if self.pressed and self.click_enabled[self.LONG]:
218226
if (
219-
time.monotonic() > long_click_due
227+
ticks_less(long_click_due, ticks_ms())
220228
and self.last_click != self.LONG
221229
):
222230
self.last_click = self.LONG

docs/conf.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525

2626
wavedrom_html_jsinline = False
2727
render_using_wavedrompy = True
28-
autodoc_mock_imports = ["microcontroller", "countio", "keypad", "asyncio"]
28+
autodoc_mock_imports = [
29+
"microcontroller",
30+
"countio",
31+
"keypad",
32+
"asyncio",
33+
"adafruit_ticks",
34+
]
2935

3036
autodoc_preserve_defaults = True
3137

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55

66
Adafruit-Blinka
77
adafruit-circuitpython-asyncio
8+
adafruit-circuitpython-ticks

tests/test_async_button.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
sys.modules["countio"] = MagicMock()
1313

14-
# pylint: disable=wrong-import-position
15-
import async_button
14+
import async_button # pylint: disable=wrong-import-position
15+
1616

1717
# this class sets the sleep interval in the monitor task to zero so tests finish quickly
1818
class FastButton(async_button.Button):
@@ -25,10 +25,8 @@ def __init__(self, pin, value_when_pressed, *, interval=0, **kwargs):
2525
class TestButton(IsolatedAsyncioTestCase):
2626
# pylint: disable=invalid-name, too-many-public-methods
2727
def setUp(self) -> None:
28-
self.time = MagicMock()
29-
self.patch1 = patch("async_button.time", new=self.time)
28+
self.patch1 = patch("async_button.ticks_ms", new=self.new_ticks_ms)
3029
self.patch1.start()
31-
self.time.monotonic.side_effect = self.new_monotonic
3230
self.keypad_keys = MagicMock()
3331
self.patch2 = patch("async_button.keypad.Keys", new=self.keypad_keys)
3432
self.patch2.start()
@@ -50,8 +48,8 @@ async def asyncTearDown(self) -> None:
5048
self.button.deinit()
5149
await asyncio.sleep(0)
5250

53-
def new_monotonic(self) -> float:
54-
return self.time_count
51+
def new_ticks_ms(self) -> float:
52+
return int(self.time_count * 1000)
5553

5654
def new_key_get(self, event: keypad.Event) -> bool:
5755
self.time_count += self.interval
@@ -256,3 +254,15 @@ async def test_wait_all(self):
256254
await self.button.wait(), (self.button.RELEASED, self.button.TRIPLE)
257255
)
258256
self.assertAlmostEqual(self.time_count, 1.10, delta=0.1)
257+
258+
259+
class TestButtonWithTimestamp(TestButton):
260+
"""
261+
This one adds a timestamp to the event
262+
"""
263+
264+
def new_key_get(self, event: keypad.Event) -> bool:
265+
result = super().new_key_get(event)
266+
if result:
267+
event.timestamp = self.new_ticks_ms()
268+
return result

0 commit comments

Comments
 (0)