ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

All ESP32 boards running MicroPython.
Target audience: MicroPython users with an ESP32 board.
Online
User avatar
pythoncoder
Posts: 3652
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by pythoncoder » Sat Feb 09, 2019 9:58 am

It's hard to answer that without knowing more. For uasyncio compatibility you should use nonblocking sockets; these require some attention to detail to read and write from them. You'll find some example code in this repo. In particular look at the simple test client and server code in the temporary directory. The server is very minimal and can only connect one client: a full-fat server is in the root directory.
Peter Hinch

User avatar
iotman
Posts: 44
Joined: Sat Feb 02, 2019 4:06 pm
Location: Nanoose Bay, Canada
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by iotman » Sat Feb 09, 2019 6:13 pm

Hi Peter, thanks for your reply and pointers, it is appreciated.

I only use a very minimalist socket, so the temporary folder code looks familiar, and I already use a non-blocking socket, so I'll give that a try.

I think my biggest challenge is to stop thinking about coding sequentially and realize that co-routines require a complete re-structuring of a program. I'm already finding it to be a very useful mechanism that works really well, so I am about to completely revamp my coding to incorporate async.

I have another function where delays exist (posting to an external server), so I'm thinking it will help with that too.

Regards, AB

User avatar
iotman
Posts: 44
Joined: Sat Feb 02, 2019 4:06 pm
Location: Nanoose Bay, Canada
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by iotman » Sun Feb 10, 2019 12:42 am

Hi Peter, I have spent quite a few hours trying to make this work with your debounce code, specifically the "Test for the Pushbutton class (coroutines)". I am wanting to provide the ability to register button presses connected to input pins on the ESP32, and that code of yours works perfectly.

The problem arises when I try to combine the button press recognition with a web server that also provides inputs from the user. I am using some pretty basic socket code the run the web server, but I can't seem to combine these 2 things to work together. The web server just waits for input and the button presses don't register, so I presume I have it in the wrong place.

I have tried all kinds of different scenarios with the async coding, but I'll be danged if I can figure out what to do. I'm wondering if you have any suggestions?

I put the shortened version of the socket code below, and the button press code in a separate window below that. I know this should be relatively easy, but as a newbie I'm missing something, I'm sure.

Regards, AB

This is the web socket code:

Code: Select all

# define web server socket parameters
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setblocking(False) 
s.bind(('',80))
s.settimeout(None)

# listen for response
while True:

        # wait for browser action - either reload or form submission
        conn,addr=s.accept()
        request=conn.recv(1024)
        request=str(request)

        # check if it's an actual POST or just a refresh
        detect_post = request.find('POST')
        
        # actions
        if detect_post > 0:
        	#do something
        else:
        	# display web form
        	html_form = form_input(browser_tab_title)
            	conn.send(html_form)
            	conn.close()
            	html_form = ''
this is the button press code I am trying to combine it with (it's your code, expanded to accommodate 4 switches):

Code: Select all


# Quit test by setting var enable_switches
async def kill_switches():
    enable_switches = True
    while enable_switches:
        # this does not affect debounce but must be there
        await asyncio.sleep_ms(1)

# actions to perform (coroutine)
async def switch_activated(switch_id, action, delay):

    global oled_on

    if action == 'pressed' and debug: print('\n---\nSwitch ' + str(switch_id) + ' was pressed')
    if action == 'released' and debug: print('Switch ' + str(switch_id) + ' was released')
    if action == 'double-click' and debug: print('Switch ' + str(switch_id) + ' was registered')
    if action == 'long-press' and debug: print('Switch ' + str(switch_id) + ' was registered')
    
    if action == 'pressed' and oled_on == False and switch_id == 3: # turn on oled display
        oled.invert(0) # White text on black background
        oled.contrast(255) # Maximum contrast
        oled.fill(0)
        oled.text("WiFi connected!", 0, 0)
        oled.show()
        oled_on = True
        if debug: print('OLED turned on by user.')
        
        # turn off oled after delay
        await asyncio.sleep_ms(10000)
        oled.invert(0) # White text on black background
        oled.contrast(0) # Minimum contrast
        oled.fill(0)
        oled.show()
        oled_on = False
        if debug: print('OLED timed out.')
        
    elif action == 'pressed' and switch_id == 3: # turn off oled display
        oled.invert(0) # White text on black background
        oled.contrast(0) # Minimum contrast
        oled.fill(0)
        oled.show()
        oled_on = False
        if debug: print('OLED turned off by user.')   
    
    # this delay has nothing to do with debounce    
    # await asyncio.sleep_ms(delay)
    #if action == 'pressed': print ('pause timer done\n---\n')


# Test for the Pushbutton class (coroutines)
# Pass True to test suppress
def load_switch_async(suppress1, dc1, lp1, suppress2, dc2, lp2,suppress3, dc3, lp3,suppress4, dc4, lp4):
    print('Switches are enabled: scheduling coroutines.')
    print(helptext)

    # CONFIGURE SWITCH 1
    pin1 = Pin(36, Pin.IN, None) # pin 36 does not have internal resistor
    sw1 = Pushbutton(pin1, suppress1)

    # CONFIGURE SWITCH 2
    pin2 = Pin(25, Pin.IN, Pin.PULL_DOWN) # pin 25 has internal resistor
    sw2 = Pushbutton(pin2, suppress2)

    # CONFIGURE SWITCH 3
    pin3 = Pin(26, Pin.IN, Pin.PULL_DOWN) # pin 26 has internal resistor
    sw3 = Pushbutton(pin3, suppress3)

    # CONFIGURE SWITCH 4
    pin4 = Pin(39, Pin.IN, None) # pin 39 does not have internal resistor
    sw4 = Pushbutton(pin4, suppress4)

    delay = 10 #msec

    # Switch 1 actions
    sw1.press_func(switch_activated, (1, 'pressed', delay))
    sw1.release_func(switch_activated, (1, 'released', delay))
    if lp1: 
        sw1.long_func(switch_activated, (1, 'long-press', delay))
        if debug: print('Long Press on Switch 1 is enabled')
    if dc1:
        sw1.double_func(switch_activated, (1, 'double-click', delay))
        if debug: print('Double Click on Switch 1 is enabled')

    # Switch 2 actions
    sw2.press_func(switch_activated, (2, 'pressed', delay))
    sw2.release_func(switch_activated, (2, 'released', delay))
    if lp2: 
        sw2.long_func(switch_activated, (2, 'long-press', delay))
        if debug: print('Long Press on Switch 2 is enabled')
    if dc2:
        sw2.double_func(switch_activated, (2, 'double-click', delay))
        if debug: print('Double Click on Switch 2 is enabled')

    # Switch 3 actions
    sw3.press_func(switch_activated, (3, 'pressed', delay))
    sw3.release_func(switch_activated, (3, 'released', delay))
    if lp3: 
        sw3.long_func(switch_activated, (3, 'long-press', delay))
        if debug: print('Long Press on Switch 3 is enabled')
    if dc3:
        sw3.double_func(switch_activated, (3, 'double-click', delay))
        if debug: print('Double Click on Switch 3 is enabled')

    # Switch 4 actions
    sw4.press_func(switch_activated, (4, 'pressed', delay))
    sw4.release_func(switch_activated, (4, 'released', delay))
    if lp4: 
        sw4.long_func(switch_activated, (4, 'long-press', delay))
        if debug: print('Long Press on Switch 4 is enabled')
    if dc4:
        sw4.double_func(switch_activated, (4, 'double-click', delay))
        if debug: print('Double Click on Switch 4 is enabled')

    # maintain loop
    loop = asyncio.get_event_loop()
    #loop.run_forever()
    loop.run_until_complete(kill_switches())

# END MODIFIED TEST
# ======================================

# start async switch routine 
load_switch_async(suppress1=False, dc1=False, lp1=False, suppress2=False, dc2=False, lp2=False,suppress3=False, dc3=False, lp3=False,suppress4=False, dc4=False, lp4=True)
Last edited by iotman on Sun Feb 10, 2019 2:39 pm, edited 1 time in total.

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

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by pythoncoder » Sun Feb 10, 2019 7:38 am

You set s to nonblocking, but the new socket conn returned by s.accept is blocking. You will need to adapt the method of reading from it accordingly.

I'm not sure that making s nonblocking is wise. If you look at the server code in the repo I referenced, the run function at the start illustrates the normal way of using a server socket: it is set to blocking mode but polled using the select mechanism. This makes the function nonblocking (well, it blocks for 1ms nominal).

In general if async code (such as my pushbutton driver) becomes unresponsive in an application it is because something is blocking. You need to track down the culprit.
Peter Hinch

User avatar
iotman
Posts: 44
Joined: Sat Feb 02, 2019 4:06 pm
Location: Nanoose Bay, Canada
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by iotman » Sun Feb 10, 2019 3:27 pm

Hi Peter, I did not realize I was using something that was blocking; I had assumed wrongly that setting s as non-blocking would be enough; thanks for pointing that out. I was hoping for a simpler solution, but down the rabbit hole I go ...

So it seems I have to use the polling version of the socket if I want it to work as a co-routine under the asyncio system. I copied the code you had pointed to, starting with the web server. Here is the link, just to be certain we're on the same page:

https://github.com/peterhinch/micropyth ... ry/serv.py

I added a little bit of code to make sure the wifi was active. I was expecting to see the text 'Message from server.' when I load the IP address in my browser (I set it to the default port of 80).

But it won't run, I get an OSError: 112 at line 40, which is:

s_sock.bind(addr)

According to this list of error codes:

http://www.ioplex.com/~miallen/errcmp.html

the error is EHOSTDOWN 112: Host is down

I'm not certain that is the correct error code, though. Do you happen to know where there is a specific list of Micropython error codes? I have had a lot of trouble finding that.

So back to the problem: am I expecting the correct thing with that text appearing in my browser?

I have put the code below in its entirety.

Regards and thanks for the help, AB

Code: Select all

# Run under CPython 3.5+ or MicroPython Unix build
# Aims to detect missed messages on socket where connection is via WiFi

import sys
upython = sys.implementation.name == 'micropython'
if upython:
    import usocket as socket
    import uasyncio as asyncio
    import uselect as select
    import uerrno as errno
    import ujson as json
    import utime as time
else:
    import socket
    import asyncio
    import select
    import errno
    import json
    import time

# get wifi IP address and network name
import network

wlan = network.WLAN(network.STA_IF)
wifi_info_tuple=wlan.ifconfig()
wifi_info_array=list(wifi_info_tuple)
ip_address=wifi_info_array[0]
ssid = wlan.config('essid')

print('IP Address is: ' + ip_address)

PORT = 80

print('Port: ' + str(PORT))

async def run(loop):
    addr = socket.getaddrinfo('192.168.1.44', PORT, 0, socket.SOCK_STREAM)[0][-1]
    s_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # server socket
    s_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s_sock.bind(addr)
    s_sock.listen(5)
    print('Awaiting connection on', PORT)
    poller = select.poll()
    poller.register(s_sock, select.POLLIN)
    while True:
        res = poller.poll(1)
        if res:
            c_sock, _ = s_sock.accept()  # get client socket
            c_sock.setblocking(False)
            loop.create_task(reader(c_sock))
            loop.create_task(writer(c_sock))
        await asyncio.sleep(0.2)

async def reader(sock):
    print('Reader start')
    istr = ''
    last = -1
    while True:
        try:
            d = sock.recv(4096)  # bytes object
        except OSError as e:
            err = e.args[0]
            if err == errno.EAGAIN:  # Would block: try later
                await asyncio.sleep(0.05)
        else:
            if d == b'':  # Reset by peer
                raise OSError('Client fail.')
            istr += d.decode()  # Add to any partial message
            # Strings from this point
            l = istr.split('\n')
            istr = l.pop()  # '' unless partial line
            for line in l:
                data = json.loads(line)
                print('Got', data)
                if last >= 0 and data[0] - last -1:
                    raise OSError('Missed message')
                last = data[0]

async def writer(sock):
    print('Writer start')
    data = [0, 'Message from server.']
    while True:
        m = '{}\n'.format(json.dumps(data))
        await send(sock, m.encode('utf8'))
        data[0] += 1
        print('sent', m)
        await asyncio.sleep(0.25)  # ???

async def send(sock, d):
    while d:
        try:
            ns = sock.send(d)  # Raise OSError if client fails
        except OSError as e:
            err = e.args[0]
            if err == errno.EAGAIN:  # Would block: try later
                await asyncio.sleep(0.1)
        else:
            d = d[ns:]
            if d:
                await asyncio.sleep(0.05)

loop = asyncio.get_event_loop()
loop.create_task(run(loop))
loop.run_forever()


User avatar
iotman
Posts: 44
Joined: Sat Feb 02, 2019 4:06 pm
Location: Nanoose Bay, Canada
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by iotman » Sun Feb 10, 2019 10:09 pm

Hi Peter, one other thought I had was to put the button press code into an interrupt timer, and leave the socket code as is. Do you think that might work?

It would foil the attempt at a full async solution, but I'm feeling a bit leery of the polling socket code as so much of my code is involved with the wifi aspect.

Regards, AB

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

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by pythoncoder » Mon Feb 11, 2019 10:29 am

That link is to a very rudimentary server intended for a specific test. The one I had in mind is this one.

There are two sockets to consider. The server socket performs the connection and accept returns a client socket: one for each client which connects. I use the server socket in blocking mode with a poller object reducing blocking time to 1ms. I set the client sockets to nonblocking mode. However some programmers advocate using blocking sockets and polling throughout. I think it's largely a matter of personal choice, but polling may be more efficient with large numbers of connected clients.

My personal view is that, if you're serious about writing firmware, it's well worth using uasyncio. Cooperative multi tasking greatly simplifies any code which has to handle multiple asynchronous events.
Peter Hinch

User avatar
iotman
Posts: 44
Joined: Sat Feb 02, 2019 4:06 pm
Location: Nanoose Bay, Canada
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by iotman » Mon Feb 11, 2019 5:55 pm

Hi Peter, thanks for your reply.

I agree that I should try to get the async approach working, but I can't seem to get any of the socket code working. I tried that more complex server code, but immediately got an error:

File "a8.py", line 15, in <module>
ValueError: cannot perform relative import

I am only using the web server to set some parameters and control relays, so it definitely does not need to be a fully fledged system. I'm hoping I can get the simpler examples working first, so that I can better understand how the async polling works, and that may also be all I need for his project.

Do you have any idea why I could not run that code in the temporary folder, with regards to the previous post where I get this error:

the error is EHOSTDOWN 112: Host is down

Regards, AB

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

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by pythoncoder » Tue Feb 12, 2019 5:57 pm

I think it's trying to tell you that there is no server on 192.168.1.44 port 80.
Peter Hinch

User avatar
iotman
Posts: 44
Joined: Sat Feb 02, 2019 4:06 pm
Location: Nanoose Bay, Canada
Contact:

Re: ESP32 Debouncing Switches & Relays: IRQ Timer vs Simple Code vs ASYNC

Post by iotman » Tue Feb 12, 2019 6:10 pm

Hi Peter, I'm surprised that it seems so difficult to monitor inputs and run a web server at the same time; I would have thought a lot of folks would want to do that.

I'll keep working on it, studying the code to figure out the polling. I'm also going to try and get that fully fledged web server running, although there is a lot of code in there that is waaay above my head, but I will look at it as a learning experience.

Can you possibly explain that initial problem I'm having, with regards to the import on line 15? I have never seen anything like that importing syntax ...

from . import gmid, isnew

Tks, AB

Post Reply