Handling Exceptions when Using Uasyncio v3

All ESP32 boards running MicroPython.
Target audience: MicroPython users with an ESP32 board.
Post Reply
ignilux
Posts: 4
Joined: Thu Sep 24, 2020 9:03 pm

Handling Exceptions when Using Uasyncio v3

Post by ignilux » Thu Sep 24, 2020 9:23 pm

All-

I'm currently using an ESP devkit C v4 board to host a minimal website, locally accessible over mDNS. I've been using uasyncio v3 to handle multitasking and scheduling, and so far it performs great. However, on occasion an OSError EADDRINUSE is thrown, which I understand will happen when attempting to open a socket on an address already bound to another socket. My question pertains to handling this specific exception in the context of a uasyncio task.

The usual approach to managing the EADDRINUSE error is to use setsocketopt() to set SO_REUSEADDR. However, uasyncio handles the socket creation in the background, and I was unable to find any way to access the socket object. With that off the table, my admittedly crude solution was to use a GPIO pin to trigger a monostable that hard resets the board. The issue here is that I seem to be unable to trap the specific exception no matter what I try. The uasyncio v3 tutorial seems to suggest that exceptions be handled in the following way:

Code: Select all

import uerrno as errno
...
try:
    asyncio.run(server.run())
except OSError as oserr:
    if oserr.args[0] == errno.EADDRINUSE:
        p13.on()	# Previously instantiated machine.Pin object controlling reset
except KeyboardInterrupt:
    print('Interrupted')  # This mechanism doesn't work on Unix build.
finally:
    server.close()
    _ = asyncio.new_event_loop()
The implementation above did not catch the OSError when manually thrown via the served website. I've also tried wrapping all of main in a giant try/except block, using the set_global_exception() function to handle the error, and nothing seems to do the business. I should note that toggling pin 13 via the REPL properly resets the device. Any advice? I'm happy to share any more code that may be relevant, but I've kept it brief as I'm not sure what's important and what's just clutter.

Cheers,
-Erik

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

Re: Handling Exceptions when Using Uasyncio v3

Post by pythoncoder » Fri Sep 25, 2020 7:20 am

What actually happens when the exception occurs? Do you get a traceback at the REPL? If so, it might be worth posting it.

How are you using uasyncio? Are you using start_server()?
Peter Hinch
Index to my micropython libraries.

ignilux
Posts: 4
Joined: Thu Sep 24, 2020 9:03 pm

Re: Handling Exceptions when Using Uasyncio v3

Post by ignilux » Fri Sep 25, 2020 12:43 pm

Hi, Peter-

Thanks for your response. I'm using a modified version of the userver.py example in your Github repository to serve the site. In my first post, the asyncio.run(server.run()) call resides in my main.py, and it calls the following method in a separate userver.py:

Code: Select all

async def run(self):
        print('Awaiting client connection.')
        self.cid = 0
        self.server = await asyncio.start_server(self.run_client, self.host, self.port, self.backlog)
        while True:
            await asyncio.sleep(100)
So indeed start_server() is called. I'm manually throwing the OSError exception by using a task named token_check to check for requests for mylamewebsite.local/?oserror=true, and throwing the exception if it's found:

Code: Select all

# Checks tokens from userver
async def token_check():
    while True:
        found = server.get_found_tokens()
        # if certain thing found
        	# do this
        # elif other thing found
        	# do this
        elif found[2] == True:
            raise OSError(errno.EADDRINUSE)
        
        server.reset_tokens_present()       # Reset to avoid false positives

        server.set_resp(site.get_html())    # Update server with latest data
        await asyncio.sleep(5)              # Check every 5 seconds
I believe the exception must be raised properly, because the following traceback posts to the REPL:

Code: Select all

Traceback (most recent call last):
  File "uasyncio/core.py", line 1, in run_until_complete
  File "main.py", line 137, in token_check
OSError: [Errno 98] EADDRINUSE
MicroPython v1.13 on 2020-09-02; ESP32 module with ESP32
Type "help()" for more information.
>>>
At this point I can trigger a "natural" EADDRINUSE event by importing main:

Code: Select all

>>> import main
Awaiting client connection.
Traceback (most recent call last):
  File "uasyncio/core.py", line 1, in run_until_complete
  File "uasyncio/stream.py", line 1, in _serve
OSError: [Errno 98] EADDRINUSE
Which, as you can see is identical except for the location from which it was raised. I'll admit that I'm fairly new to Python, and that there may be a large, obvious problem that I'm not seeing. I'd appreciate any further input you may have.

Cheers,
-Erik

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

Re: Handling Exceptions when Using Uasyncio v3

Post by pythoncoder » Sat Sep 26, 2020 5:44 am

How are you running token_check()? If you are using asyncio.create_task() there is no calling context which can trap the exception. You need to await the task:

Code: Select all

async def foo():
    try:
        await token_check()
    except OSError as err:
        # Handle the exception
As a general point my userver.py is intended as a simple demo of the use of sockets at low level. Others have written web servers which may be better suited to your application - PicoWeb and MicroWebSrv2 come to mind, but a forum search may throw up others.
Peter Hinch
Index to my micropython libraries.

ignilux
Posts: 4
Joined: Thu Sep 24, 2020 9:03 pm

Re: Handling Exceptions when Using Uasyncio v3

Post by ignilux » Sat Sep 26, 2020 1:38 pm

token_check() is indeed created with create_task(), and that makes sense in the context of manually throwing the exception, but I don't see how it extends to the actual scenario when there is a socket bound to the desired address. The main loop is initiated when calling asyncio.run(server.run()), which is why I wrapped it in the try/except statements. It also doesn't explain why a keyboard interrupt is properly caught in this manner, but not the OSError.

I realize that there may be other server frameworks better suited to my needs, but for the sake of learning I'd like to wrap my head around what's happening here. From my perspective it looks like my try/except needs to be within the uasyncio library files themselves, but clearly that's not the case.

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

Re: Handling Exceptions when Using Uasyncio v3

Post by kevinkk525 » Sat Sep 26, 2020 3:28 pm

See https://github.com/micropython/micropython/pull/5796 on how to handle exceptions in micropython uasyncio and CPython asyncio.
Kevin Köck
Micropython Smarthome Firmware (with Home-Assistant integration): https://github.com/kevinkk525/pysmartnode

ignilux
Posts: 4
Joined: Thu Sep 24, 2020 9:03 pm

Re: Handling Exceptions when Using Uasyncio v3

Post by ignilux » Sat Sep 26, 2020 6:38 pm

kevinkk525-

That was exactly what I needed! Specifically, I hadn't thought to look at the CPython documentation, which made it clear to me. I had played around with the handle_exception(loop, context) function, but the MicroPython documentation didn't mention that the context dictionary contains a copy of the exception object. Learning that made it easy to handle the EADDRINUSE in the intended manner.

Thanks everyone!

Post Reply