Using Timers on Pyboard 'D'.

The official PYBD running MicroPython, and its accessories.
Target audience: Users with a PYBD
Post Reply
nherriot
Posts: 22
Joined: Wed Oct 19, 2016 1:02 pm

Using Timers on Pyboard 'D'.

Post by nherriot » Mon Jun 17, 2019 3:58 pm

Hi Micropython'ers,

I'm scratching my head at what on the face of it is a simple problem. And looking for some advice. I'm writing a simple Wifi_manager class for a project I'm on which simply monitors my Wifi connection and if it drops, tries to re-connect.

To solve this problem, when the user uses creates a Wifi_manager object and asks it to 'connect' it will attach a 'callback' to a timer object. Every time the timer pops the callback will check the Wifi connection using the isconnected() method. If it's not connected it will try and reconnect.

1) Is there a better way of doing this? i.e. should I be using timers?

2) I've got most of the methods all working. But when the Wifi connection does drop my callback fails and I get an error:

Code: Select all

sdio_transfer timeout STA=0x00400004
Then i get more errors:

Code: Select all

cyw43_kso_set(1): failed
sdio_transfer_cmd53: timeout wr=1 len=64 dma=1 buf_idx=0 STA=00400040 SDMMC=00000000:00000000 DMA=00000000:00000000:0000ffff RCC=2050017f
[CYW43] do_ioctl(2, 263, 19): timeout
3) Can someone point me to a good resource on 'timers' on pyboard with lots of examples? :-)
This could be an actual bug - but wanted to check here first before reporting it.

The code is just alfa at the moment so please excuse the mess!
https://github.com/SamsungResearchUK-Io ... connect.py


To reproduce the problem you need to copy the wifi_connect.py to your pyboard 'd'.
Then do:

Code: Select all

>>> import wifi_connect
>>> from wifi_connect import Wifi_manager
>>> 
>>> mw = Wifi_manager()                             # Create my wifi manager object
WiFi Manager bringing up wlan interface.
>>> 
>>> mw.status()                                             # Get staus of the wifi manager
(True, {'Retries': 3, 'Started': 614099554, 'SSID name': None, 'Current IP address': '192.168.123.105', 'Active monitor': False, 'Connected': True, 'Password': None})
>>> 
>>> mw.retries()                                            # Check how many retries it will attempt when initially connecting.
3
>>> mw.retries(8)                                          # Set retries to 8 - wifi is really poor in this area.
(True, 'Retry set to 8 retries')
>>> mw.connect('srbackup', '**********')         # Connect to my wifi network 'srbackup' and provide password.
If you get this far - i have a debug print line every time the manager checks the connection which will look like this:

Code: Select all

>>> 
>>> Connection up
Connection up
Connection up
Connection up
Connection up
Connection up
All good at this point! It checks approx every 10 seconds.
But if it runs a line of code at line number 163 for me:

Code: Select all

    def __check_connection(self, timer):  # we will receive the timer object when being called
        """
         The private method __check_connection is called by a timer as a callback.
         It's used to check the IP connection and try and reconnect in the event of the connection being pulled down.

         :return:
         """
        # print(timer.counter())              # show current timer's counter value
        if self.active:                     # Check first that we are required to try and reconnect
            if not self._wifi.isconnected():
                self.connected = False
                print("Warning: WiFi connection lost. Trying to reconnect")
 [b]               self._wifi.connect(self.ssid, self._password)[/b]                         # Line 163 problem
            else:
                #print("Connected on IP: {}".format(self.current_ip_address))
                print("Connection up")
                self.connected = True
        else:
            # OK - it looks like we should not be monitoring this connection. Could be a race condition. Make sure we stop monitoring!
            self._timer.deinit()
            print('Wifi Manager has stopped monitoring the connection')
 
I start getting errors.... :-( .....

Basically my object is calling up the low level connect method which is in the object attribute self._wifi.connect()
The other thing to note is the timer callback. I'm using this timer function within my object and it gets the hidden 'self' parameter as well as the timer object like this:

Code: Select all

        if not self.active:
            print("WiFi Manager is now monitoring the connection")
            self.active = True                                  # Let the WiFi manager store state on managing the WiFi network to true.
            self._timer = pyb.Timer(1, freq=0.1)                # create a timer object using timer 1 - trigger at 0.1Hz
            self._timer.callback(self.__check_connection)       # set the callback to our timer function
Notice I put the object attribute in the timer callback:

Code: Select all

self._timer.callback(self.__check_connection)
But when the timer calls this private method it gets passed 2 parameters, not one! LIke this:

Code: Select all

def __check_connection(self, timer):  # we will receive the timer object when being called
This all works fine until the method __check_connection tries to run this line I think:

Code: Select all

                self._wifi.connect(self.ssid, self._password)
So, totally stuck on this! Think I need more domain knowledge. Any help is appreciated.

Kind regards, Nicholas.

chuckbook
Posts: 107
Joined: Fri Oct 30, 2015 11:55 pm

Re: Using Timers on Pyboard 'D'.

Post by chuckbook » Mon Jun 17, 2019 5:58 pm

I didn't check in detail, but it looks like the callback method runs inside the interrupt service routine.
This should be avoided because of several restrictions that apply to IRQ handlers.
One of these restrictions is a locked heap within IRQ handlers.
To circumvent this, the IRQ service routine may schedule another method which is executed after IRQ handler exits
https://docs.micropython.org/en/latest/ ... rules.html

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

Re: Using Timers on Pyboard 'D'.

Post by pythoncoder » Mon Jun 17, 2019 6:07 pm

Ensuring a resilient WiFi connection is non trivial.

Maintaining a WiFi connection in the face of inevitable outages is best achieved using asynchronous programming (uasyncio). This enables better control than timers which can kick in at any point in the code. this doc describes the technical issues. The resilient MQTT library offers one solution, and this library offers a resilient socket-like object with an option for guaranteed message delivery.

If you fancy writing your own solution be warned: testing this stuff is very time consuming. There are many possible failure modes to be checked. There are also a lot of things that can go wrong when dealing with a failure. Sockets must be closed, and both ends of the interface need to detect the failure and to respond in a deterministic manner.

There is also an ongoing issue with the Pyboard D. This makes a resilient connection impossible at present but it will undoubtedly be fixed in due course.
Peter Hinch

nherriot
Posts: 22
Joined: Wed Oct 19, 2016 1:02 pm

Re: Using Timers on Pyboard 'D'.

Post by nherriot » Tue Jun 18, 2019 11:19 am

chuckbook wrote:
Mon Jun 17, 2019 5:58 pm
I didn't check in detail, but it looks like the callback method runs inside the interrupt service routine.
This should be avoided because of several restrictions that apply to IRQ handlers.
One of these restrictions is a locked heap within IRQ handlers.
To circumvent this, the IRQ service routine may schedule another method which is executed after IRQ handler exits
https://docs.micropython.org/en/latest/ ... rules.html
:-) Thanks for the quick response!!!!
Well I thought I took a lot of precaution :roll: i.e.
1) When I initialise the object the __init__ creates a bound function in:

Code: Select all

self._wifi = network.WLAN()                     # An instance of the network WLAN manager
In other words I'm making sure any allocation of memory for the object is done first before any timer objects are created and passed a callback. I'm assuming creating bound methods will require memory allocation.

2) I pass the bound method which was created in the constructor in the callback method. Which does work for the path that a debug message is printed to the screen. But not for the path were the bound method is actually calling a system call to reconnect! :-( ...

So probably the system call to 'connect' is what is causing the issue - maybe this has memory allocation happening etc...? The example I followed is herehttp://docs.micropython.org/en/latest/l ... Timer.html

I'm going to quickly try and use micropython.schedule to see what effects that have may have. The post from 'pythoncoder' has some good suggestions too that I'm looking at.
This post seems to be also similar: viewtopic.php?t=4027

All more difficult than I thought initially !

nherriot
Posts: 22
Joined: Wed Oct 19, 2016 1:02 pm

Re: Using Timers on Pyboard 'D'.

Post by nherriot » Thu Jun 20, 2019 11:28 am

pythoncoder wrote:
Mon Jun 17, 2019 6:07 pm
Ensuring a resilient WiFi connection is non trivial.

Maintaining a WiFi connection in the face of inevitable outages is best achieved using asynchronous programming (uasyncio). This enables better control than timers which can kick in at any point in the code. this doc describes the technical issues. The resilient MQTT library offers one solution, and this library offers a resilient socket-like object with an option for guaranteed message delivery.

If you fancy writing your own solution be warned: testing this stuff is very time consuming. There are many possible failure modes to be checked. There are also a lot of things that can go wrong when dealing with a failure. Sockets must be closed, and both ends of the interface need to detect the failure and to respond in a deterministic manner.

There is also an ongoing issue with the Pyboard D. This makes a resilient connection impossible at present but it will undoubtedly be fixed in due course.
Thanks pythoncoder :-)

Started looking at uasyncio, which looks like what I'll use going forward.
The documentation link is great too - so thanks for that.

The problem we have in this particular lab is actually Wifi dropping out - so this poor solution i have for now should work.
By the way, I've managed to isolate my particular bug and it's down to the 'format' method on printing a formatted string. I thought doing any sort of print on a string with passed in argument would be fine, but i was wrong! The 'format' method must allocate memory from the heap - I've created a little program that may help people in future if they come across the same issue. Enjoy!

Code: Select all

import pyb, network, utime, time, micropython

class Wifi_manager():
    """
        A pythonic Wifi Manager which will:
        1) Allow you to configure SSID and Password to connect and maintain/check that connection.
        2) Retry and connect if it's been asked to connect and the connection goes down.
        3) Use a WiFi SSID/Password from Flash if it exists.
        4) Encrypt the WiFi SSID/Password on Flash if it does not exist.
        5) Stop a connection and stop the retry/check
        6) Switch to an Access Point mode

        The Wifi manager has internal states for:
        - the current SSID
        - the current password
        - active (i.e. managing a connection )
        - connected
        - last connected local time
    """

    def __init__(self, ssid=None, password=None):
        self.ssid=ssid
        self._password = password
        self._timer = None                              # The timer object which handles periodic retries
        self._wifi = network.WLAN()                     # An instance of the network WLAN manager
        print("WiFi Manager bringing up wlan interface.")
        self.x = 0.1
        self.bar_bound = self.bar

    def bar(self, bar_value):
        self.x *= 1.2
        print("bar value is: {}".format(bar_value))
        print("SSID is: {}".format(self.ssid))
        print("Password is: {}".format(self._password))
        print("X value is: {}".format(self.x))
        if self.ssid is None:
            self.ssid = '********'
        if self._password is None:
            self._password = '********'
        self._wifi.connect(self.ssid, self._password)

    def cb(self, timer):
        # Passing self.bar would cause memory allocation failure.
        #print("Timer counter is: {}".format(timer.counter()))      # This causes a timer exception to be thrown when it's part of the callback.
        print(timer.counter())
        micropython.schedule(self.bar_bound, "hello world")

    def connect(self):
        print("connect 2 started.....")
        self._timer = pyb.Timer(1, freq=0.1)
        self._timer.callback(self.cb)
The line: print("Timer counter is: {}".format(timer.counter())) causes the issue.
This happens as a callback initiated by the timer.
I might put something in the Wiki about this... :? 8-)

Think I'm back on track now! Thanks for your help. :-)

Kind regards, Nicholas.

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

Re: Using Timers on Pyboard 'D'.

Post by pythoncoder » Fri Jun 21, 2019 5:43 am

If you consider how format works, it must allocate.

Code: Select all

'{:5d}'.format(42)
produces a string ' 42'. Arbitrary format statements can produce strings of any length, and in general the length can only be known at runtime (because the args are variables). So there is no option other than for it to allocate.
Peter Hinch

Post Reply