Ecrire du code robuste

Ecrire du code robuste

source:jarekgrafik@pixabayCette semaine, on se propose de vous montrer quelques trucs et astuces de programmation pour rendre plus robuste une application qui utilise des modules Yoctopuce. C'est important si vous voulez construire des installations qui doivent tourner 24/7 sans faillir.

Cet article est illustré d'exemples en Python, mais l'essentiel de ce qui est expliqué ici est aussi applicable aux autres langages de programmation.

Un exemple (trop) simple

Imaginons l'exemple le plus simple qui puisse exister: lire la température mesurée par un Yocto-Temperature. Le code le plus simple qui soit pour lire en boucle les valeurs du capteur ressemble à ça:

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)



Dans une situation idéale, ce code fonctionne parfaitement

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



Un tel bout de code suffit amplement pour effectuer un test rapide du module, mais il présente plusieurs problèmes qui le rendent peu fiable, et donc inutilisable en production.

Test de YAPI.RegisterHub

Il est fortement recommandé de tester le résultat de YAPI.RegisterHub. Si cet appel n'a pas fonctionné, rien ne marchera. Notez que le meilleur endroit pour faire cet appel est le début de votre application et qu'il est inutile d'appeler YAPI.RegisterHub plusieurs fois avec les mêmes paramètres.

Si votre application accède à des modules à travers le réseau, vous aurez peut-être envie d'utiliser YAPI.PreregisterHub() à la place de YAPI.Registerhub(), mais cela vous obligera à modifier la façon dont les modules sont découverts.

Vérifier que le capteur est présent

L'appel à YTemperature.FirstTemperature() renverra la valeur "None" si aucun capteur n'a été trouvé. Si vous ne vérifiez pas le résultat, votre code va crasher quand le capteur n'est pas branché.

Vérifier que le capteur reste branché

Les modules Yoctopuce sont des modules USB et par conséquent, ils peuvent être débranchés à tout moment. Il est donc prudent de vérifier que chaque module est bien présent avant d'y accéder. Typiquement, dans l'exemple précédent, si vous débranchez le capteur en cours d'exécution, l'application va crasher comme dans l'exemple d'exécution ci-dessous.

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)


Notez qu'il n'y a pas forcement besoin de débrancher le cable USB pour qu'un module se déconnecte. Cela peut aussi arriver en cas d'alimentation insuffisante, ou encore sur des machines dont la partie USB a été un peu bâclée.

Pour palier à ce problème, on dispose de la fonction isOnline() qui permet de vérifier qu'un module est bien présent. Notez que ça ne constitue pas une protection à 100%. Rien ne garantit qu'un module ne sera pas débranché pile entre l'appel à isOnline() et l'appel au module.

Utiliser la fonction YAPI.Sleep()

La librairie Yoctopuce a besoin de prendre la main de temps en temps pour faire son travail, même lorsqu'il ne se passe pas grand-chose d'intéressant pour l'utilisateur. C'est pourquoi il est recommandé d'utiliser la fonction YAPI.Sleep() à la place de la fonction système correspondante. En plus de faire une attente non-active, cette fonction s'assure que l'API dispose du temps nécessaire pour faire son travail.

Si on tient compte de toutes ces remarques, on obtient un code un peu plus touffu qui constitue le minimum syndical pour un programme raisonnablement fiable.

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)


Avec ce code, si on débranche le module, non seulement ça ne plante plus, mais le problème est signalé.

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


Notez l'utilisation de sys.exit(1) pour quitter le programme en cas de problème non-récupérable. L'idée est de signaler au système d'exploitation qu'il y a eu un problème. Ainsi, si ce code est utilisé dans un deamon ou autre service apparenté, il serait possible d'utiliser les mécanismes du système d'exploitation pour le redémarrer automatiquement en cas de problème.

Améliorations supplémentaires

Il est possible d'améliorer encore le concept pour rendre notre code encore plus robuste.

Utiliser le mode Callback plutôt que le polling

Plutôt que d'interroger explicitement le module pour obtenir les valeurs, on peut lui demander de les communiquer automatiquement à intervalle régulier en utilisant le mécanisme de callback offert par l'API Yoctopuce.

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()


Du coup, la boucle principale ne contient plus que l'appel à YAPI.Sleep() et un appel pour s'assurer que l'API gérera bien les arrivées des modules Yoctopuce. L'avantage, c'est que si le module disparaît, le code ne crashera pas, les callbacks s’arrêteront et reprendront dès que le module reviendra. Évidement, vous aurez peut-être à ajouter un peu de code pour détecter une absence prolongée du module. Noter que la fréquence de callback est stockée dans le module, c'est pourquoi il ne faut pas oublier de sauver le changement dans la mémoire flash du module grâce à saveToFlash().

Ecrire un fichier de log

Il est prudent de faire en sorte que votre application consigne tous les évènements notables dans un fichier lisible par un humain, aussi appelé "fichier de logs". Ainsi, si quelque chose de bizarre se passe dans votre application, vous pourrez peut-être comprendre ce qui s'est passé après-coup en étudiant ce fichier de log. N'oubliez pas de capturer les logs de l'API. Ils contiennent souvent des informations précieuses en cas de problème de communication avec les modules Yoctopuce.

Cependant, n'oubliez pas de faire en sorte de ne pas remplir le disque avec ce fichier de logs, en d'autres termes, il faut être capable de tronquer ce fichier lorsqu'il devient trop grand.

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)


Capturer les exceptions

Une bonne pratique consiste à capturer et à enregistrer les exceptions qui n'ont pas été traitées, quitte à stopper l’exécution si elles se reproduisent plusieurs fois de suite. Le bon endroit pour cela est la boucle principale du programme.

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)


Le code complet

Après toutes ces améliorations, le programme complet ressemble à ceci:

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)


Il donne le même résultat que le code original, mais il résiste bien mieux à l'absence du capteur et laisse une trace dans un fichier de log.

L'exécution:

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


Les 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

On est passé d'un bout de code de 8 lignes qui ne marche que quand les conditions sont idéales à un programme d'une cinquantaine de lignes qui résiste à (presque) tout et qui laisse des traces permettant de reconstituer ce qui s'est passé en cas de problème. Ce qui tend à confirmer ce que certains semblent prendre pour mythe: dans un programme, l'essentiel du code sert à gérer proprement les problèmes potentiels.

Commenter aucun commentaire Retour au blog












Yoctopuce, get your stuff connected.