Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for an extension of module delay_ms #133

Open
wnelis opened this issue Jan 13, 2025 · 5 comments
Open

Proposal for an extension of module delay_ms #133

wnelis opened this issue Jan 13, 2025 · 5 comments

Comments

@wnelis
Copy link

wnelis commented Jan 13, 2025

In most of my scripts running on micro-controllers, one or more tasks are to be scheduled at regular intervals, independent of the actual running time of the task at hand. To achieve this when using asyncio, a small extension to module delay_ms suffice. The difference between the official and the extended version of module delay_ms is:

--- ./delay_ms.py	2025-01-13 10:06:27.779011912 +0100
+++ ./micropython-utils/delay_ms.py	2025-01-13 10:12:42.318156831 +0100
@@ -5,6 +5,9 @@
 # Copyright (c) 2018-2022 Peter Hinch
 # Released under the MIT License (MIT) - see LICENSE file
 
+# Added methods repeat() and restart(), allowing a timer to be used to schedule
+# a task at regular intervals.  2024 Wim Nelis
+
 import asyncio
 from utime import ticks_add, ticks_diff, ticks_ms
 from . import launch
@@ -22,6 +25,7 @@
         self._args = args
         self._durn = duration  # Default duration
         self._retn = None  # Return value of launched callable
+        self._tper =   -1  # Repeater period [ms]
         self._tend = None  # Stop time (absolute ms).
         self._busy = False
         self._trig = asyncio.ThreadSafeFlag()
@@ -52,11 +56,29 @@
     def trigger(self, duration=0):  # Update absolute end time, 0-> ctor default
         if self._mtask is None:
             raise RuntimeError("Delay_ms.deinit() has run.")
-        self._tend = ticks_add(ticks_ms(), duration if duration > 0 else self._durn)
+        self._tper = duration  if duration > 0  else self._durn
+        self._tend = ticks_add(ticks_ms(), self._tper)
         self._retn = None  # Default in case cancelled.
         self._busy = True
         self._trig.set()
 
+    def repeat(self):
+        assert not self._busy, "Can't repeat a running timer"
+        assert self._tper > 0, "Trigger not invoked yet"
+#       now= ticks_ms()  # Handle tasks running longer dan _tper [ms]
+#       while ticks_diff(now, self._tend) > self._tper:
+#           self._tend = ticks_add(self._tend, self._tper)
+        self._tend = ticks_add(self._tend, self._tper)
+        self._busy = True
+        self._tout.clear()
+        self._trig.set()
+
+    def restart(self):
+        assert self._tper > 0, "Trigger not invoked yet"
+        if self._busy:
+            self.stop()
+        self.trigger(self._tper)
+
     def stop(self):
         self._ttask.cancel()
         self._ttask = self._fake

Using this extension, an asynchronous task can be scheduled at regular intervals in the following way:

atimer = delay_ms.Delay_ms()  # Timer of 1 [s]

async def atask():
    while True:
        await atimer.wait()
        atimer.repeat()  # Implicit atimer.clear()
        <Do the job>

How about including this extension in module delay_ms?

@peterhinch
Copy link
Owner

Have you seen the schedule module? This is my solution to scheduling tasks at regular intervals.

@wnelis
Copy link
Author

wnelis commented Jan 13, 2025

No, not yet. I'll look at it.

@wnelis
Copy link
Author

wnelis commented Jan 13, 2025

The schedule module does have an impressive functionality, which does what is needed in my current project, which is reading sensors (interval is typically 10 to 100 [s]) and reporting the results via MQTT (interval typically 100 to 1000 [s]). However I expect I will not (be able to) use it, as the underlying hardware is an ESP8266. I fear the schedule module is too big for this micro-controller. I still have to include an MQTT client (probably your asyncio client).

As far as I understand it now, the use cases of the schedule module versus the proposed extension are quite different, with my current project being in the intersection of the two. The proposed extension is meant for cases in which no real time clock is needed or available, an interval of 0.01 to 1000 [s] is to be realized and the resource usage (RAM) is minimal.

Thus I would still like to propose to implement the extension, for the benefit of users of very small micro-controllers.

@peterhinch
Copy link
Owner

I have two reservations. Firstly I'm reluctant to increase the size of delay_ms which is already bigger than I would like.

Secondly I think that the mission you wish to accomplish is best achieved with async code rather than by means of delay_ms. Consider this coroutine which triggers an Event at regular intervals:

async def runner(t, evt):
    while True:
        await asyncio.sleep_ms(t)
        evt.set()

This could be used by multiple tasks as follows:

async def this_job():
    ev = asyncio.Event()
    asyncio.create_task(runner(3000, ev))  # Runner instance
    while True:
        await ev.wait()
        ev.clear()
        # do work

As a general comment the defining features of delay_ms are that it can be retriggered, its delay time can be dynamically altered and it can be triggered from an ISR. Cases that don't require any of those features can usually be addressed more cheaply by other means.

@wnelis
Copy link
Author

wnelis commented Jan 14, 2025

I like the approach you sketched: it is compact and bundles the definition and usage of a timer in a single coro.

As a consequence I withdraw my proposal.

After some tests, the following variant, which shows a better timing of the successive events, will be used in my project:

class pre( asyncio.ThreadSafeFlag ):    # Periodic repeating event
  def __init__( self, t ):
    super().__init__()
    self.at= ticks_ms()			# (Last) activation time
    self.ts= t				# Time step

async def runner( evt ):
  while True:
    now= ticks_ms()
    while ticks_diff( evt.at, now ) <= 0:
      evt.at= ticks_add( evt.at, evt.ts )  # Calculate time stamp of next event
    await asyncio.sleep_ms( ticks_diff(evt.at,now) )  # Wait the remaining time
    evt.set()

It can be used in tasks in the following way:

async def this_job():
  ev= pre( 1000 )
  asyncio.create_task( runner(ev) ) 
  while True:
    await ev.wait()
    # Do something usefull

Thank you very much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants