uasyncio: Schedule a task to run _now_

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
Post Reply
User avatar
scy
Posts: 7
Joined: Sat Sep 05, 2020 12:50 am
Contact:

uasyncio: Schedule a task to run _now_

Post by scy » Sat Sep 05, 2020 1:24 am

Hey there! I’ve been trying to write some debouncing code (on an ESP32, fwiw) using uasyncio and interrupts. The basic idea is as follows:
  • Bind an interrupt handler to the pin.
  • This interrupt handler then notes down the utime.ticks_ms() when the interrupt occured and then uses micropython.schedule to run a function that creates a "debouncing" task.
  • That function creates a task that asynchronously sleeps until the interrupt timestamp is more than 10ms ago. After that time the pin is considered debounced and another function is called to work with the value it settled on.
  • If the IRQ re-triggers while the function is sleeping, the timestamp will be updated, causing the function re-calculate wait time once it wakes up again. There’s a flag in the class that prevents launching more than one task simultaneously.
I’ve noticed however that the sleeping/debouncing task will not launch instantly after it’s been created. Further tests showed that it will only run once any other task finishes its “await sleep_ms(…)”. In other words, if you have a task to blink an LED that waits for sleep_ms(200) in a loop, the debouncing task will start 0 to 200ms after the interrupt. If the only task you have running is something that sleeps for 10s, it can take the debouncing task up to 10 seconds to start, which is of course utterly useless.

This makes sense, I guess: The sleeping tasks won’t be interrupted; this is not real concurrency after all.

But then I though: Oh, wait, you can cancel tasks. If I have my “endless loop” always-on coro running and cancel it after calling create_task(), i.e. inject the CancelledError into it, might that interrupt its sleep? But turns out, at the moment I call create_task(), the endless-loop coro is considered active and I get an exception that a task can’t cancel itself (why though?).

So… I guess there’s no way to use dynamically started uasyncio tasks for debouncing? And yes, I’ve seen Peter’s Switch class, but it a) relies on an always-running task that wakes up every n ms (with n being the debounce time), which is somewhat wasteful, and b) doesn’t handle re-triggers during that time.

If there’s no better way, I might go for timer interrupts instead then, but since the ESP32 has only four of them and no virtual ones, I kind of wanted to avoid that.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: uasyncio: Schedule a task to run _now_

Post by pythoncoder » Sat Sep 05, 2020 5:38 am

Your description suggests that there may be a bug in your code. It certainly doesn't match how uasyncio works. Assume you have a coroutine foo defined with async def. If you issue

Code: Select all

uasyncio.create_task(foo())
foo will run immediately. The fact that other tasks are waiting using

Code: Select all

await uasyncio.sleep(t)
will not affect the startup. A key purpose of uasyncio is to enable program operation while tasks are waiting on events or times.

What will affect program behaviour is if you have a running task which hogs the CPU. For example, if you have a task which issues time.sleep(1) it will lock out all other tasks for the duration; a tight loop with no await will have the same effect. This may be what's happening in your code.

Incidentally I think your idea of switch debouncing is incorrect. You can trigger your function as soon as the switch closure is detected. The purpose of a debouncer is to prevent subsequent triggers until the bouncing is complete. As you correctly observe, my debouncer locks out retriggers during that time: that's what debouncing means. I also suggest you read this article on the subject - even my default delay is too short for some switches.
Peter Hinch
Index to my micropython libraries.

kevinkk525
Posts: 969
Joined: Sat Feb 03, 2018 7:02 pm

Re: uasyncio: Schedule a task to run _now_

Post by kevinkk525 » Sat Sep 05, 2020 7:27 am

About the debouncing I have to say that there is indeed a use-case for the "reverse debouncing" as @scy described, for example if you have a badly implemented/connected switch like the bell signal I connected. Then you get "false" pulses that trigger the interrupt, even though nobody rang the bell but I just used the door opener or someone rang the bell in a different appartement. So I use a software debounce to get rid of those false triggers. Of course it would be better if that was implemented in hardware but it works in software too.
And that's one use-case for this "reverse debouncing" (or whatever you want to call it to show the difference to @pythoncoder's debouncing which works great for normal switches/buttons).

So my bell implementations have a similar approach, get triggered by interrupt, check if signal stays high/low after 10-50ms so it is a confirmed activation and not just a false trigger.
You can see my implementations here: https://github.com/kevinkk525/pysmartno ... nsors/bell
One implementation uses an interrupt and then the uasyncio task polls the pin value until it's sure the signal stays that way.
The other implementation polls the pin value in the uasyncio task without any interrupts.
If you look at the commit history, I have an old implementation that used an interrupt and a timer to do what you intended to do (but I never used micropython.schedule): https://github.com/kevinkk525/pysmartno ... rs/bell.py
Kevin Köck
Micropython Smarthome Firmware (with Home-Assistant integration): https://github.com/kevinkk525/pysmartnode

User avatar
scy
Posts: 7
Joined: Sat Sep 05, 2020 12:50 am
Contact:

Re: uasyncio: Schedule a task to run _now_

Post by scy » Sat Sep 05, 2020 11:08 am

pythoncoder wrote:
Sat Sep 05, 2020 5:38 am
If you issue

Code: Select all

uasyncio.create_task(foo())
foo will run immediately. The fact that other tasks are waiting using

Code: Select all

await uasyncio.sleep(t)
will not affect the startup.
I’m sorry, but that’s not the behavior I’m seeing.

I guess it’s easier with a minimal example. Take this code:

Code: Select all

import uasyncio
from machine import Pin
import micropython


async def example_task():
    print('example task running')


def task_launcher(_):
    print('task_launcher running')
    uasyncio.create_task(example_task())


def irq_handler(pin):
    print('IRQ!', pin.value())
    micropython.schedule(task_launcher, None)


# My board has an on-board button with a pull-up.
# You'll probably need to change this line for your hardware.
trigger = Pin(34, Pin.IN)

trigger.irq(irq_handler, Pin.IRQ_FALLING | Pin.IRQ_RISING)


async def endless_loop():
    while True:
        print('Tick.')
        await uasyncio.sleep(5)
        print('Tock.')
        await uasyncio.sleep(5)

uasyncio.run(endless_loop())
When I run this code on an ESP32, it will print “Tick”, sleep for 5 seconds, print “Tock”, sleep for five seconds again, and then repeat. Pushing the button will instantly print “IRQ! 0” (or 1, depending on the pin value), instantly followed by “task_launcher running”. The task_launcher has been scheduled to run after the interrupt handler in order to minimize time in the handler.

However, “example_task running” will not be printed until directly before a Tick or Tock, suggesting that it won’t start until the current task (endless_loop) stopped sleeping.

Can you reproduce this behavior?

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: uasyncio: Schedule a task to run _now_

Post by pythoncoder » Sun Sep 06, 2020 9:04 am

Yes I can, and I think I know the reason. When a task issues

Code: Select all

await uasyncio.sleep(t)
the task is suspended. Because (in your script) no other task exists the scheduler goes into a wait state, waking up when the task is ready for execution. The example code calls uasyncio.create_task() from a soft interrupt context: the task is placed on the event queue immediately. However the scheduler only gets to run this when its period of suspension is over.

The (obvious and ugly) workround is to run a task which does nothing:

Code: Select all

async def dolittle():
    while True:
        await uasyncio.sleep(0)
This prevents the scheduler from going into a wait state.

I can't see an obvious and efficient solution - my attempt to use an Event was unsuccessful. I have raised an issue and we'll see what the maintainers have to say.

My Message class can interface between hard ISR's and other coroutines but it just uses a busy-wait so is not an elegant solution. However it supports the same API as uasyncio.Event. The maintainers intend that Event should be capable of being triggered from a soft ISR context, and we should see this in the next release (V1.14). So I suggest you use Message, substituting uasyncio.Event when it acquires the capability.
Peter Hinch
Index to my micropython libraries.

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

uasyncio ISR interfacing

Post by pythoncoder » Sun Sep 06, 2020 11:49 am

To add to this, @kevinkk525 demonstrated that setting an Event in an ISR is unsafe. For the same reasons this almost certainly applies to calling .create_task().

The Message class provides a crude ISR interface until uasyncio.Event is fixed to support setting in a soft ISR.
Peter Hinch
Index to my micropython libraries.

Post Reply