ACTIVITE PYTHON : récupérer les données série d’une balance (envoyées par liaison USB)

Objectif de cette activité : Récupérer dans Python les données d’une balance « interfaçable » ou d’un autre type d’appareil avec connexion USB ou série.

Pré-requis : être familier avec le langage Python

Pour l’initiation au langage Python , je vous invite à consulter cet article : Initiation au langage Python

Nous avons au laboratoire une balance avec prise USB et prise RS232. Nous souhaitons récupérer les données mais le logiciel du fournisseur est un peu coûteux. Notre but est donc de voir si cette balance envoie spontanément des messages de type chaîne de caractères à intervalle régulier. Si c’est le cas, il pourrait être possible de récupérer ces données avec le langage Python, et donc de réaliser des acquisitions temporelles avec tracé de graphe, exporter les données au format txt,…

Il a fallu paramétrer préalablement cette balance pour activer le port USB et régler le débit à 9600 bauds.

Vérifier que la bibliothèque serial est installée en écrivant dans le shell (interpréteur) import serial. Si un message d’erreur apparaît, installer serial en écrivant dans le shell :

pip install serial

Etape 1 : Récupérer les valeurs dans Python pour les stocker dans une liste

Pour la communication avec la liaison série de la carte Arduino (via le câble USB), il faut importer la bibliothèque serial en écrivant ces deux lignes :

#Importation des modules
import serial
import serial.tools.list_ports   # pour la communication avec le port série

On initialise la liste pour les mesures de masses:

#initialisation des listes
liste_masse = []

On définit une fonction qui permet à la fois :

  • de détecter automatiquement le port série utilisé par la carte Arduino
  • de se connecter à la carte
  • de récupérer les données série venant de la carte
# Fonction pour la récupération des données série venant de la balance
def recup_port_USB() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'USB' in p.description :
            mData = serial.Serial(p.device,9600)
    print(mData.is_open) # Affiche et vérifie que le port est ouvert
    print(mData.name) # Affiche le nom du port 
    return mData

Remarque : cette fonction a été écrite ici de la manière la plus basique possible, on pourrait rajouter des lignes de code supplémentaires pour gérer les exceptions (pas de balance détectée, choisir entre plusieurs balances connectées en même temps …)

Ensuite on fait appel à cette fonction pour récupérer les données :

Data = recup_port_USB()

Pour lire une ligne de données, il faut utiliser la méthode .readline() pour récupérer chaque ligne sous forme de chaîne de caractères.

La méthode .strip() permettra de supprimer les caractères d’espacement et de tabulation en début et fin de chaîne.

On utilisera aussi la méthode .split() pour séparer les différentes informations de la ligne pour les stocker dans une liste.

On essaie pour l’instant d’afficher une seule ligne (en utilisant .readline()) et la liste correspondante (avec les méthodes .strip() et .split()) :

line1 = Data.readline() 
print (line1)
donnee=line1.strip().split()
print (donnee)

Le résultat se présente de la façon suivante :

b'ST,GS-       16.2 g \r\n'
[b'ST,GS-', b'16.2', b'g']

La première ligne est la chaîne de caractères récupérée, et la deuxième ligne est la liste que nous avons créée avec les différents éléments de cette chaîne. L’information qui nous intéresse (valeur de la masse) est donc le 2ème élément de la liste … donc d’indice 1 … et oui rappelez-vous : l’indice du premier élément d’une liste est 0 !

Mais que veulent dire tous ces b devant les chaines de caractères ?

Ces données ne sont pas vraiment des chaînes de caractères mais des données de type bytes (séquences d’octet). il faudra donc les décoder (avec la méthode .decode()) et éventuellement les convertir en float si on a besoin de faire des calculs.

Nous pouvons donc demander d’afficher une série de 20 valeurs (et de les stocker dans la liste liste_masse) de la façon suivante :

# Essai pour une succession de 20 lignes de données
for k in range(20) :
    line1 = Data.readline() 
    print (line1)
    donnee=line1.strip().split()
    print (donnee)

    

Data.close()   # pour arrêter la lecture des données série

Il s’affiche ceci dans le shell :

b'\r\n'
[]
b'\r\n'
[]
b'ST,GS-       16.2 g \r\n'
[b'ST,GS-', b'16.2', b'g']
b'\r\n'
[]
b'\r\n'
[]
b'ST,GS-       16.2 g \r\n'
[b'ST,GS-', b'16.2', b'g']
b'\r\n'
[]
b'\r\n'
[]

On s’aperçoit que seule une ligne sur 3 contient des informations !

Nous allons donc ajouter une condition pour éliminer les listes vides

# Essai pour une succession de 20 lignes de données
for k in range(20) :
    line1 = Data.readline() 
    print (line1)
    donnee=line1.strip().split()
    print (donnee)

    if len(donnee) !=0 : # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
        distance = float(donnee[1].decode())  # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        liste_masse.append(masse)
        print ("masse: ", masse, " mm")

Data.close()   # pour arrêter la lecture des données série

Quelques lignes de code supplémentaires pour enregistrer ces informations dans un fichier texte de format .txt. Cela peut être utile pour l’importation de ces données dans des logiciels ou tableurs (Latis pro, Regressi, Excel, Libre Office ,…). Le chemin est défini par défaut dans le dossier où se trouve le fichier python mais il est possible de définir d’autres chemins:

#Ecriture dans un fichier txt
lines=['m\n'] #première ligne du fichier txt
for i in range (len (liste_masse)):
    line = str(liste_masse[i])+'\n'
    lines.append(line)

fichier = open('data_balance.txt', 'w').writelines(lines) #création d'un nouveau fichier texte

Le code complet pour ce programme de récupération de données :

#Importation des modules
import serial
import serial.tools.list_ports   # pour la communication avec le port série



#initialisation des listes
liste_masse = []


# Fonction pour la récupération des données série venant du port USB
def recup_port_usb() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'USB' in p.description :
            mData = serial.Serial(p.device,9600)
    print(mData.is_open) # Affiche et vérifie que le port est ouvert
    print(mData.name) # Affiche le nom du port 
    return mData

Data =recup_port_usb()

# Essai pour une succession de 20 lignes de données
for k in range(20) :
    line1 = Data.readline() 
    print (line1)
    donnee=line1.strip().split()

    if len(donnee) !=0 :
        masse = float(donnee[1].decode())  # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        liste_masse.append(masse)
        print ("masse : ", masse, " g")
Data.close()



#Ecriture dans un fichier txt
lines=['t\tm\n'] #première ligne du fichier txt
for i in range (len (liste_masse)):
    line = str(liste_temps[i]) +'\t'+ str(liste_masse[i])+'\n'
    lines.append(line)

fichier = open('data_balance.txt', 'w').writelines(lines) #création d'un nouveau fichier texte

Astuce : parfois un message d’erreur apparaît (problème de communication avec la carte, données reçues incomplètes …), comment résoudre ce problème ?

La plupart du temps, il suffit de redémarrer le shell (CTRL+K) et relancer ensuite le programme.

Etape 2 : Acquisition temporelle

On peut aussi afficher ces données en fonction du temps grâce au module time.

Nous partons donc du même code vu ci-dessus avec quelques compléments et modifications.

Tout d’abord, penser à importer le module time en début de programme

import time        # gestion du temps

Nous allons créer une liste pour la mesure du temps « brute ». Une autre liste va être créée pour la mesure du temps avec t=0 pour le début de l’acquisition


#initialisation des listes
liste_temps_mesure =[] # liste pour stocker le temps"brut"
liste_temps=[] # liste pour stocker les valeurs de temps en partant de t=0
liste_masse = []

On définit un temps d’acquisition :

t_acquisition = 5.0

On modifie la boucle pour qu’elle s’arrête à la fin du temps d’acquisition :

Data =recup_port_usb()

# Essai pour une succession de 20 lignes de données
tempsreel=0
while tempsreel <= t_acquisition:
    line1 = Data.readline()
    print (line1)
    #on retire les caractères d'espacement en début et fin de chaîne
    listeDonnees = line1.strip()
    # on sépare les informations reçues séparées par les espaces et on stocke ces informations dans une liste pour chacune de lignes
    listeDonnees = line1.split()
    print (listeDonnees)

    if len(listeDonnees)!= 0: # permet de nettoyer le flux de données car seule une liste sur 3 contient des informations
        masse = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        tempsmes = time.time()
        liste_temps_mesure.append(tempsmes) # temps mesuré "brut" stocké dans une liste
        tempsreel = tempsmes - liste_temps_mesure[0] # pour faire partir le temps de 0 (cette valeur de temps sera stockée dans une autre liste : liste_temps)

        liste_masse.append(masse)
        print("m = %f"%(masse), " g") # affichage de la valeur de la masse
        liste_temps.append(tempsreel)
        print("temps mesuré = %f"%(tempsmes), " s") # affichage de la valeur du temps absolu
        print("temps réel= %f"%(tempsreel), " s") # affichage de la valeur du temps en partant de 0

Data.close()

Le code complet pour ce programme d’acquisition temporelle :

import serial
import serial.tools.list_ports   # pour la communication avec le port série
import time # gestion du temps


#initialisation des listes
liste_temps_mesure =[] # liste pour stocker le temps"brut"
liste_temps=[] # liste pour stocker les valeurs de temps en partant de t=0
liste_masse = []

t_acquisition = 5.0


# Fonction pour la récupération des données série venant du port USB
def recup_port_usb() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'USB' in p.description :
            mData = serial.Serial(p.device,9600)
    print(mData.is_open) # Affiche et vérifie que le port est ouvert
    print(mData.name) # Affiche le nom du port
    return mData

Data =recup_port_usb()

# Essai pour une succession de 20 lignes de données
tempsreel=0
while tempsreel <= t_acquisition:
    line1 = Data.readline()
    print (line1)
    #on retire les caractères d'espacement en début et fin de chaîne
    listeDonnees = line1.strip()
    # on sépare les informations reçues séparées par les espaces et on stocke ces informations dans une liste pour chacune de lignes
    listeDonnees = line1.split()
    print (listeDonnees)

    if len(listeDonnees)!= 0: # permet de nettoyer le flux de données car seule une liste sur 3 contient des informations
        masse = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        tempsmes = time.time()
        liste_temps_mesure.append(tempsmes) # temps mesuré "brut" stocké dans une liste
        tempsreel = tempsmes - liste_temps_mesure[0] # pour faire partir le temps de 0 (cette valeur de temps sera stockée dans une autre liste : liste_temps)

        liste_masse.append(masse)
        print("m = %f"%(masse), " g") # affichage de la valeur de la masse
        liste_temps.append(tempsreel)
        print("temps mesuré = %f"%(tempsmes), " s") # affichage de la valeur du temps absolu
        print("temps réel= %f"%(tempsreel), " s") # affichage de la valeur du temps en partant de 0

Data.close()

#Ecriture dans un fichier txt
lines=['t\tm\n'] #première ligne du fichier txt
for i in range (len (liste_masse)):
    line = str(liste_temps[i]) +'\t'+ str(liste_masse[i])+'\n'
    lines.append(line)

fichier = open('data_balance.txt', 'w').writelines(lines) #création d'un nouveau fichier texte

Etape 3 : tracé de graphe (statique ou dynamique)

Graphe statique (qui sera affiché en fin d’acquisition)

Pour tracer un graphe, il faut au préalable importer la bibliothèque matplotlib de la manière suivante :

import matplotlib.pyplot as plt  # pour le tracé de graphe

Il suffit de taper ces quelques lignes de code pour afficher le graphe avec les valeurs stockées dans les listes :


plt.title('m=f(t)') # titre du graphique
plt.plot(liste_temps,liste_masse, color ='r', marker = 'o') # On affiche les points de coordonnées (I,U) avec des points rouges
plt.xlim (min(liste_temps),max(liste_temps))  #limtes pour les axes avec les valeurs extrêmes de I et de U
plt.ylim(min(liste_masse),max(liste_masse))
plt.show()

Le code complet pour le tracé de graphe statique :

#importation des modules
import serial
import serial.tools.list_ports # pour la communication avec le port série
import matplotlib.pyplot as plt  # pour le tracé de graphe
import time # gestion du temps



#initialisation des listes
liste_temps_mesure =[] # liste pour stocker le temps"brut"
liste_temps=[] # liste pour stocker les valeurs de temps en partant de t=0
liste_masse = []

t_acquisition = 5.0


# Fonction pour la récupération des données série venant du port USB
def recup_port_usb() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'USB' in p.description :
            mData = serial.Serial(p.device,9600)
    print(mData.is_open) # Affiche et vérifie que le port est ouvert
    print(mData.name) # Affiche le nom du port 
    return mData

Data =recup_port_usb()

# Essai pour une succession de 20 lignes de données
tempsreel=0
while tempsreel <= t_acquisition:
    line1 = Data.readline()
    print (line1)
    #on retire les caractères d'espacement en début et fin de chaîne
    listeDonnees = line1.strip()
    # on sépare les informations reçues séparées par les espaces et on stocke ces informations dans une liste pour chacune de lignes
    listeDonnees = line1.split()
    print (listeDonnees)

    if len(listeDonnees)!= 0: # permet de nettoyer le flux de données car seule une liste sur 3 contient des informations
        masse = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        tempsmes = time.time()
        liste_temps_mesure.append(tempsmes) # temps mesuré "brut" stocké dans une liste
        tempsreel = tempsmes - liste_temps_mesure[0] # pour faire partir le temps de 0 (cette valeur de temps sera stockée dans une autre liste : liste_temps)

        liste_masse.append(masse)
        print("m = %f"%(masse), " g") # affichage de la valeur de la masse
        liste_temps.append(tempsreel)
        print("temps mesuré = %f"%(tempsmes), " s") # affichage de la valeur du temps absolu
        print("temps réel= %f"%(tempsreel), " s") # affichage de la valeur du temps en partant de 0

Data.close()

plt.title('m=f(t)') # titre du graphique
plt.plot(liste_temps,liste_masse, color ='r', marker = 'o') # On affiche les points de coordonnées (I,U) avec des points rouges
plt.xlim (min(liste_temps),max(liste_temps))  #limtes pour les axes avec les valeurs extrêmes de I et de U
plt.ylim(min(liste_masse),max(liste_masse))
plt.xlabel('temps en s')
plt.ylabel('masse en g')
plt.show()

Tracé de graphe en temps réel (avec la fonction animate)

Pour tracer un graphe « animé » en temps réel, il faut importer les modules suivants :

import matplotlib.pyplot as plt  # pour le tracé de graphe
from matplotlib import animation # pour la figure animée

On définit un temps d’acquisition et une distance maximale pour l’échelle du graphe :

t_acquisition = 5.0
Massemax= 200 #en g

Nous allons inclure les instructions de la boucle dans une fonction nommée animate(i) :

#pour le graphe en temps réel
def animate(i):
    line1 = Data.readline()
    print (line1)
    # on retire les caractères d'espacement en début et fin de chaîne
    listeDonnees = line1.strip()
    # on sépare les informations reçues séparées par les espaces et on stocke ces informations dans une liste pour chacune de lignes
    listeDonnees = line1.split()
    print (listeDonnees)


    if len(listeDonnees)!= 0 : # permet de nettoyer le flux de données car seule une liste sur 3 contient des informations
        masse = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        tempsmes = time.time()
        liste_temps_mesure.append(tempsmes) # temps mesuré "brut" stocké dans une liste
        tempsreel = tempsmes - liste_temps_mesure[0] # pour faire partir le temps de 0 (cette valeur de temps sera stockée dans une autre liste : liste_temps)

        while tempsreel <= t_acquisition:
            liste_masse.append(masse)
            print("m = %f"%(masse), " g") # affichage de la valeur de la masse
            liste_temps.append(tempsreel)
            print("temps mesuré = %f"%(tempsmes), " s") # affichage de la valeur du temps absolu
            print("temps réel= %f"%(tempsreel), " s") # affichage de la valeur du temps en partant de 0
            line.set_data(liste_temps,liste_masse)
            return line,




Puis on récupère les données et on fait afficher la figure animée avec animation.FuncAnimation :


# Fonction pour la récupération des données série venant du port USB
def recup_port_USB() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        print (p)
        if 'USB' in p.description :
            mData = serial.Serial(p.device,9600)
    print(mData.is_open) # Affiche et vérifie que le port est ouvert
    print(mData.name) # Affiche le nom du port 
    return mData






Data =recup_port_USB() #récupération des données

# Création figure
fig=plt.figure()
line, = plt.plot([],[])
plt.xlim(0, t_acquisition)
plt.ylim(0,Massemax)
plt.grid()


#Animation
ani = animation.FuncAnimation(fig, animate, frames=200,  interval=20,repeat=False)

plt.show()
Data.close() # pour arrêter la lecture des données série

Le code complet pour le tracé de graphe en temps réel :

#importation des modules
import serial
import serial.tools.list_ports # pour la communication avec le port série
import matplotlib.pyplot as plt  # pour le tracé de graphe
from matplotlib import animation # pour la figure animée
import time # gestion du temps

#initialisation des listes

liste_temps_mesure =[] # liste pour stocker le temps"brut"
liste_temps=[] # liste pour stocker les valeurs de temps en partant de t=0
liste_masse = [] # liste pour stocker les valeurs de masse

t_acquisition = 5.0
Massemax= 200


#pour le graphe en temps réel
def animate(i):
    line1 = Data.readline()
    print (line1)
    # on retire les caractères d'espacement en début et fin de chaîne
    listeDonnees = line1.strip()
    # on sépare les informations reçues séparées par les espaces et on stocke ces informations dans une liste pour chacune de lignes
    listeDonnees = line1.split()
    print (listeDonnees)


    if len(listeDonnees)!= 0 : # permet de nettoyer le flux de données car seule une liste sur 3 contient des informations
        masse = float(listeDonnees[1].decode()) # après consulation des données, nous choisissons le 2ème élément de listeDonnees
        tempsmes = time.time()
        liste_temps_mesure.append(tempsmes) # temps mesuré "brut" stocké dans une liste
        tempsreel = tempsmes - liste_temps_mesure[0] # pour faire partir le temps de 0 (cette valeur de temps sera stockée dans une autre liste : liste_temps)

        while tempsreel <= t_acquisition:
            liste_masse.append(masse)
            print("m = %f"%(masse), " g") # affichage de la valeur de la masse
            liste_temps.append(tempsreel)
            print("temps mesuré = %f"%(tempsmes), " s") # affichage de la valeur du temps absolu
            print("temps réel= %f"%(tempsreel), " s") # affichage de la valeur du temps en partant de 0
            line.set_data(liste_temps,liste_masse)
            return line,






# Fonction pour la récupération des données série venant du port USB
def recup_port_USB() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        print (p)
        if 'USB' in p.description :
            mData = serial.Serial(p.device,9600)
    print(mData.is_open) # Affiche et vérifie que le port est ouvert
    print(mData.name) # Affiche le nom du port 
    return mData






Data =recup_port_USB() #récupération des données

# Création figure
fig=plt.figure()
line, = plt.plot([],[])
plt.xlim(0, t_acquisition)
plt.ylim(0,Massemax)
plt.grid()


#Animation
ani = animation.FuncAnimation(fig, animate, frames=200,  interval=20,repeat=False)

plt.show()
Data.close()
plt.close(fig)

#Ecriture dans un fichier txt
lines=['t\tm\n'] #première ligne du fichier txt
for i in range (len (liste_masse)):
    line = str(liste_temps[i]) +'\t'+ str(liste_masse[i])+'\n'
    lines.append(line)

fichier = open('data_balance.txt', 'w').writelines(lines) #création d'un nouveau fichier texte

Exécution du programme dans cette vidéo :

Script Python disponible sur ce lien : https://github.com/jonasforlot/python-arduino/blob/main/Donn%C3%A9es%20s%C3%A9rie%20balance/recup_donnees_balance.py

Un grand merci à Lionel Grillet, professeur de SI, pour son aide précieuse grâce à ses connaissances pointues en langage Python !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *