Skip to content

Commit ccc21a0

Browse files
committed
events.py: Add ELO class.
1 parent d5edede commit ccc21a0

File tree

4 files changed

+283
-13
lines changed

4 files changed

+283
-13
lines changed

v3/docs/EVENTS.md

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ This document assumes familiarity with `asyncio`. See [official docs](http://doc
2020
5.1 [Use of Delay_ms](./EVENTS.md#51-use-of-delay_ms) A retriggerable delay
2121
5.2 [Long and very long button press](./EVENTS.md#52-long-and-very-long-button-press)
2222
5.3 [Application example](./EVENTS.md#53-application-example)
23-
6. [Drivers](./EVENTS.md#6-drivers) Minimal Event-based drivers
24-
6.1 [ESwitch](./EVENTS.md#61-eswitch) Debounced switch
25-
6.2 [EButton](./EVENTS.md#62-ebutton) Debounced pushbutton with double and long press events
23+
6. [ELO class](./EVENTS.md#6-elo-class) Convert a coroutine or task to an event-like object.
24+
7. [Drivers](./EVENTS.md#7-drivers) Minimal Event-based drivers
25+
7.1 [ESwitch](./EVENTS.md#71-eswitch) Debounced switch
26+
7.2 [EButton](./EVENTS.md#72-ebutton) Debounced pushbutton with double and long press events
2627

2728
[Appendix 1 Polling](./EVENTS.md#100-appendix-1-polling)
2829

@@ -61,6 +62,11 @@ Users only need to know the names of the bound `Event` instances. By contast
6162
there is no standard way to specify callbacks, to define the passing of
6263
callback arguments or to define how to retrieve their return values.
6364

65+
There are other ways to define an API without callbacks, notably the stream
66+
mechanism and the use of asynchronous iterators with `async for`. This doc
67+
discusses the `Event` based approach which is ideal for sporadic occurrences
68+
such as responding to user input.
69+
6470
###### [Contents](./EVENTS.md#0-contents)
6571

6672
# 2. Rationale
@@ -135,6 +141,10 @@ ELO examples are:
135141
| [Delay_ms][2m] | Y | Y | Y | Self-setting |
136142
| [WaitAll](./EVENTS.md#42-waitall) | Y | Y | N | See below |
137143
| [WaitAny](./EVENTS.md#41-waitany) | Y | Y | N | |
144+
| [ELO instances](./EVENTS.md#44-elo-class) | Y | N | N | |
145+
146+
The `ELO` class converts coroutines or `Task` instances to event-like objects,
147+
allowing them to be included in the arguments of event based primitives.
138148

139149
Drivers exposing `Event` instances include:
140150

@@ -316,19 +326,118 @@ async def foo():
316326
else:
317327
# Normal outcome, process readings
318328
```
329+
###### [Contents](./EVENTS.md#0-contents)
330+
331+
# 6. ELO class
332+
333+
This converts a task to an "event-like object", enabling tasks to be included in
334+
`WaitAll` and `WaitAny` arguments. An `ELO` instance is a wrapper for a `Task`
335+
instance and its lifetime is that of its `Task`. The constructor can take a
336+
coroutine or a task as its first argument; in the former case the coro is
337+
converted to a `Task`.
338+
339+
#### Constructor args
340+
341+
1. `coro` This may be a coroutine or a `Task` instance.
342+
2. `*args` Positional args for a coroutine (ignored if a `Task` is passed).
343+
3. `**kwargs` Keyword args for a coroutine (ignored if a `Task` is passed).
344+
345+
If a coro is passed it is immediately converted to a `Task` and scheduled for
346+
execution.
347+
348+
#### Asynchronous method
349+
350+
1. `wait` Pauses until the `Task` is complete or is cancelled. In the latter
351+
case no exception is thrown.
352+
353+
#### Synchronous method
354+
355+
1. `__call__` Returns the instance's `Task`. If the instance's `Task` was
356+
cancelled the `CancelledError` exception is returned. The function call operator
357+
allows a running task to be accessed, e.g. for cancellation. It also enables return values to be
358+
retrieved.
359+
360+
#### Usage example
361+
362+
In most use cases an `ELO` instance is a throw-away object which allows a coro
363+
to participate in an event-based primitive:
364+
```python
365+
evt = asyncio.Event()
366+
async def my_coro(t):
367+
await asyncio.wait(t)
368+
369+
async def foo(): # Puase until the event has been triggered and coro has completed
370+
await WaitAll((evt, ELO(my_coro, 5))).wait() # Note argument passing
371+
```
372+
#### Retrieving results
373+
374+
A task may return a result on completion. This may be accessed by awaiting the
375+
`ELO` instance's `Task`. A reference to the `Task` may be acquired with function
376+
call syntax. The following code fragment illustrates usage. It assumes that
377+
`task` has already been created, and that `my_coro` is a coroutine taking an
378+
integer arg. There is an `EButton` instance `ebutton` and execution pauses until
379+
tasks have run to completion and the button has been pressed.
380+
```python
381+
async def foo():
382+
elos = (ELO(my_coro, 5), ELO(task))
383+
events = (ebutton.press,)
384+
await WaitAll(elos + events).wait()
385+
for e in elos: # Retrieve results from each task
386+
r = await e() # Works even though task has already completed
387+
print(r)
388+
```
389+
This works because it is valid to `await` a task which has already completed.
390+
The `await` returns immediately with the result. If `WaitAny` were used an `ELO`
391+
instance might contain a running task. In this case the line
392+
```python
393+
r = await e()
394+
```
395+
would pause before returning the result.
396+
397+
#### Cancellation
398+
399+
The `Task` in `ELO` instance `elo` may be retrieved by issuing `elo()`. For
400+
example the following will subject an `ELO` instance to a timeout:
401+
```python
402+
async def elo_timeout(elo, t):
403+
await asyncio.sleep(t)
404+
elo().cancel() # Retrieve the Task and cancel it
405+
406+
async def foo():
407+
elo = ELO(my_coro, 5)
408+
asyncio.create_task(elo_timeout(2))
409+
await WaitAll((elo, ebutton.press)).wait() # Until button press and ELO either finished or timed out
410+
```
411+
If the `ELO` task is cancelled, `.wait` terminates; the exception is retained.
412+
Thus `WaitAll` or `WaitAny` behaves as if the task had terminated normally. A
413+
subsequent call to `elo()` will return the exception. In an application
414+
where the task might return a result or be cancelled, the following may be used:
415+
```python
416+
async def foo():
417+
elos = (ELO(my_coro, 5), ELO(task))
418+
events = (ebutton.press,)
419+
await WaitAll(elos + events).wait()
420+
for e in elos: # Check each task
421+
t = e()
422+
if isinstance(t, asyncio.CancelledError):
423+
# Handle exception
424+
else: # Retrieve results
425+
r = await t # Works even though task has already completed
426+
print(r)
427+
```
319428

320429
###### [Contents](./EVENTS.md#0-contents)
321430

322-
# 6. Drivers
431+
# 7. Drivers
323432

324433
The following device drivers provide an `Event` based interface for switches and
325434
pushbuttons.
326435

327-
## 6.1 ESwitch
436+
## 7.1 ESwitch
328437

329438
This is now documented [here](./DRIVERS.md#31-eswitch-class).
330439

331-
## 6.2 EButton
440+
## 7.2 EButton
332441

333442
This is now documented [here](./DRIVERS.md#41-ebutton-class).
334443

v3/primitives/__init__.py

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

1212
async def _g():
1313
pass
14+
15+
1416
type_coro = type(_g())
1517

1618
# If a callback is passed, run it and return.
@@ -22,14 +24,18 @@ def launch(func, tup_args):
2224
res = asyncio.create_task(res)
2325
return res
2426

27+
2528
def set_global_exception():
2629
def _handle_exception(loop, context):
2730
import sys
31+
2832
sys.print_exception(context["exception"])
2933
sys.exit()
34+
3035
loop = asyncio.get_event_loop()
3136
loop.set_exception_handler(_handle_exception)
3237

38+
3339
_attrs = {
3440
"AADC": "aadc",
3541
"Barrier": "barrier",
@@ -44,6 +50,7 @@ def _handle_exception(loop, context):
4450
"Switch": "switch",
4551
"WaitAll": "events",
4652
"WaitAny": "events",
53+
"ELO": "events",
4754
"ESwitch": "events",
4855
"EButton": "events",
4956
"RingbufQueue": "ringbuf_queue",

v3/primitives/events.py

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# events.py Event based primitives
22

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

66
import uasyncio as asyncio
@@ -34,9 +34,10 @@ def event(self):
3434
return self.trig_event
3535

3636
def clear(self):
37-
for evt in (x for x in self.events if hasattr(x, 'clear')):
37+
for evt in (x for x in self.events if hasattr(x, "clear")):
3838
evt.clear()
3939

40+
4041
# An Event-like class that can wait on an iterable of Event-like instances,
4142
# .wait pauses until all passed events have been set.
4243
class WaitAll:
@@ -46,6 +47,7 @@ def __init__(self, events):
4647
async def wait(self):
4748
async def wt(event):
4849
await event.wait()
50+
4951
tasks = (asyncio.create_task(wt(event)) for event in self.events)
5052
try:
5153
await asyncio.gather(*tasks)
@@ -54,15 +56,65 @@ async def wt(event):
5456
task.cancel()
5557

5658
def clear(self):
57-
for evt in (x for x in self.events if hasattr(x, 'clear')):
59+
for evt in (x for x in self.events if hasattr(x, "clear")):
5860
evt.clear()
5961

62+
63+
# Convert to an event-like object: either a running task or a coro with args.
64+
# Motivated by a suggestion from @sandyscott iss #116
65+
class ELO_x:
66+
def __init__(self, coro, *args, **kwargs):
67+
self._coro = coro
68+
self._args = args
69+
self._kwargs = kwargs
70+
self._task = None # Current running task (or exception)
71+
72+
async def wait(self):
73+
cr = self._coro
74+
istask = isinstance(cr, asyncio.Task) # Instantiated with a Task
75+
if istask and isinstance(self._task, asyncio.CancelledError):
76+
return # Previously awaited and was cancelled/timed out
77+
self._task = cr if istask else asyncio.create_task(cr(*self._args, **self._kwargs))
78+
try:
79+
await self._task
80+
except asyncio.CancelledError as e:
81+
self._task = e # Let WaitAll or WaitAny complete
82+
83+
# User can retrieve task/coro results by awaiting .task() (even if task had
84+
# run to completion). If task was cancelled CancelledError is returned.
85+
# If .task() is called before .wait() returns None or result of prior .wait()
86+
# Caller issues isinstance(task, CancelledError)
87+
def task(self):
88+
return self._task
89+
90+
91+
# Convert to an event-like object: either a running task or a coro with args.
92+
# Motivated by a suggestion from @sandyscott iss #116
93+
class ELO:
94+
def __init__(self, coro, *args, **kwargs):
95+
tsk = isinstance(coro, asyncio.Task) # Instantiated with a Task
96+
self._task = coro if tsk else asyncio.create_task(coro(*args, **kwargs))
97+
98+
async def wait(self):
99+
try:
100+
await self._task
101+
except asyncio.CancelledError as e:
102+
self._task = e # Let WaitAll or WaitAny complete
103+
104+
# User can retrieve task/coro results by awaiting elo() (even if task had
105+
# run to completion). If task was cancelled CancelledError is returned.
106+
# If .task() is called before .wait() returns None or result of prior .wait()
107+
# Caller issues isinstance(task, CancelledError)
108+
def __call__(self):
109+
return self._task
110+
111+
60112
# Minimal switch class having an Event based interface
61113
class ESwitch:
62114
debounce_ms = 50
63115

64116
def __init__(self, pin, lopen=1): # Default is n/o switch returned to gnd
65-
self._pin = pin # Should be initialised for input with pullup
117+
self._pin = pin # Should be initialised for input with pullup
66118
self._lopen = lopen # Logic level in "open" state
67119
self.open = asyncio.Event()
68120
self.close = asyncio.Event()
@@ -92,6 +144,7 @@ def deinit(self):
92144
self.open.clear()
93145
self.close.clear()
94146

147+
95148
# Minimal pushbutton class having an Event based interface
96149
class EButton:
97150
debounce_ms = 50 # Attributes can be varied by user
@@ -103,13 +156,14 @@ def __init__(self, pin, suppress=False, sense=None):
103156
self._supp = suppress
104157
self._sense = pin() if sense is None else sense
105158
self._state = self.rawstate() # Initial logical state
106-
self._ltim = Delay_ms(duration = EButton.long_press_ms)
107-
self._dtim = Delay_ms(duration = EButton.double_click_ms)
159+
self._ltim = Delay_ms(duration=EButton.long_press_ms)
160+
self._dtim = Delay_ms(duration=EButton.double_click_ms)
108161
self.press = asyncio.Event() # *** API ***
109162
self.double = asyncio.Event()
110163
self.long = asyncio.Event()
111164
self.release = asyncio.Event() # *** END API ***
112-
self._tasks = [asyncio.create_task(self._poll(EButton.debounce_ms))] # Tasks run forever. Poll contacts
165+
# Tasks run forever. Poll contacts
166+
self._tasks = [asyncio.create_task(self._poll(EButton.debounce_ms))]
113167
self._tasks.append(asyncio.create_task(self._ltf())) # Handle long press
114168
if suppress:
115169
self._tasks.append(asyncio.create_task(self._dtf())) # Double timer

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy