Skip to content

Commit bdd98cd

Browse files
committed
Add wait_all, ESwitch and Ebutton.
1 parent 2315331 commit bdd98cd

File tree

4 files changed

+378
-18
lines changed

4 files changed

+378
-18
lines changed

v3/docs/TUTORIAL.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -705,15 +705,15 @@ constant creation of tasks. Arguably the `Barrier` class is the best approach.
705705

706706
### 3.2.1 Wait on multiple events
707707

708-
The `wait_any` primitive allows a task to wait on a list of events. When one
708+
The `WaitAny` primitive allows a task to wait on a list of events. When one
709709
of the events is triggered, the task continues. It is effectively a logical
710710
`or` of events.
711711
```python
712-
from primitives import wait_any
712+
from primitives import WaitAny
713713
evt1 = Event()
714714
evt2 = Event()
715715
# Launch tasks that might trigger these events
716-
evt = await wait_any((evt1, evt2))
716+
evt = await WaitAny((evt1, evt2))
717717
# One or other was triggered
718718
if evt == evt1:
719719
evt1.clear()
@@ -722,6 +722,18 @@ else:
722722
evt2.clear()
723723
# evt2 was triggered
724724
```
725+
The `WaitAll` primitive is similar except that the calling task will pause
726+
until all passed `Event`s have been set:
727+
```python
728+
from primitives import WaitAll
729+
evt1 = Event()
730+
evt2 = Event()
731+
wa = WaitAll((evt1, evt2)) #
732+
# Launch tasks that might trigger these events
733+
await wa
734+
# Both were triggered
735+
```
736+
Awaiting `WaitAll` or `WaitAny` may be cancelled or subject to a timeout.
725737

726738
###### [Contents](./TUTORIAL.md#contents)
727739

v3/primitives/__init__.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# __init__.py Common functions for uasyncio primitives
22

3-
# Copyright (c) 2018-2020 Peter Hinch
3+
# Copyright (c) 2018-2022 Peter Hinch
44
# Released under the MIT License (MIT) - see LICENSE file
55

66
try:
@@ -30,20 +30,6 @@ def _handle_exception(loop, context):
3030
loop = asyncio.get_event_loop()
3131
loop.set_exception_handler(_handle_exception)
3232

33-
async def wait_any(events):
34-
evt = asyncio.Event()
35-
trig_event = None
36-
async def wt(event):
37-
nonlocal trig_event
38-
await event.wait()
39-
evt.set()
40-
trig_event = event
41-
tasks = [asyncio.create_task(wt(event)) for event in events]
42-
await evt.wait()
43-
for task in tasks:
44-
task.cancel()
45-
return trig_event
46-
4733
_attrs = {
4834
"AADC": "aadc",
4935
"Barrier": "barrier",
@@ -57,6 +43,10 @@ async def wt(event):
5743
"Semaphore": "semaphore",
5844
"BoundedSemaphore": "semaphore",
5945
"Switch": "switch",
46+
"WaitAll": "events",
47+
"WaitAny": "events",
48+
"ESwitch": "events",
49+
"EButton": "events",
6050
}
6151

6252
# Copied from uasyncio.__init__.py

v3/primitives/events.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# events.py Event based primitives
2+
3+
# Copyright (c) 2022 Peter Hinch
4+
# Released under the MIT License (MIT) - see LICENSE file
5+
6+
import uasyncio as asyncio
7+
from . import Delay_ms
8+
9+
# An Event-like class that can wait on an iterable of Event instances.
10+
# .wait pauses until any passed event is set.
11+
class WaitAny:
12+
def __init__(self, events):
13+
self.events = events
14+
self.trig_event = None
15+
self.evt = asyncio.Event()
16+
17+
async def wait(self):
18+
tasks = [asyncio.create_task(self.wt(event)) for event in self.events]
19+
try:
20+
await self.evt.wait()
21+
finally:
22+
self.evt.clear()
23+
for task in tasks:
24+
task.cancel()
25+
return self.trig_event
26+
27+
async def wt(self, event):
28+
await event.wait()
29+
self.evt.set()
30+
self.trig_event = event
31+
32+
def event(self):
33+
return self.trig_event
34+
35+
# An Event-like class that can wait on an iterable of Event instances,
36+
# .wait pauses until all passed events have been set.
37+
class WaitAll:
38+
def __init__(self, events):
39+
self.events = events
40+
41+
async def wait(self):
42+
async def wt(event):
43+
await event.wait()
44+
tasks = (asyncio.create_task(wt(event)) for event in self.events)
45+
try:
46+
await asyncio.gather(*tasks)
47+
finally: # May be subject to timeout or cancellation
48+
for task in tasks:
49+
task.cancel()
50+
51+
# Minimal switch class having an Event based interface
52+
class ESwitch:
53+
debounce_ms = 50
54+
55+
def __init__(self, pin, lopen=1): # Default is n/o switch returned to gnd
56+
self._pin = pin # Should be initialised for input with pullup
57+
self._lopen = lopen # Logic level in "open" state
58+
self.open = asyncio.Event()
59+
self.close = asyncio.Event()
60+
self._state = self._pin() ^ self._lopen # Get initial state
61+
asyncio.create_task(self._poll(ESwitch.debounce_ms))
62+
63+
async def _poll(self, dt): # Poll the button
64+
while True:
65+
if (s := self._pin() ^ self._lopen) != self._state:
66+
self._state = s
67+
self._of() if s else self._cf()
68+
await asyncio.sleep_ms(dt) # Wait out bounce
69+
70+
def _of(self):
71+
self.open.set()
72+
73+
def _cf(self):
74+
self.close.set()
75+
76+
# ***** API *****
77+
# Return current state of switch (0 = pressed)
78+
def __call__(self):
79+
return self._state
80+
81+
def deinit(self):
82+
self._poll.cancel()
83+
84+
# Minimal pushbutton class having an Event based interface
85+
class EButton:
86+
debounce_ms = 50 # Attributes can be varied by user
87+
long_press_ms = 1000
88+
double_click_ms = 400
89+
90+
def __init__(self, pin, suppress=False, sense=None):
91+
self._pin = pin # Initialise for input
92+
self._supp = suppress
93+
self._sense = pin() if sense is None else sense
94+
self._state = self.rawstate() # Initial logical state
95+
self._ltim = Delay_ms(duration = EButton.long_press_ms)
96+
self._dtim = Delay_ms(duration = EButton.double_click_ms)
97+
self.press = asyncio.Event() # *** API ***
98+
self.double = asyncio.Event()
99+
self.long = asyncio.Event()
100+
self.release = asyncio.Event() # *** END API ***
101+
self._tasks = [asyncio.create_task(self._poll(EButton.debounce_ms))] # Tasks run forever. Poll contacts
102+
self._tasks.append(asyncio.create_task(self._ltf())) # Handle long press
103+
if suppress:
104+
self._tasks.append(asyncio.create_task(self._dtf())) # Double timer
105+
106+
async def _poll(self, dt): # Poll the button
107+
while True:
108+
if (s := self.rawstate()) != self._state:
109+
self._state = s
110+
self._pf() if s else self._rf()
111+
await asyncio.sleep_ms(dt) # Wait out bounce
112+
113+
def _pf(self): # Button press
114+
if not self._supp:
115+
self.press.set() # User event
116+
if not self._ltim(): # Don't retrigger long timer if already running
117+
self._ltim.trigger()
118+
if self._dtim(): # Press occurred while _dtim is running
119+
self.double.set() # User event
120+
self._dtim.stop() # _dtim's Event is only used if suppress
121+
else:
122+
self._dtim.trigger()
123+
124+
def _rf(self): # Button release
125+
self._ltim.stop()
126+
if not self._supp or not self._dtim(): # If dtim running postpone release otherwise it
127+
self.release.set() # is set before press
128+
129+
async def _ltf(self): # Long timeout
130+
while True:
131+
await self._ltim.wait()
132+
self._ltim.clear() # Clear the event
133+
self.long.set() # User event
134+
135+
async def _dtf(self): # Double timeout (runs if suppress is set)
136+
while True:
137+
await self._dtim.wait()
138+
self._dtim.clear() # Clear the event
139+
if not self._ltim(): # Button was released
140+
self.press.set() # User events
141+
self.release.set()
142+
143+
# ****** API ******
144+
# Current non-debounced logical button state: True == pressed
145+
def rawstate(self):
146+
return bool(self._pin() ^ self._sense)
147+
148+
# Current debounced state of button (True == pressed)
149+
def __call__(self):
150+
return self._state
151+
152+
def deinit(self):
153+
for task in self._tasks:
154+
task.cancel()

0 commit comments

Comments
 (0)