ACTIVITE ARDUINO/PYTHON : Récupérer avec Python les données d’une carte Arduino pour tracer la caractéristique d’une LDR (avec régression linéaire)

Pré-requis : être familier avec le langage Python … et éventuellement avoir des bases en programmation pour les microcontrôleurs Arduino (mais pas obligatoire car on peut partir d’une carte Arduino déjà programmée avec un code inconnu et s’en sortir quand même !)

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

Découverte des microcontrôleurs et du langage Arduino disponible dans cet article : Découverte des microcontrôleurs et du langage Arduino

Si cette bibliothèque n’est pas installée (pour le vérifier, tester dans le shell import serial ), il faudra écrire dans le shell (interpréteur) :

pip install serial

Nous allons partir du montage suivant (vu déjà dans l’activité Tracé de caractéristique d’une photorésistance). Les mesures dans le code Arduino se font en continu (boucle infinie avec while(true))

Rappel : Tracer la caractéristique U = f(I) d’une photorésistance en faisant varier une tension (à l’aide d’un potentiomètre) aux bornes d’une association série LDR-résistance connue (par exemple  1kΩ).

Bonus : Faire une régression linéaire en utilisant le module  linregress (en important le module stats de scipy).


float R= 1000;
int const broche_LDR = A0;
int const broche_alim = A1;
int Valeur_LDR;
int Valeur_alim;
float U_alim;
float U_LDR;

//==================================================================================================
// Procédure d'initialisation des périphériques
//==================================================================================================
void setup() {
  // Initialisation de la communication série avec le terminal à 9600 baud.
  Serial.begin(9600);
  
}


//==================================================================================================
// Boucle principale Arduino.
//==================================================================================================
void loop() {





  // Mesures de la tension U_LDR en faisant varier U_alim avec le potentiomètre

  while(true)
  { 
    Valeur_alim = analogRead(broche_alim); // Valeur comprise entre 0 et 1023
    U_alim = Valeur_alim *5.0/1023; // Calcul de la tension U_alim
    Valeur_LDR   = analogRead(broche_LDR); // Valeur comprise entre 0 et 1023
    U_LDR = (float)Valeur_LDR*5/1023; //Calcul de la tension aux bornes de la photorésistance
    
    delay(1000);       // Délai en ms pour la stabilisation de la tension 
                       //pour la mesure de résistances.
    
   
    float courant_A   = (U_alim - U_LDR)/R; //Calcul de l'intensité du courant en A

    //Affichage des resultats


    Serial.print(" Tension LDR: ");   // Affichage de la tension en V sur le moniteur série
    Serial.print(U_LDR);            
    Serial.print(" V");              
  
    Serial.print(" Courant: ");          // Affichage du courant en mA sur le moniteur série
    Serial.print(courant_A*1000);       
    Serial.print(" mA");                
    
    Serial.println("");                 // Saut de ligne.

  }
           
}

Voilà ce qui s’affiche dans le moniteur série d’Arduino :

On souhaiterait récupérer ces données dans Python pour tracer un graphe, récupérer ces valeurs dans un fichier txt, …

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 faire appel à 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 les listes :

# Initialisation des listes 

lines = ['I(A)\tU(V)\n'] # première ligne du fichier texte dans lequel on enregistrera les données
U = []  # liste pour les valeurs de tension aux bornes de la LDR
I = []  # liste pour les valeurs d'intensité du courant

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 carte Arduino
def recup_port_Arduino() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'Arduino' 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 carte Arduino détecté, choisir entre plusieurs cartes Arduino connectées en même temps …)

Note pour les utilisateurs de MAC

Il semblerait que le mot « Arduino » n’apparaisse pas dans les descriptions de port. Pour la reconnaissance de carte, remplacer la ligne

if « Arduino » in p.descripton : par if « CDC »in p.description :

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

Data = recup_port_Arduino()

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.

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

La méthode .split() permet de 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éthode .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' Tension LDR: 2.65 V Courant: 2.24 mA\r\n'
[b'Tension', b'LDR:', b'2.65', b'V', b'Courant:', b'2.24', b'mA']

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. Les informations qui nous intéressent (valeurs de tension et d’intensité) sont les 3ème et 6ème élément de la liste … donc d’indice 2 et 5 … 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 choisir une acquisition pour 10 points (et de stocker les valeurs mesurées dans les listes U et I) de la façon suivante :

for i in range(10) : #acquisition pour 10 valeurs
    line = Data.readline()  # Lit la ligne venant du port série.
    print (line)
    listeDonnees = line.strip()
    listeDonnees = line.split() # on sépare les données de la ligne  et on les stocke dans une liste
    print(listeDonnees)  # affichage de la liste obtenue.

    if len(listeDonnees)!=0: # extraction des données (valeurs d'intensité et tension)
        tension = float(listeDonnees[2].decode())
        courant = (float(listeDonnees[5].decode()))/1000 # conversion en A
        U.append(tension)
        print("U = %f V"%(tension))
        I.append(courant)
        print("I = %f A"%(courant))
        
    
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

for i in range (len (I)):
    line = str(I[i]) +'\t'+ str(U[i])+'\n'
    lines.append(line)

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

Etape 2 : 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

Nous allons utiliser la fonction stats.linregress(X,Y) pour faire la régression linéaire. Cette fonction retourne plusieurs valeurs, les trois premières sont la pente, l’origine à l’ordonnée et le coefficient de corrélation (à mettre au carré).

eq = stats.linregress (I,U) # pour faire la régression linéaire

pente = eq[0] # pente
ordorig = eq[1] # ordonnée à l'origine
coeff2 = eq[2]**2 # coefficient de corrélation au carré r²

Xcalc = np.linspace(0,max(I) , 256) # création de points pour le tracé du modèle : on crée 256 points régulièrement espacés entre 0 et la valeur max de I
Ycalc = pente*Xcalc+ordorig # on fait calculer U avec les paramètres de la régression linéaire pour ces valeurs de I
texte = 'equation de la droite  U = '+str(round(pente,3))+' I + '+str(round(ordorig,3))+'     R² = '+str(round(coeff2,3)) # on affiche l'équation de la droite avec 3 décimales

print (texte)

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

# Affichage du graphique
plt.title('U=f(I)') # titre du graphique
plt.scatter(I,U, color ='r', marker = 'o') # On affiche les points de coordonnées (I,U) avec des points rouges
plt.plot(Xcalc,Ycalc,color = 'b',label = texte) # Affichage de la courbe modélisée en bleu
plt.xlabel('I en A')       # nommer l'axe des abscisses
plt.ylabel('U en V')       # nommer l'axe des ordonnéees
plt.xlim (min(I),max(I))  #limtes pour les axes avec les valeurs extrêmes de I et de U
plt.ylim(min(U),max(U))
plt.legend()   # pour afficher les légendes (label)
plt.show()  #afficher le graphique (ne rien mettre dans la parenthèse)

Le code complet pour le tracé de graphe statique :


"""
Programme Python pour récupérer les donnnées d'un code Arduino permettant de tracer une caractéristique d'une photorésistance (ou autre capteur résistif). On affiche ensuite les résultats d'une régression linéaire.
Montage : Une tension de 5 V est appliquée aux deux points extrêmes d'un potentiomètre. On crée une alimentation variable en récupérant la tension entre le point milieu et la masse .
On applique cette tension à une association série photorésistance avec résistance connue (1 kohms)-. On fait mesurer par Arduino la tension aux bornes de la photorésistance et l'intensité parcourant le circuit I = (tension alim - tension photorésistance)/R.
"""

#########################################  IMPORTATION DES BIBLIOTHEQUES ET MODULES  ########################################################

import numpy   # numpy pour les maths , par exemple pour créer 256 valeurs régulièrement espacées entre 0 et 10 : np.linspace(0,10,256)
from time import sleep             # pour faire des "pauses" dans l'exécution du programme
import matplotlib.pyplot as plt # pour les graphiques


from scipy import stats # module permettant de faire la régression linéaire à partir d'une liste X et d'une liste Y, stats.linregress(X,Y) renvoie 5 valeurs. Les 3 premières valeurs sont la pente, l'ordonnée à l'origine, et le coefficient de corrélation (à mettre au carré)


import serial
import serial.tools.list_ports



#########################################  COMMUNICATION AVEC CARTE ARDUINO ET DEFINITION DES VARIABLES  #######################################################

#initialisation des listes
U=[]
I=[]
lines = ['I(A)\tU(V)\n']



# Fonction pour la récupération des données série venant de la carte Arduino
def recup_port_Arduino() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'Arduino' 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





#########Récupération des données qu'on stocke dans des listes ########################################################################

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

for i in range(10) : #acquisition pour 10 valeurs
    line = Data.readline()  # Lit la ligne venant du port série.
    print (line)
    listeDonnees = line.strip()
    listeDonnees = line.split() # on sépare les données de la ligne  et on les stocke dans une liste
    print(listeDonnees)  # affichage de la liste obtenue.

    if len(listeDonnees)!=0: # extraction des données (valeurs d'intensité et tension)
        tension = float(listeDonnees[2].decode())
        courant = (float(listeDonnees[5].decode()))/1000 # conversion en A
        U.append(tension)
        print("U = %f V"%(tension))
        I.append(courant)
        print("I = %f A"%(courant))
        
    
Data.close()

#Ecriture dans un fichier txt
for i in range (len (I)):
    line = str(I[i]) +'\t'+ str(U[i])+'\n'
    lines.append(line)

open('data.txt', 'w').writelines(lines) #création d'un nouveau fichier texte sans la première ligne


eq = stats.linregress (I,U) # pour faire la régression linéaire

pente = eq[0] # pente
ordorig = eq[1] # ordonnée à l'origine
coeff2 = eq[2]**2 # coefficient de corrélation au carré r²

Xcalc = numpy.linspace(0,max(I) , 256) # création de points pour le tracé du modèle : on crée 256 points régulièrement espacés entre 0 et la valeur max de I
Ycalc = pente*Xcalc+ordorig # on fait calculer U avec les paramètres de la régression linéaire pour ces valeurs de I

global texte
texte = 'equation de la droite  U = '+str(round(pente,3))+' I + '+str(round(ordorig,3))+'     R² = '+str(round(coeff2,3)) # on affiche l'équation de la droite avec 3 décimales

print (texte)

# Affichage du graphique
plt.title('U=f(I)') # titre du graphique
plt.scatter(I,U, color ='r', marker = 'o') # On affiche les points de coordonnées (I,U) avec des points rouges
plt.plot(Xcalc,Ycalc,color = 'b',label = texte) # Affichage de la courbe modélisée en bleu
plt.xlabel('I en A')       # nommer l'axe des abscisses
plt.ylabel('U en V')       # nommer l'axe des ordonnéees
plt.xlim (min(I),max(I))  #limtes pour les axes avec les valeurs extrêmes de I et de U
plt.ylim(min(U),max(U))
plt.legend()   # pour afficher les légendes (label)
plt.show()  #afficher le graphique (ne rien mettre dans la parenthèse)

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

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

def animate(i):

    line = Data.readline()  # Lit la ligne venant du port série.
    print (line)
    listeDonnees = line.strip()
    listeDonnees = line.split() # on sépare les données de la ligne  et on les stocke dans une liste
    print(listeDonnees)  # affichage de la liste obtenue.
    while  len (U)<10 :
        if len(listeDonnees)!=0: # extraction des données (valeurs d'intensité et tension)
            tension = float(listeDonnees[2].decode())
            courant = (float(listeDonnees[5].decode()))/1000 # conversion en A
            U.append(tension)
            print("U = %f V"%(tension))
            I.append(courant)
            print("I = %f A"%(courant))
            line0.set_data(I, U)
            return line0,
    



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


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


# Création figure

fig=plt.figure()
line0, = plt.plot([],[])
plt.xlim(0, 5)
plt.ylim(0,0.005)
plt.xlabel("I en A")
plt.ylabel("U en V")

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

plt.show()
Data.close()

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


"""
Programme Python pour récupérer les donnnées d'un code Arduino permettant de tracer en temps réel une caractéristique d'une photorésistance (ou autre capteur résistif). On affiche ensuite les résultats d'une régression linéaire.
Montage : Une tension de 5 V est appliquée aux deux points extrêmes d'un potentiomètre. On crée une alimentation variable en récupérant la tension entre le point milieu et la masse .
On applique cette tension à une association série photorésistance avec résistance connue (1 kohms)-. On fait mesurer par Arduino la tension aux bornes de la photorésistance et l'intensité parcourant le circuit I = (tension alim - tension photorésistance)/R.
"""

#########################################  IMPORTATION DES BIBLIOTHEQUES ET MODULES  ########################################################

import numpy   # numpy pour les maths , par exemple pour créer 256 valeurs régulièrement espacées entre 0 et 10 : np.linspace(0,10,256)
from time import sleep             # pour faire des "pauses" dans l'exécution du programme
import matplotlib.pyplot as plt # pour les graphiques
import matplotlib.animation as animation

from scipy import stats # module permettant de faire la régression linéaire à partir d'une liste X et d'une liste Y, stats.linregress(X,Y) renvoie 5 valeurs. Les 3 premières valeurs sont la pente, l'ordonnée à l'origine, et le coefficient de corrélation (à mettre au carré)


import serial
import serial.tools.list_ports



#########################################  COMMUNICATION AVEC CARTE ARDUINO ET DEFINITION DES VARIABLES  #######################################################

R= 1000.0 # valeur de résistance connue pour mesure de l'intensité


U_alim = 0.0
courant_A =0.0
lines = ['I(A)\tU(V)\n']


# Fonction pour la récupération des données série venant de la carte Arduino
def recup_port_Arduino() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'Arduino' 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


#########################################  Fonction animate(i) pour tracé en temps réel    ##############################################################################


def animate(i):

    line = Data.readline()  # Lit la ligne venant du port série.
    print (line)
    listeDonnees = line.strip()
    listeDonnees = line.split() # on sépare les données de la ligne  et on les stocke dans une liste
    print(listeDonnees)  # affichage de la liste obtenue.
    while  len (U)<10 :
        if len(listeDonnees)!=0: # extraction des données (valeurs d'intensité et tension)
            tension = float(listeDonnees[2].decode())
            courant = (float(listeDonnees[5].decode()))/1000 # conversion en A
            U.append(tension)
            print("U = %f V"%(tension))
            I.append(courant)
            print("I = %f A"%(courant))
            line0.set_data(I, U)
            return line0,
    



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


#initialisation des listes

U=[]
I=[]
# Création figure "dynamique" en temps réel

fig=plt.figure()
line0, = plt.plot([],[])
plt.xlim(0, 5)
plt.ylim(0,0.005)
plt.xlabel("I en A")
plt.ylabel("U en V")

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

plt.show()
Data.close()


eq = stats.linregress (I,U) # pour faire la régression linéaire

pente = eq[0] # pente
ordorig = eq[1] # ordonnée à l'origine
coeff2 = eq[2]**2 # coefficient de corrélation au carré r²

Xcalc = numpy.linspace(0,max(I) , 256) # création de points pour le tracé du modèle : on crée 256 points régulièrement espacés entre 0 et la valeur max de I
Ycalc = pente*Xcalc+ordorig # on fait calculer U avec les paramètres de la régression linéaire pour ces valeurs de I

global texte
texte = 'equation de la droite  U = '+str(round(pente,3))+' I + '+str(round(ordorig,3))+'     R² = '+str(round(coeff2,3)) # on affiche l'équation de la droite avec 3 décimales

print (texte)

# tracé du graphe "statique" avec résultat de la régression linéaire après fermeture du graphe "dynamique"
plt.title('U=f(I)') # titre du graphique
plt.scatter(I,U, color ='r', marker = 'o') # On affiche les points de coordonnées (I,U) avec des points rouges
plt.plot(Xcalc,Ycalc,color = 'b',label = texte) # Affichage de la courbe modélisée en bleu
plt.xlabel('I en A')       # nommer l'axe des abscisses
plt.ylabel('U en V')       # nommer l'axe des ordonnéees
plt.xlim (min(I),max(I))  #limtes pour les axes avec les valeurs extrêmes de I et de U
plt.ylim(min(U),max(U))
plt.legend()   # pour afficher les légendes (label)
plt.show()  #afficher le graphique (ne rien mettre dans la parenthèse)


#Ecriture dans un fichier txt
for i in range (len (I)):
    line = str(I[i]) +'\t'+ str(U[i])+'\n'
    lines.append(line)

open('data.txt', 'w').writelines(lines) #création d'un nouveau fichier texte sans la première ligne


Voici en vidéo ce qui se passe : pendant l’exécution du programme, nous tournons régulièrement le potentiomètre pour l’acquisition de 10 points et el tracé se fait en temps réel. Une fois la fenêtre de ce graphique fermée, il apparaît un graphe (statique) avec les résultats de la régression linéaire.

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

Laisser un commentaire