Skip to content

Commit 24ec5da

Browse files
committed
Schedule: schedule.py fix iss. 100.
1 parent a9a5e3a commit 24ec5da

File tree

3 files changed

+75
-40
lines changed

3 files changed

+75
-40
lines changed

v3/as_drivers/sched/cron.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
# Copyright (c) 2020-2023 Peter Hinch
44
# Released under the MIT License (MIT) - see LICENSE file
55

6+
# A cron is instantiated with sequence specifier args. An instance accepts an integer time
7+
# value (in secs since epoch) and returns the number of seconds to wait for a matching time.
8+
# It holds no state.
9+
# See docs for restrictions and limitations.
10+
611
from time import mktime, localtime
712
# Validation
813
_valid = ((0, 59, 'secs'), (0, 59, 'mins'), (0, 23, 'hrs'),
@@ -28,8 +33,8 @@ def do_arg(a, cv): # Arg, current value
2833
raise ValueError('Invalid None value for secs')
2934
if not isinstance(secs, int) and len(secs) > 1: # It's an iterable
3035
ss = sorted(secs)
31-
if min((a[1] - a[0] for a in zip(ss, ss[1:]))) < 2:
32-
raise ValueError("Can't have consecutive seconds.", last, x)
36+
if min((a[1] - a[0] for a in zip(ss, ss[1:]))) < 10:
37+
raise ValueError("Seconds values must be >= 10s apart.")
3338
args = (secs, mins, hrs, mday, month, wday) # Validation for all args
3439
valid = iter(_valid)
3540
vestr = 'Argument {} out of range'

v3/as_drivers/sched/sched.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
# sched.py
22

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

66
import uasyncio as asyncio
77
from sched.primitives import launch
8-
from time import time
8+
from time import time, mktime, localtime
99
from sched.cron import cron
1010

11+
12+
# uasyncio can't handle long delays so split into 1000s (1e6 ms) segments
13+
_MAXT = const(1000)
14+
# Wait prior to a sequence start
15+
_PAUSE = const(2)
16+
1117
async def schedule(func, *args, times=None, **kwargs):
12-
fcron = cron(**kwargs)
13-
maxt = 1000 # uasyncio can't handle arbitrarily long delays
18+
async def long_sleep(t): # Sleep with no bounds. Immediate return if t < 0.
19+
while t > 0:
20+
await asyncio.sleep(min(t, _MAXT))
21+
t -= _MAXT
22+
23+
tim = mktime(localtime()[:3] + (0, 0, 0, 0, 0)) # Midnight last night
24+
now = round(time()) # round() is for Unix
25+
fcron = cron(**kwargs) # Cron instance for search.
26+
while tim < now: # Find first event in sequence
27+
# Defensive. fcron should never return 0, but if it did the loop would never quit
28+
tim += max(fcron(tim), 1)
29+
await long_sleep(tim - now - _PAUSE) # Time to wait (can be < 0)
30+
1431
while times is None or times > 0:
15-
tw = fcron(int(time())) # Time to wait (s)
16-
while tw > 0: # While there is still time to wait
17-
await asyncio.sleep(min(tw, maxt))
18-
tw -= maxt
32+
tw = fcron(round(time())) # Time to wait (s)
33+
await long_sleep(tw)
1934
res = launch(func, args)
2035
if times is not None:
2136
times -= 1

v3/docs/SCHEDULE.md

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
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-
4.5 [Initialisation](./SCHEDULE.md#45-initialisation)__
1413
5. [The cron object](./SCHEDULE.md#5-the-cron-object) For hackers and synchronous coders
1514
5.1 [The time to an event](./SCHEDULE.md#51-the-time-to-an-event)
1615
5.2 [How it works](./SCHEDULE.md#52-how-it-works)
1716
6. [Hardware timing limitations](./SCHEDULE.md#6-hardware-timing-limitations)
1817
7. [Use in synchronous code](./SCHEDULE.md#7-use-in-synchronous-code) If you really must.
18+
7.1 [Initialisation](./SCHEDULE.md#71-initialisation)__
1919
8. [The simulate script](./SCHEDULE.md#8-the-simulate-script) Rapidly test sequences.
2020

21+
Release note:
22+
3rd April 2023 Fix issue #100. Where an iterable is passed to `secs`, triggers
23+
must now be at least 10s apart (formerly 2s).
24+
2125
##### [Tutorial](./TUTORIAL.md#contents)
2226
##### [Main V3 README](../README.md)
2327

@@ -161,16 +165,20 @@ The args may be of the following types.
161165
3. An object supporting the Python iterator protocol and iterating over
162166
integers. For example `hrs=(3, 17)` will cause events to occur at 3am and 5pm,
163167
`wday=range(0, 5)` specifies weekdays. Tuples, lists, ranges or sets may be
164-
passed. If using this feature please see
165-
[Initialisation](./SCHEDULE.md#45-initialisation).
168+
passed.
166169

167170
Legal integer values are listed above. Basic validation is done as soon as
168171
`schedule` is run.
169172

170173
Note the implications of the `None` wildcard. Setting `mins=None` will schedule
171174
the event to occur on every minute (equivalent to `*` in a Unix cron table).
172-
Setting `secs=None` or consecutive seconds values will cause a `ValueError` -
173-
events must be at least two seconds apart.
175+
Setting `secs=None` will cause a `ValueError`.
176+
177+
Passing an iterable to `secs` is not recommended: this library is intended for
178+
scheduling relatively long duration events. For rapid sequencing, schedule a
179+
coroutine which awaits `uasyncio` `sleep` or `sleep_ms` routines. If an
180+
iterable is passed, triggers must be at least ten seconds apart or a
181+
`ValueError` will result.
174182

175183
Default values schedule an event every day at 03.00.00.
176184

@@ -246,29 +254,6 @@ and duplicates when they go back. Scheduling those times will fail. A solution
246254
is to avoid scheduling the times in your region where this occurs (01.00.00 to
247255
02.00.00 in March and October here).
248256

249-
## 4.5 Initialisation
250-
251-
Where a time specifier is an iterator (e.g. `mins=range(0, 60, 15)`) and there
252-
are additional constraints (e.g. `hrs=3`) it may be necessary to delay the
253-
start. The problem is specific to scheduling a sequence at a future time, and
254-
there is a simple solution.
255-
256-
A `cron` object searches forwards from the current time. Assume the above case.
257-
If the code start at 7:05 it picks the first later minute in the `range`,
258-
i.e. `mins=15`, then picks the hour. This means that the first trigger occurs
259-
at 3:15. Subsequent behaviour will be correct, but the first trigger would be
260-
expected at 3:00. The solution is to delay start until the minutes value is in
261-
the range`45 < mins <= 59`. The `hours` value is immaterial but a reasonable
262-
general solution is to delay until just before the first expected callback:
263-
264-
```python
265-
async def run():
266-
asyncio.create_task(schedule(payload, args, hrs=3, mins=range(0, 60, 15)))
267-
268-
async def delay_start():
269-
asyncio.create_task(schedule(run, hrs=2, mins=55, times=1))
270-
```
271-
272257
##### [Top](./SCHEDULE.md#0-contents)
273258

274259
# 5. The cron object
@@ -397,8 +382,38 @@ available to the application including cancellation of scheduled tasks. The
397382
above code is incompatible with `uasyncio` because of the blocking calls to
398383
`time.sleep()`.
399384

400-
If scheduling a sequence to run at a future time please see
401-
[Initialisation](./SCHEDULE.md#45-initialisation).
385+
## 7.1 Initialisation
386+
387+
Where a time specifier is an iterator (e.g. `mins=range(0, 60, 15)`) and there
388+
are additional constraints (e.g. `hrs=3`) it may be necessary to delay the
389+
start. The problem is specific to scheduling a sequence at a future time, and
390+
there is a simple solution (which the asynchronous version implements
391+
transparently).
392+
393+
A `cron` object searches forwards from the current time. Assume the above case.
394+
If the code start at 7:05 it picks the first later minute in the `range`,
395+
i.e. `mins=15`, then picks the hour. This means that the first trigger occurs
396+
at 3:15. Subsequent behaviour will be correct, but the first trigger would be
397+
expected at 3:00. The solution is to delay start until the minutes value is in
398+
the range`45 < mins <= 59`. The general solution is to delay until just before
399+
the first expected callback:
400+
401+
```python
402+
def wait_for(**kwargs):
403+
tim = mktime(localtime()[:3] + (0, 0, 0, 0, 0)) # Midnight last night
404+
now = round(time())
405+
scron = cron(**kwargs) # Cron instance for search.
406+
while tim < now: # Find first event in sequence
407+
tim += scron(tim) + 2
408+
twait = tim - now - 600
409+
if twait > 0:
410+
sleep(twait)
411+
tcron = cron(**kwargs)
412+
while True:
413+
now = round(time())
414+
tw = tcron(now)
415+
sleep(tw + 2)
416+
```
402417

403418
##### [Top](./SCHEDULE.md#0-contents)
404419

0 commit comments

Comments
 (0)