Writing robust code

Writing robust code

source:jarekgrafik@pixabayThis week we'll show you some programming tips and tricks to make an application that uses Yoctopuce modules more robust. This is important if you want to build sytems that need to run 24/7 without failing.

This post is illustrated with examples in Python, but most of what is explained here is also applicable to other programming languages.

A (way too) simple example

Let's imagine the simplest example that can exist: reading the temperature measured by a Yocto-Temperature. The simplest possible code to read the sensor values in a loop looks like this:

from yoctopuce.yocto_api import *
from yoctopuce.yocto_temperature import *

errmsg = YRefParam()
YAPI.RegisterHub("usb", errmsg)
sensor = YTemperature.FirstTemperature()
while (True):
  print(str(sensor.get_currentValue())+" "+sensor.get_unit())
  time.sleep(1)



In an ideal situation, this code works perfectly

Z:\>python main.py 24.75 °C 24.81 °C 24.81 °C 24.75 °C 24.75 °C



Such a piece of code is more than enough to perform a quick test of the module, but it has several problems that make it unreliable, and therefore unusable in production.

Testing YAPI.RegisterHub

We strongly recommend to test the result of YAPI.RegisterHub. If this call did not work, nothing will work. Note that the best place to make this call is at the beginning of your application and that it is useless to call YAPI.RegisterHub several times with the same parameters.

If your application accesses modules over the network, you may want to use YAPI.PreregisterHub() instead of YAPI.Registerhub(), but this requires you to change the way modules are discovered.

Check that the sensor is present

The call to YTemperature.FirstTemperature() returns the "None" value if no sensor was found. If you don't check the result, your code crashes when the sensor is not connected.

Check that the sensor stays connected

Yoctopuce modules are USB modules and they can be disconnected at any time. It is therefore prudent to check that each module is present before accessing it. Typically, in the previous example, if you unplug the sensor while running, the application crashes as in the example run below.

Z:\>python main.py 26.56 °C 26.62 °C 26.62 °C Traceback (most recent call last): File "Z:\main.py", line 8, in <module> print(str(sensor.get_currentValue())+" "+sensor.get_unit()) File "C:\Python39\lib\site-packages\yoctopuce\yocto_api.py", line 7168, in get_currentValue if self.load(YAPI._yapiContext.GetCacheValidity()) != YAPI.SUCCESS: File "C:\Python39\lib\site-packages\yoctopuce\yocto_api.py", line 5223, in load res = devRef.value.requestAPI(apiresRef, errmsgRef) File "C:\Python39\lib\site-packages\yoctopuce\yocto_api.py", line 4297, in requestAPI raise YAPI.YAPI_Exception(res, errmsgRef.value) yoctopuce.yocto_api.YAPI_Exception: Device not found (yapi:4027)


Note that it is not necessary to disconnect the USB cable for a module to disconnect. It can also happen in case of insufficient power supply, or on machines where the USB part has been a bit botched.

To solve this problem, we have the isOnline() function which allows you to check that a module is present. Note that this is not a 100% protection. There is no guarantee that a module will not be disconnected between the call to isOnline() and the call to the module.

Using the YAPI.Sleep() function

The Yoctopuce library needs to take over from time to time to do its job, even when there is not much interesting happening for the user. That is why we recommend to use the YAPI.Sleep() function instead of the corresponding system function. In addition to doing a non-active wait, this function makes sure that the API has enough time to do its job.

If we take all these remarks into account, we get a slightly thicker and more complex code which is the bare minimum for a reasonably reliable program.

from yoctopuce.yocto_api import *
from yoctopuce.yocto_temperature import *

errmsg = YRefParam()
if (YAPI.RegisterHub("usb", errmsg)!=YAPI.SUCCESS):
  print(errmsg.value)
  sys.exit(1)

sensor = YTemperature.FirstTemperature()
if (sensor==None):
  print("No temperature sensor found")
  sys.exit(1)

while (True):
  if  sensor.isOnline():
    print(str(sensor.get_currentValue())+" "+sensor.get_unit())
  else:
    print("sensor went offline")
  YAPI.Sleep(1000)


With this code, if you unplug the module, not only does it not crash anymore, but the problem is reported.

Z:\>python main.py 27.38 °C 27.5 °C 27.81 °C sensor went offline sensor went offline sensor went offline 29.0 °C 29.0 °C 29.0 °C 29.0 °C 28.94 °C


Note the use of sys.exit(1) to exit the program in case of a non-recoverable problem. The idea is to tell the operating system that there was a problem. Thus, if this code is used in a deamon or other related service, it would be possible to use the operating system's mechanisms to restart it automatically in case of a problem.

Further improvements

There is room for further improvement to make our code even more robust.

Using the callback mode instead of polling

Instead of explicitly polling the module to get the values, we can ask it to communicate them automatically at regular intervals by using the callback mechanism offered by the Yoctopuce API.

def valueCallback(source,measure):
  print (measure.get_averageValue())

sensor.set_reportFrequency("1/s")
sensor.get_module().saveToFlash()
sensor.registerTimedReportCallback(valueCallback)

while (True):
  YAPI.Sleep(1000)
  YAPI.UpdateDeviceList()


As a result, the main loop only contains the call to YAPI.Sleep() and a call to make sure that the API handles the arrival of Yoctopuce modules. The advantage is that if the module disappears, the code does not crash, the callbacks stop and resume as soon as the module returns. Of course, you may have to add some code to detect a prolonged absence of the module. Note that the callback frequency is stored in the module, so don't forget to save the change in the module's flash memory with saveToFlash().

Writing a log file

It is wise to make your application log all notable events in a human-readable file, also called a "log file". That way, if something weird happens in your application, you may be able to figure out what happened afterwards by studying this log file. Don't forget to capture the API logs. They often contain valuable information in case of communication problems with Yoctopuce modules.

However, don't forget to make sure that you don't fill the disk with this log file, in other words, you should be able to truncate this file when it gets too big.

def log(msg):
  logfile =  "main.log"
  line=  str(datetime.now()) + " "+msg
  with open("main.log", "a") as myfile:
    myfile.write(line+"\n")
  if os.path.getsize(logfile) > 32*1024:
    with open(logfile, 'r') as fin:
      lines = fin.read().splitlines(True)
    with open(logfile, 'w') as fout:
      start = int(len(lines)/10)
      fout.writelines(lines[start:])

YAPI.RegisterLogFunction(log)


Catching exceptions

A good practice is to catch and log exceptions that have not been handled, even if it means stopping the execution if they occur several times in a row. The right place for this is the main loop of the program.

while (True):
  try :
    YAPI.Sleep(1000)
    YAPI.UpdateDeviceList()
    errorCount = 0
  except Exception as e:
    log("An exception occured : "+str(e));
    errorCount = errorCount + 1
    if (errorCount>3):
       log("Giving up..")
       sys.exit(1)


The complete code

After all these improvements, the complete program looks like this:

from yoctopuce.yocto_api import *
from yoctopuce.yocto_temperature import *
from datetime import *

def log(msg,onscreenAsWell =False ):
  if (onscreenAsWell) : print(msg)
  logfile =  "main.log"
  line=  str(datetime.now()) + " "+msg

  with open("main.log", "a") as myfile:
    myfile.write(line+"\n")
  if os.path.getsize(logfile) > 32*1024:
    with open(logfile, 'r') as fin:
      lines = fin.read().splitlines(True)
    with open(logfile, 'w') as fout:
      start = int(len(lines)/10)
      fout.writelines(lines[start:])

lastCallback=datetime.now()
unit = ""

def valueCallback(source,measure):
  global lastCallback
  global unit
  print (str(measure.get_averageValue())+unit)
  lastCallback=datetime.now()

log("**** Hello, starting application...",False)

YAPI.RegisterLogFunction(log)

errmsg = YRefParam()
if (YAPI.RegisterHub("usb", errmsg)!=YAPI.SUCCESS):
  log(errmsg.value,True)
  sys.exit(1)

sensor = YTemperature.FirstTemperature()
if (sensor==None):
  log("No temperature sensor found",True)
  sys.exit(1)

unit = sensor.get_unit();
sensor.set_reportFrequency("1/s")
sensor.get_module().saveToFlash()
sensor.registerTimedReportCallback(valueCallback)

errorCount = 0

while (True):
  try :
    YAPI.Sleep(1000)
    YAPI.UpdateDeviceList()
    if  (datetime.now() - lastCallback).total_seconds()>=2:
      log("device is offline",True)
    errorCount = 0

  except Exception as e:
    log("An exception occured : "+str(e),True)
    errorCount = errorCount + 1
    if (errorCount>3):
       log("Giving up..")
       sys.exit(1)


It provides the same result as the original code, but it resists much better to the absence of the sensor and leaves a trace in a log file.

Execution:

Z:\>python main.py No temperature sensor found Z:\>python main.py 27.875°C 27.875°C 28.0°C device is offline device is offline device is offline device is offline device is offline 28.375°C 28.375°C 28.375°C 28.375°C


Logs:

2023-04-25 09:49:57.711059 **** Hello, starting application... 2023-04-25 09:49:57.785989 No temperature sensor found 2023-04-25 09:50:04.991446 **** Hello, starting application... 2023-04-25 09:50:05.011050 [1]ystream: 2314: Device TMPSENS1-1C0FA2 plugged 2023-04-25 09:50:08.191313 [1]ystream: 2613: yPacketDispatchReceive error: TMPSENS1-1C0FA2:0(ypkt_win:684): (1167)The device is not connected. 2023-04-25 09:50:08.191313 [1]ystream: 438: Error from idle TMPSENS1-1C0FA2(4) : TMPSENS1-1C0FA2:0(ypkt_win:684): (1167)The device is not connected. 2023-04-25 09:50:09.106175 [1]ystream: 2253: Device TMPSENS1-1C0FA2 unplugged 2023-04-25 09:50:10.116443 device is offline 2023-04-25 09:50:11.121668 device is offline 2023-04-25 09:50:12.136475 device is offline 2023-04-25 09:50:13.146248 device is offline 2023-04-25 09:50:14.161044 [1]ystream: 2314: Device TMPSENS1-1C0FA2 plugged



Conclusion

We went from an 8 line long piece of code that works only when the conditions are ideal to a program of about fifty lines that resists (almost) everything and that leaves traces allowing you to reconstitute what happened in case of a problem. This tends to confirm what some people seem to think is a myth: in a program, most of the code is used to cleanly handle potential problems.

Add a comment No comment yet Back to blog












Yoctopuce, get your stuff connected.