uasyncio --- implementation of interrupts

Discussion about programs, libraries and tools that work with MicroPython. Mostly these are provided by a third party.
Target audience: All users and developers of MicroPython.
Post Reply
doceave
Posts: 26
Joined: Fri Feb 14, 2020 4:02 pm

uasyncio --- implementation of interrupts

Post by doceave » Wed Apr 01, 2020 6:32 am

Hi there all

I am certain there is a way to solve this problem:
> I have a number of hardware interrupts that are served as follows (only one IRQ displayed for example purposes)
> When a button press is detected I need the function ACT_Now_R() to run
- when it begins running it sets the variable hs_status_r to "Running" such that the IRQ will not call the function if it is already running
- this function takes many minutes to complete
- when the function has completed it sets hs_status_r back to "Ready" such that the IRQ should again be able to trigger the function to run?
--- This is however not happening
--- I fear I am not properly implementing uasyncio

How does one, using uasyncio, call a function to run asap and return immediately, and then also be able to again call that same function, sometime much later, once it has completed running?
- In layman terms, I want the function ACT_Now_R() to run and then "cancel itself" when complete such that can be called again afresh by the IRQ polling which will run continuously.

Perhaps even more useful to the community, and which would answer my question, would be a very simple example of how a single button may be used to start a time consuming function (if not already running) repeatedly on button press.

Code: Select all

########################################################
#### IRQ Handlers ####
def IRQ_handler_tip_right(pin):
    global IRQ_flag
    global flag_tip_right
    print ("Right tip SW pressed...")
    IRQ_flag = 1
    flag_tip_right = 1
########################################################
    
    
########################################################
#### IRQs #####
irq_tip_right.irq(trigger=machine.Pin.IRQ_RISING, handler=IRQ_handler_tip_right)
########################################################


########################################################
#### IRQ polling ####
async def IRQ_poll():
    global system_ready
    global IRQ_flag
    global flag_tip_right
    while True:
        await asyncio.sleep_ms(100)
        if (IRQ_flag == 1) and (system_ready == 1):
            if (flag_tip_right == 1) and (hs_status_r == "Ready"):
                flag_tip_right = 0
                irq_loop = asyncio.get_event_loop()
                irq_loop.create_task(ACT_Now_R()) ## Will require update of this function
            else:
                flag_tip_right = 0
            ####################
        IRQ_flag = 0
########################################################


########################################################
#### Run through initial test sequence #####

## Load settings from file:
loop = asyncio.get_event_loop()
loop.run_until_complete(json_get_settings())

## Attempt connect to Wifi:
loop.run_until_complete(wifi_connect())

## Start persistent loops:
loop.create_task(get_temperatures())
loop.run_until_complete(snooze_ms(1000))
loop.create_task(PID_heater_control())

## Installation sequence:
loop.run_until_complete(test_sequence())
loop.create_task(home_screen())
loop.create_task(IRQ_poll())

## Now run the actual ACT process:
system_ready = 1
loop.run_forever()

Thanks again for the excellent support offered here.

EDIT:
> It now seems to work. I changed something small and now the function returns and can be called repeatedly
> I will leave the question up such that perhaps it may be followed by a succinct, simple example of how to handle bulky functions that should be run as async tasks when triggered by IRQ's....

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

Re: uasyncio --- implementation of interrupts

Post by pythoncoder » Wed Apr 01, 2020 8:33 am

My advice is not to use IRQ's for button pressed. Interrupts are for cases where μs response is required. With good application design uasyncio is perfectly capable of responding to, and debouncing, pushbuttons without recourse to interrupts. See the Switch and Pushbutton classes in this repo.
Peter Hinch

doceave
Posts: 26
Joined: Fri Feb 14, 2020 4:02 pm

Re: uasyncio --- implementation of interrupts

Post by doceave » Thu Apr 02, 2020 7:10 pm

Hi there Peter

I am learning... and every time I meet a problem I learn more how epic uasyncio is!

I endeavor to remove the interrupts as you have suggested.

Thanks and keep well

derp
Posts: 3
Joined: Mon May 18, 2020 11:52 pm

Re: uasyncio --- implementation of interrupts

Post by derp » Tue May 19, 2020 3:35 am

Thanks, pythoncoder. So, are you saying to just poll every 10-100 ms ? That seems kind of wasteful (I think that this would be an issue if you are trying to run on battery). Surely, there is a good way to mix interrupts and asyncio without frequent polling?

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

Re: uasyncio --- implementation of interrupts

Post by pythoncoder » Wed May 20, 2020 8:03 am

Currently uasyncio does not support low power operation as the scheduler runs continuously. This has been discussed. Enabling low power support would involve changes to the polling mechanism in the uselect module. There is also the fact that the timebase for uasyncio stops when the board goes into lightsleep.

I did some rather experimental work on uasyncio V2 which replaced the timebase with one based on the RTC, and simply ran a task which put the board into lightsleep for a period each time it was scheduled. Obviously that imposed latency on all the other tasks. Its performance was inconsistent, sometimes greatly reducing power consumption and sometimes not. I'm not going to try this with the new version as it's too hacky even for me ;) Hopefully @Damien will produce a proper solution in due course.

As for polling pins it is inefficient. However if you look at my code, the normal path through the switchcheck coroutine is

Code: Select all

    async def switchcheck(self):
        while True:
            state = self.pin.value()
            if state != self.switchstate:
                # This is not the normal path
            await asyncio.sleep_ms(Switch.debounce_ms)  # 50ms
My guess is that reading the pin and checking its value might take, say, 80μs per loop. The scheduler has an overhead of about 250μs. So the task might use 330μs of CPU time every 50ms, an overhead of 0.66%. I would defend polling on grounds of simplicity.
Peter Hinch

User avatar
tve
Posts: 214
Joined: Wed Jan 01, 2020 10:12 pm
Location: Santa Barbara, CA
Contact:

Re: uasyncio --- implementation of interrupts

Post by tve » Wed May 20, 2020 4:54 pm

To answer the OP's original question, the pattern I like the most is to use a persistent task (`while True` loop) for your long running function and have it block on a asyncio.Event at the top of each iteration. The "interrupt" function can then set the Event to "launch the function". In the current state of MP & uasyncio I would recommend to use polling instead of interrupts but to use the same Event signaling, this way you can swap the polling for true interrupts once those become supported by uasyncio.

derp
Posts: 3
Joined: Mon May 18, 2020 11:52 pm

Re: uasyncio --- implementation of interrupts

Post by derp » Fri May 22, 2020 11:02 pm

Thanks, guys.

I'm pretty disappointed that poling from within an EventLoop is the "best way" available right now, but it's good to know. Hopefully, this gets addressed soon.

Ideally, what I'd like to see is for MP to add the call_soon_threadsafe method to EventLoop (call_soon_threadsafe is available in standard asyncio). Then, you'd call that from interrupt handlers. E.g.

Code: Select all

async def monitor_switch(sw: Pin, sw_changed: Event):
  previous_value = sw.value()
  while True:
    try:
      await uasyncio.wait_for(switch_changed.wait(), timeout=1.0)
    except TimeoutError:
      # One would expect that sw.value is now != previous_value,
      # but I suppose it might not be due to a race...
      print('Still alive.')

      if is_it_time_to_give_up():
        return  # or raise or whatever

      continue  # Try again, aka keep waiting for sw to change.

    # Acknowledge event. Otherwise, switch_changed.wait will
    # return immediately during the next iteration.
    switch_changed.clear()

    # switch_changed.wait is telling us that now is a
    # good time to read sw.
    new_value = sw.value()

    # THIS IS WHERE THE REAL WORK IS PERFORMED!
    if new_value:
      handle_high()
    else:
      handle_low()

    # Get ready for the next iteration.
    previous_value = new_value

sw = Pin(...)
sw_changed = Event()
sw.irq(trigger=..., handler=lambda sw_unused: event_loop.call_soon_threadsafe(Event.set, sw_changed))
I had already tried using Event to communicate to a running coroutine (running in an EventLoop). However, unlike what tve suggested, I did not try polling from within an EventLoop to trigger the Event. Instead, I tried setting the Event using interrupts. I tried a few variations of this, but none of them seem to work. Instead, what seems to happen is that uasyncio does not notice that the Event has been set until after wait_for times out, which rather defeats the purpose. The fact that all of my attempts failed isn't too surprising because there seems to be no way to safely call an EventLoop method from outside a running EventLoop in MP. In standard asyncio, that is what call_soon_threadsafe is for, which is why I think MP really needs that method in order to bridge from the world of interrupts to the world of (u)asyncio.

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

Re: uasyncio --- implementation of interrupts

Post by pythoncoder » Sat May 23, 2020 7:53 am

This is under active development and you might like to follow (or contribute to) this RFC.
Peter Hinch

Post Reply