Skip to content

Commit 1e6dbe5

Browse files
committed
Schedule: Add async for interface.
1 parent 2a64578 commit 1e6dbe5

File tree

2 files changed

+159
-77
lines changed

2 files changed

+159
-77
lines changed

v3/as_drivers/sched/sched.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@
1414
# Wait prior to a sequence start
1515
_PAUSE = const(2)
1616

17+
18+
class Sequence: # Enable asynchronous iterator interface
19+
def __init__(self):
20+
self._evt = asyncio.Event()
21+
self._args = None
22+
23+
def __aiter__(self):
24+
return self
25+
26+
async def __anext__(self):
27+
await self._evt.wait()
28+
self._evt.clear()
29+
return self._args
30+
31+
def trigger(self, args):
32+
self._args = args
33+
self._evt.set()
34+
35+
1736
async def schedule(func, *args, times=None, **kwargs):
1837
async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0.
1938
while t > 0:
@@ -23,16 +42,20 @@ async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0.
2342
tim = mktime(localtime()[:3] + (0, 0, 0, 0, 0)) # Midnight last night
2443
now = round(time()) # round() is for Unix
2544
fcron = cron(**kwargs) # Cron instance for search.
26-
while tim < now: # Find first event in sequence
45+
while tim < now: # Find first future trigger in sequence
2746
# Defensive. fcron should never return 0, but if it did the loop would never quit
2847
tim += max(fcron(tim), 1)
29-
await long_sleep(tim - now - _PAUSE) # Time to wait (can be < 0)
48+
# Wait until just before the first future trigger
49+
await long_sleep(tim - now - _PAUSE) # Time to wait (can be < 0)
3050

31-
while times is None or times > 0:
32-
tw = fcron(round(time())) # Time to wait (s)
51+
while times is None or times > 0: # Until all repeats are done (or forever).
52+
tw = fcron(round(time())) # Time to wait (s) (fcron is stateless).
3353
await long_sleep(tw)
54+
res = None
3455
if isinstance(func, asyncio.Event):
3556
func.set()
57+
elif isinstance(func, Sequence):
58+
func.trigger(args)
3659
else:
3760
res = launch(func, args)
3861
if times is not None:

v3/docs/SCHEDULE.md

Lines changed: 132 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
1. [Scheduling tasks](./SCHEDULE.md#1-scheduling-tasks)
44
2. [Overview](./SCHEDULE.md#2-overview)
55
3. [Installation](./SCHEDULE.md#3-installation)
6-
4. [The schedule function](./SCHEDULE.md#4-the-schedule-function) The primary interface for asyncio
6+
4. [The schedule coroutine](./SCHEDULE.md#4-the-schedule-coroutine) The primary interface for asyncio.
77
4.1 [Time specifiers](./SCHEDULE.md#41-time-specifiers)
88
4.2 [Calendar behaviour](./SCHEDULE.md#42-calendar-behaviour) Calendars can be tricky...
99
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.2.1 [Behaviour of mday and wday values](./SCHEDULE.md#421-behaviour-of-mday-and-wday-values)
1010
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;4.2.2 [Time causing month rollover](./SCHEDULE.md#422-time-causing-month-rollover)
1111
4.3 [Limitations](./SCHEDULE.md#43-limitations)
1212
4.4 [The Unix build](./SCHEDULE.md#44-the-unix-build)
13-
5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders
13+
4.5 [Callback interface](./SCHEDULE.md#45-callback-interface) Alternative interface using callbacks.
14+
4.6 [Event interface](./SCHEDULE.md#46-event-interface) Alternative interface using Event instances.
15+
5. [The cron object](./SCHEDULE.md#5-the-cron-object) The rest of this doc is for hackers and synchronous coders.
1416
5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event)
1517
5.2 [How it works](./SCHEDULE.md#52-how-it-works)
1618
6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations)
@@ -19,6 +21,7 @@
1921
8. [The simulate script](./SCHEDULE.md#8-the-simulate-script) Rapidly test sequences.
2022

2123
Release note:
24+
23rd Nov 2023 Add asynchronous iterator interface.
2225
3rd April 2023 Fix issue #100. Where an iterable is passed to `secs`, triggers
2326
must now be at least 10s apart (formerly 2s).
2427

@@ -38,34 +41,40 @@ latter it is less capable but is small, fast and designed for microcontroller
3841
use. Repetitive and one-shot events may be created.
3942

4043
It is ideally suited for use with `asyncio` and basic use requires minimal
41-
`asyncio` knowledge. Users intending only to schedule callbacks can simply
42-
adapt the example code. It can be used in synchronous code and an example is
43-
provided.
44+
`asyncio` knowledge. Example code is provided offering various ways of
45+
responding to timing triggers including running callbacks. The module can be
46+
also be used in synchronous code and an example is provided.
4447

4548
It is cross-platform and has been tested on Pyboard, Pyboard D, ESP8266, ESP32
4649
and the Unix build.
4750

4851
# 2. Overview
4952

50-
The `schedule` function (`sched/sched.py`) is the interface for use with
51-
`asyncio`. The function takes a callback and causes that callback to run at
52-
specified times. A coroutine may be substituted for the callback - at the
53-
specified times it will be promoted to a `Task` and run.
53+
The `schedule` coroutine (`sched/sched.py`) is the interface for use with
54+
`asyncio`. Three interface alternatives are offered which vary in the behaviour:
55+
which occurs when a scheduled trigger occurs:
56+
1. An asynchronous iterator is triggered.
57+
2. A user defined `Event` is set.
58+
3. A user defined callback or coroutine is launched.
5459

55-
The `schedule` function instantiates a `cron` object (in `sched/cron.py`). This
56-
is the core of the scheduler: it is a closure created with a time specifier and
57-
returning the time to the next scheduled event. Users of `asyncio` do not need
58-
to deal with `cron` instances.
60+
One or more `schedule` tasks may be assigned to a `Sequence` instance. This
61+
enables an `async for` statement to be triggered whenever any of the `schedule`
62+
tasks is triggered.
5963

60-
This library can also be used in synchronous code, in which case `cron`
61-
instances must explicitly be created.
64+
Under the hood the `schedule` function instantiates a `cron` object (in
65+
`sched/cron.py`). This is the core of the scheduler: it is a closure created
66+
with a time specifier and returning the time to the next scheduled event. Users
67+
of `asyncio` do not need to deal with `cron` instances. This library can also be
68+
used in synchronous code, in which case `cron` instances must explicitly be
69+
created.
6270

6371
##### [Top](./SCHEDULE.md#0-contents)
6472

6573
# 3. Installation
6674

67-
Copy the `sched` directory and contents to the target's filesystem. This may be
68-
done with the official [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html):
75+
The `sched` directory and contents must be copied to the target's filesystem.
76+
This may be done with the official
77+
[mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html):
6978
```bash
7079
$ mpremote mip install "github:peterhinch/micropython-async/v3/as_drivers/sched"
7180
```
@@ -75,7 +84,7 @@ On networked platforms it may be installed with [mip](https://docs.micropython.o
7584
```
7685
Currently these tools install to `/lib` on the built-in Flash memory. To install
7786
to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used.
78-
Move to the SD card root, run `rshell` and issue:
87+
Move to `as_drivers` on the PC, run `rshell` and issue:
7988
```
8089
> rsync sched /sd/sched
8190
```
@@ -94,16 +103,19 @@ The following files are installed in the `sched` directory.
94103
The `crontest` script is only of interest to those wishing to adapt `cron.py`.
95104
It will run on any MicroPython target.
96105

97-
# 4. The schedule function
106+
# 4. The schedule coroutine
98107

99-
This enables a callback or coroutine to be run at intervals. The callable can
100-
be specified to run forever, once only or a fixed number of times. `schedule`
101-
is an asynchronous function.
108+
This enables a response to be triggered at intervals. The response can be
109+
specified to occur forever, once only or a fixed number of times. `schedule`
110+
is a coroutine and is typically run as a background task as follows:
111+
```python
112+
asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4)))
113+
```
102114

103115
Positional args:
104-
1. `func` The callable (callback or coroutine) to run. Alternatively an
105-
`Event` may be passed (see below).
106-
2. Any further positional args are passed to the callable.
116+
1. `func` This may be a callable (callback or coroutine) to run, a user defined
117+
`Event` or an instance of a `Sequence`.
118+
2. Any further positional args are passed to the callable or the `Sequence`.
107119

108120
Keyword-only args. Args 1..6 are
109121
[Time specifiers](./SCHEDULE.md#41-time-specifiers): a variety of data types
@@ -125,65 +137,37 @@ the value returned by that run of the callable.
125137
Because `schedule` does not terminate promptly it is usually started with
126138
`asyncio.create_task`, as in the following example where a callback is
127139
scheduled at various times. The code below may be run by issuing
128-
```python
129-
import sched.asynctest
130-
```
131-
This is the demo code.
132-
```python
133-
import asyncio as asyncio
134-
from sched.sched import schedule
135-
from time import localtime
136-
137-
def foo(txt): # Demonstrate callback
138-
yr, mo, md, h, m, s, wd = localtime()[:7]
139-
fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
140-
print(fst.format(txt, h, m, s, md, mo, yr))
140+
The event-based interface can be simpler than using callables:
141141

142-
async def bar(txt): # Demonstrate coro launch
143-
yr, mo, md, h, m, s, wd = localtime()[:7]
144-
fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
145-
print(fst.format(txt, h, m, s, md, mo, yr))
146-
await asyncio.sleep(0)
142+
The remainder of this section describes the asynchronous iterator interface as
143+
this is the simplest to use. The other interfaces are discussed in
144+
* [4.5 Callback interface](./SCHEDULE.md#45-callback-interface)
145+
* [4.6 Event interface](./SCHEDULE.md#46-event-interface)
147146

148-
async def main():
149-
print('Asynchronous test running...')
150-
asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4)))
151-
asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5)))
152-
# Launch a coroutine
153-
asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3)))
154-
# Launch a one-shot task
155-
asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1))
156-
await asyncio.sleep(900) # Quit after 15 minutes
157-
158-
try:
159-
asyncio.run(main())
160-
finally:
161-
_ = asyncio.new_event_loop()
162-
```
163-
The event-based interface can be simpler than using callables:
147+
One or more `schedule` instances are collected in a `Sequence` object. This
148+
supports the asynchronous iterator interface:
164149
```python
165-
import asyncio as asyncio
166-
from sched.sched import schedule
150+
import uasyncio as asyncio
151+
from sched.sched import schedule, Sequence
167152
from time import localtime
168153

169154
async def main():
170155
print("Asynchronous test running...")
171-
evt = asyncio.Event()
172-
asyncio.create_task(schedule(evt, hrs=10, mins=range(0, 60, 4)))
173-
while True:
174-
await evt.wait() # Multiple tasks may wait on an Event
175-
evt.clear() # It must be cleared.
156+
seq = Sequence() # A Sequence comprises one or more schedule instances
157+
asyncio.create_task(schedule(seq, 'every 4 mins', hrs=None, mins=range(0, 60, 4)))
158+
asyncio.create_task(schedule(seq, 'every 5 mins', hrs=None, mins=range(0, 60, 5)))
159+
asyncio.create_task(schedule(seq, 'every 3 mins', hrs=None, mins=range(0, 60, 3)))
160+
# A one-shot trigger
161+
asyncio.create_task(schedule(seq, 'one shot', hrs=None, mins=range(0, 60, 2), times=1))
162+
async for args in seq:
176163
yr, mo, md, h, m, s, wd = localtime()[:7]
177-
print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr}")
164+
print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr} args: {args}")
178165

179166
try:
180167
asyncio.run(main())
181168
finally:
182169
_ = asyncio.new_event_loop()
183170
```
184-
See [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#32-event).
185-
Also [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/EVENTS.md)
186-
for a discussion of event-based programming.
187171

188172
##### [Top](./SCHEDULE.md#0-contents)
189173

@@ -286,6 +270,81 @@ is to avoid scheduling the times in your region where this occurs (01.00.00 to
286270

287271
##### [Top](./SCHEDULE.md#0-contents)
288272

273+
## 4.5 Callback interface
274+
275+
In this instance a user defined `callable` is passed as the first `schedule` arg.
276+
A `callable` may be a function or a coroutine. It is possible for multiple
277+
`schedule` instances to call the same callback, as in the example below. The
278+
code is included in the library as `sched/asyntest.py` and may be run as below.
279+
```python
280+
import sched.asynctest
281+
```
282+
This is the demo code.
283+
```python
284+
import uasyncio as asyncio
285+
from sched.sched import schedule
286+
from time import localtime
287+
288+
def foo(txt): # Demonstrate callback
289+
yr, mo, md, h, m, s, wd = localtime()[:7]
290+
fst = 'Callback {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
291+
print(fst.format(txt, h, m, s, md, mo, yr))
292+
293+
async def bar(txt): # Demonstrate coro launch
294+
yr, mo, md, h, m, s, wd = localtime()[:7]
295+
fst = 'Coroutine {} {:02d}:{:02d}:{:02d} on {:02d}/{:02d}/{:02d}'
296+
print(fst.format(txt, h, m, s, md, mo, yr))
297+
await asyncio.sleep(0)
298+
299+
async def main():
300+
print('Asynchronous test running...')
301+
asyncio.create_task(schedule(foo, 'every 4 mins', hrs=None, mins=range(0, 60, 4)))
302+
asyncio.create_task(schedule(foo, 'every 5 mins', hrs=None, mins=range(0, 60, 5)))
303+
# Launch a coroutine
304+
asyncio.create_task(schedule(bar, 'every 3 mins', hrs=None, mins=range(0, 60, 3)))
305+
# Launch a one-shot task
306+
asyncio.create_task(schedule(foo, 'one shot', hrs=None, mins=range(0, 60, 2), times=1))
307+
await asyncio.sleep(900) # Quit after 15 minutes
308+
309+
try:
310+
asyncio.run(main())
311+
finally:
312+
_ = asyncio.new_event_loop()
313+
```
314+
##### [Top](./SCHEDULE.md#0-contents)
315+
316+
## 4.6 Event interface
317+
318+
In this instance a user defined `Event` is passed as the first `schedule` arg.
319+
It is possible for multiple `schedule` instances to trigger the same `Event`.
320+
The user is responsible for clearing the `Event`. This interface has a drawback
321+
in that extra positional args passed to `schedule` are lost.
322+
```python
323+
import uasyncio as asyncio
324+
from sched.sched import schedule
325+
from time import localtime
326+
327+
async def main():
328+
print("Asynchronous test running...")
329+
evt = asyncio.Event()
330+
asyncio.create_task(schedule(evt, hrs=10, mins=range(0, 60, 4)))
331+
while True:
332+
await evt.wait() # Multiple tasks may wait on an Event
333+
evt.clear() # It must be cleared.
334+
yr, mo, md, h, m, s, wd = localtime()[:7]
335+
print(f"Event {h:02d}:{m:02d}:{s:02d} on {md:02d}/{mo:02d}/{yr}")
336+
337+
try:
338+
asyncio.run(main())
339+
finally:
340+
_ = asyncio.new_event_loop()
341+
```
342+
See [tutorial](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/TUTORIAL.md#32-event).
343+
Also [this doc](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/EVENTS.md)
344+
for a discussion of event-based programming.
345+
346+
##### [Top](./SCHEDULE.md#0-contents)
347+
289348
# 5. The cron object
290349

291350
This is the core of the scheduler. Users of `asyncio` do not need to concern
@@ -450,9 +509,9 @@ def wait_for(**kwargs):
450509

451510
# 8. The simulate script
452511

453-
This enables the behaviour of sets of args to `schedule` to be rapidly checked.
454-
The `sim` function should be adapted to reflect the application specifics. The
455-
default is:
512+
In `sched/simulate.py`. This enables the behaviour of sets of args to `schedule`
513+
to be rapidly checked. The `sim` function should be adapted to reflect the
514+
application specifics. The default is:
456515
```python
457516
def sim(*args):
458517
set_time(*args)

0 commit comments

Comments
 (0)