Skip to content

Commit 0d8312a

Browse files
committed
Pushbutton: suppress release on long and double clicks.
1 parent b0dcc78 commit 0d8312a

File tree

3 files changed

+99
-58
lines changed

3 files changed

+99
-58
lines changed

DRIVERS.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ events.
1616
and also a software retriggerable delay object. Pushbuttons are a
1717
generalisation of switches providing logical rather than physical status along
1818
with double-clicked and long pressed events.
19-
3. `asyn.py` Provides synchronisation primitives. Required by `aswitch.py`.
20-
4. `astests.py` Test/demonstration programs for `aswitch.py`.
19+
3. `astests.py` Test/demonstration programs for `aswitch.py`.
2120

2221
# 3. Module aswitch.py
2322

@@ -104,7 +103,7 @@ scheduled for execution and will run asynchronously.
104103
Constructor arguments:
105104

106105
1. `pin` Mandatory. The initialised Pin instance.
107-
2. `lpmode` Default `False`. See below.
106+
2. `suppress` Default `False`. See 3.2.1 below.
108107

109108
Methods:
110109

@@ -148,17 +147,28 @@ loop = asyncio.get_event_loop()
148147
loop.run_until_complete(my_app()) # Run main application code
149148
```
150149

151-
The `lpmode` constructor argument modifies the behaviour of `press_func` in the
152-
event that a `long_func` is specified.
153-
154-
If `lpmode` is `False`, if a button press occurs `press_func` runs immediately;
155-
`long_func` runs if the button is still pressed when the timeout has elapsed.
156-
If `lpmode` is `True`, `press_func` is delayed until button release. If, at the
157-
time of release, `long_func` has run, `press_func` will be suppressed.
158-
159-
The default provides for low latency but a long button press will trigger
160-
`press_func` and `long_func`. `lpmode` = `True` prevents `press_func` from
161-
running.
150+
### 3.2.1 The suppress constructor argument
151+
152+
When the button is pressed `press_func` runs immediately. This minimal latency
153+
is ideal for applications such as games, but does imply that in the event of a
154+
long press, both `press_func` and `long_func` run: `press_func` immediately and
155+
`long_func` if the button is still pressed when the timer has elapsed. Similar
156+
reasoning applies to the double click function.
157+
158+
There can be a need for a **function** which runs if a button is pressed but
159+
only if a doubleclick or long press function does not run. The soonest that the
160+
absence of a long press can be detected is on button release. The absence of a
161+
double click can only be detected when the double click timer times out without
162+
a second press occurring.
163+
164+
This **function** is the `release_func`. If the `suppress` constructor arg is
165+
set, `release_func` will be launched as follows:
166+
1. If `double_func` does not exist on rapid button release.
167+
2. If `double_func` exists, after the expiration of the doubleclick timer.
168+
3. If `long_func` exists and the press duration causes `long_func` to be
169+
launched, `release_func` will not be launched.
170+
4. If `double_func` exists and a double click occurs, `release_func` will not
171+
be launched.
162172

163173
## 3.3 Delay_ms class
164174

@@ -192,6 +202,7 @@ Methods:
192202
2. `stop` No argument. Cancels the timeout, setting the `running` status
193203
`False`. The timer can be restarted by issuing `trigger` again.
194204
3. `running` No argument. Returns the running status of the object.
205+
4. `__call__` Alias for running.
195206

196207
If the `trigger` method is to be called from an interrupt service routine the
197208
`can_alloc` constructor arg should be `False`. This causes the delay object
@@ -236,11 +247,18 @@ of its behaviour if the switch is toggled rapidly.
236247

237248
Demonstrates the use of callbacks to toggle the red and green LED's.
238249

239-
## 4.3 Function test_btn()
250+
## 4.3 Function test_btn(lpmode=False)
240251

241252
This will flash the red LED on button push, and the green LED on release. A
242253
long press will flash the blue LED and a double-press the yellow one.
243254

255+
Test the launching of coroutines and also the `suppress` constructor arg.
256+
257+
It takes three optional positional boolean args:
258+
1. `Suppresss=False` If `True` sets the `suppress` constructor arg.
259+
2. `lf=True` Declare a long press coro.
260+
3. `df=true` Declare a double click coro.
261+
244262
The note below on race conditions applies.
245263

246264
## 4.4 Function test_btncb()
@@ -268,3 +286,6 @@ In the case of this test program it might be to ignore events while a similar
268286
one is running, or to extend the timer to prolong the LED illumination.
269287
Alternatively a subsequent button press might be required to terminate the
270288
illumination. The "right" behaviour is application dependent.
289+
290+
A further consequence of scheduling new coroutine instances when one or more
291+
are already running is that the `uasyncio` queue can fill causing an exception.

astests.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Tested on Pyboard but should run on other microcontroller platforms
33
# running MicroPython with uasyncio library.
44
# Author: Peter Hinch.
5-
# Copyright Peter Hinch 2017 Released under the MIT license.
5+
# Copyright Peter Hinch 2017-2018 Released under the MIT license.
66

77
from machine import Pin
88
from pyb import LED
@@ -60,20 +60,22 @@ def test_swcb():
6060
loop.run_until_complete(killer())
6161

6262
# Test for the Pushbutton class (coroutines)
63-
# Pass True to test lpmode
64-
def test_btn(lpmode=False):
63+
# Pass True to test suppress
64+
def test_btn(suppress=False, lf=True, df=True):
6565
print('Test of pushbutton scheduling coroutines.')
6666
print(helptext)
6767
pin = Pin('X1', Pin.IN, Pin.PULL_UP)
6868
red = LED(1)
6969
green = LED(2)
7070
yellow = LED(3)
7171
blue = LED(4)
72-
pb = Pushbutton(pin, lpmode)
72+
pb = Pushbutton(pin, suppress)
7373
pb.press_func(pulse, (red, 1000))
7474
pb.release_func(pulse, (green, 1000))
75-
pb.double_func(pulse, (yellow, 1000))
76-
pb.long_func(pulse, (blue, 1000))
75+
if df:
76+
pb.double_func(pulse, (yellow, 1000))
77+
if lf:
78+
pb.long_func(pulse, (blue, 1000))
7779
loop = asyncio.get_event_loop()
7880
loop.run_until_complete(killer())
7981

aswitch.py

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ def trigger(self, duration=0): # Update end time
6767
def running(self):
6868
return self.tstop is not None
6969

70+
__call__ = running
71+
7072
async def killer(self):
7173
twait = time.ticks_diff(self.tstop, time.ticks_ms())
7274
while twait > 0: # Must loop here: might be retriggered
@@ -118,34 +120,37 @@ class Pushbutton(object):
118120
debounce_ms = 50
119121
long_press_ms = 1000
120122
double_click_ms = 400
121-
def __init__(self, pin, lpmode=False):
123+
def __init__(self, pin, suppress=False):
122124
self.pin = pin # Initialise for input
123-
self._lpmode = lpmode
124-
self._press_pend = False
125-
self._true_func = False
126-
self._false_func = False
127-
self._double_func = False
128-
self._long_func = False
125+
self._supp = suppress
126+
self._dblpend = False # Doubleclick waiting for 2nd click
127+
self._dblran = False # Doubleclick executed user function
128+
self._tf = False
129+
self._ff = False
130+
self._df = False
131+
self._lf = False
132+
self._ld = False # Delay_ms instance for long press
133+
self._dd = False # Ditto for doubleclick
129134
self.sense = pin.value() # Convert from electrical to logical value
130135
self.buttonstate = self.rawstate() # Initial state
131136
loop = asyncio.get_event_loop()
132137
loop.create_task(self.buttoncheck()) # Thread runs forever
133138

134139
def press_func(self, func, args=()):
135-
self._true_func = func
136-
self._true_args = args
140+
self._tf = func
141+
self._ta = args
137142

138143
def release_func(self, func, args=()):
139-
self._false_func = func
140-
self._false_args = args
144+
self._ff = func
145+
self._fa = args
141146

142147
def double_func(self, func, args=()):
143-
self._double_func = func
144-
self._double_args = args
148+
self._df = func
149+
self._da = args
145150

146151
def long_func(self, func, args=()):
147-
self._long_func = func
148-
self._long_args = args
152+
self._lf = func
153+
self._la = args
149154

150155
# Current non-debounced logical button state: True == pressed
151156
def rawstate(self):
@@ -155,42 +160,55 @@ def rawstate(self):
155160
def __call__(self):
156161
return self.buttonstate
157162

163+
def _ddto(self): # Doubleclick timeout: no doubleclick occurred
164+
self._dblpend = False
165+
if self._supp:
166+
if not self._ld or (self._ld and not self._ld()):
167+
launch(self._ff, self._fa)
168+
169+
def _ldip(self): # True if a long delay exists and is running
170+
d = self._ld
171+
return d and d()
172+
158173
async def buttoncheck(self):
159174
loop = asyncio.get_event_loop()
160-
if self._long_func:
161-
longdelay = Delay_ms(self._long_func, self._long_args)
162-
if self._double_func:
163-
doubledelay = Delay_ms()
175+
if self._lf:
176+
self._ld = Delay_ms(self._lf, self._la)
177+
if self._df:
178+
self._dd = Delay_ms(self._ddto)
164179
while True:
165180
state = self.rawstate()
166181
# State has changed: act on it now.
167182
if state != self.buttonstate:
168183
self.buttonstate = state
169184
if state:
170185
# Button is pressed
171-
if self._long_func and not longdelay.running():
186+
if self._lf:
172187
# Start long press delay
173-
longdelay.trigger(Pushbutton.long_press_ms)
174-
if self._double_func:
175-
if doubledelay.running():
176-
launch(self._double_func, self._double_args)
188+
self._ld.trigger(Pushbutton.long_press_ms)
189+
if self._df:
190+
if self._dd():
191+
self._dd.stop()
192+
self._dblpend = False
193+
self._dblran = True # Prevent suppressed launch on release
194+
launch(self._df, self._da)
177195
else:
178196
# First click: start doubleclick timer
179-
doubledelay.trigger(Pushbutton.double_click_ms)
180-
if self._true_func:
181-
if self._long_func and self._lpmode:
182-
self._press_pend = True # Delay launch until release
183-
else:
184-
launch(self._true_func, self._true_args)
197+
self._dd.trigger(Pushbutton.double_click_ms)
198+
self._dblpend = True # Prevent suppressed launch on release
199+
if self._tf:
200+
launch(self._tf, self._ta)
185201
else:
186202
# Button release
187-
if longdelay.running():
203+
if self._ff:
204+
if self._supp:
205+
if self._ldip() and not self._dblpend and not self._dblran:
206+
launch(self._ff, self._fa)
207+
else:
208+
launch(self._ff, self._fa)
209+
if self._ldip():
188210
# Avoid interpreting a second click as a long push
189-
longdelay.stop()
190-
if self._press_pend:
191-
launch(self._true_func, self._true_args)
192-
if self._false_func:
193-
launch(self._false_func, self._false_args)
194-
self._press_pend = False
211+
self._ld.stop()
212+
self._dblran = False
195213
# Ignore state changes until switch has settled
196214
await asyncio.sleep_ms(Pushbutton.debounce_ms)

0 commit comments

Comments
 (0)