ACTIVITE ARDUINO/PYTHON : Récupérer avec Python les données d’une carte Arduino (avec tracé de graphes). Exemple avec une mesure de distance par ultrason.

Objectif de cette activité : Récupérer dans Python les données du moniteur série d’un programme Arduino en utilisant simplement la bibliothèque serial.

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

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

pip install pyserial

Nous allons partir du montage suivant (vu déjà dans l’activité Radar de recul).

Le tracé d’un graphique n’a peut-être pas grand intérêt avec ce montage mais le but de cet article est juste de montrer comment récupérer les données, nous verrons dans d’autres articles des exemples plus pertinents : tracé de caractéristique d’une photodiode avec régression linéaire, récupération de données pour d’autres types d’interfaces USB (par exemple acquisition temporelle d’une masse avec une balance…)

MONTAGE ULTRASON AVEC CODE ARDUINO radar de recul avec alertes LED couleurs différentes selon distance

Rappel du montage: Faire clignoter des LED de couleurs différentes (rouge, orange, verte) selon la position de l’obstacle par rapport au capteur US. On utilise des résistances de protection de 220 Ω.

  • Zone verte : entre 30 cm et 50 cm
  • Zone orange : entre 10 cm et 30 cm
  • Zone rouge : moins de 10 cm

Bonus : on fait clignoter les LED plus rapidement quand l’obstacle se rapproche !

Voici le code Arduino téléversé dans la carte :

/* Constantes pour les broches */
const byte TRIGGER_PIN = 13; // Broche TRIGGER
const byte ECHO_PIN = 12;    // Broche ECHO
const byte LED_verte = 8; // broches pour les LED 
const byte LED_orange = 9;
const byte LED_rouge = 10;
unsigned long temps;


 
/* Constantes pour le timeout */
const unsigned long MEASURE_TIMEOUT = 25000UL; // 25ms = ~8m à 340m/s, temps limite pour la mesure de distance

/* Vitesse du son dans l'air en mm/us */
const float SOUND_SPEED = 340.0 / 1000;

/** Fonction setup() */
void setup() {
   
  /* Initialise le port série */
  Serial.begin(9600);
   
  /* Initialise les broches */
  pinMode(TRIGGER_PIN, OUTPUT);
  digitalWrite(TRIGGER_PIN, LOW); // La broche TRIGGER doit être à LOW au repos
  pinMode(ECHO_PIN, INPUT);
  pinMode (LED_verte, OUTPUT); // On déclare les broches des LED et buzzer comme sorties
  pinMode (LED_orange, OUTPUT);
  pinMode (LED_rouge, OUTPUT);
//  
}
 
/** Fonction loop() */
void loop() {
  temps = millis();
  /* 1. Lance une mesure de distance en envoyant une impulsion HIGH de 10µs sur la broche TRIGGER */
  digitalWrite(TRIGGER_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIGGER_PIN, LOW);
  
  /* 2. Mesure le temps en us entre l'envoi de l'impulsion ultrasonique et son écho (si il existe) */
  long measure = pulseIn(ECHO_PIN, HIGH, MEASURE_TIMEOUT);
   
  /* 3. Calcul de la distance à partir du temps mesuré */
  float distance_mm = measure / 2.0 * SOUND_SPEED;
  
 // zone pour laquelle la LED verte clignote 
if ((distance_mm < 500.0)and (distance_mm > 300.0)) {
digitalWrite (LED_verte, HIGH); //la LED s'allume pendant 1 s et s'éteint pendant 0,5 s
delay(1000);
digitalWrite (LED_verte,LOW);
delay (500);
 }
 
//zone pour laquelle la LED orange clignote 
if ((distance_mm < 300.0) and (distance_mm > 100.0)) { //la LED s'allume pendant 0,5 s et s'éteint pendant 0,25 s
digitalWrite (LED_orange, HIGH); 
delay(500);
digitalWrite (LED_orange,LOW);
delay (250);
}

//zone pour laquelle la LED rouge clignote
 if (distance_mm < 100.0) { //la LED s'allume pendant 0,1 s et s'éteint pendant 0,05 s
digitalWrite (LED_rouge, HIGH); 
delay(100);
digitalWrite (LED_rouge,LOW);
delay (50);

 }

// sinon il ne se passe rien ... 
else {
  digitalWrite (LED_verte,LOW);
  digitalWrite (LED_orange,LOW);
  digitalWrite (LED_rouge,LOW);
  delay (50);
  
}
    
  
  /* Affiche le temps en ms et les résultats en mm, on simplifie le code pour récupérer seulement les valeurs*/
//  Serial.print("temps en ms : ");
  Serial.print(temps);
  Serial.print("\t");
//  Serial.print("distance en mm : ");
  Serial.println(distance_mm);
//  Serial.println(" mm, ");

//   
  /* Délai d'attente pour éviter d'afficher trop de résultats à la seconde */
  delay(50);
}

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 : Acqusition temporelle

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 distances :

#initialisation des listes
liste_temps=[] # liste pour stocker les valeurs de temps en s
liste_distance = [] # liste pour stocker la distance en mm

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 du port USB
def recup_port_Arduino() :
    ports = list(serial.tools.list_ports.comports())
    for p in ports:
        if 'Arduino' in p.description :
        # if 'CDC' in p.description :    #pour les utilisateurs de mac
            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 à cette 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.

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é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'0\t64.60\r\n'
[b'0', b'64.60']

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 temps et de distance). Le temps est donc est le premier élément de la liste … donc d’indice 0; la distance 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.

On définit un temps d’acquisition :

t_acquisition = 5.0

On crée une boucle qui doit s’arrêter à la fin du temps d’acquisition :

Data =recup_port_Arduino()

# Acquition temporelle
temps=0
while temps <= 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)== 2: # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
        temps = float(listeDonnees[0].decode())/1000.0 # après consultation des données, nous choisissons le 1er élément de listeDonnees, conversion en secondes
        distance = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        liste_temps.append(temps)
        liste_distance.append(distance)
        print("temps = %f"%(temps), " s") # affichage de la valeur du temps en partant de 0
        print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm


Data.close()

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 = ['t\td\n']  # première ligne du fichier txt
for i in range(len(liste_temps)):
    line = str(liste_temps[i]) +'\t'+str(liste_distance[i]) + '\n'
    lines.append(line)

# Utilisation de 'with open' pour assurer la fermeture du fichier
with open('data_arduino.txt', 'w', encoding='utf-8') as fichier:
    fichier.writelines(lines)  # création d'un nouveau fichier texte

Le code complet pour ce programme d’acquisition temporelle :

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



#initialisation des listes
liste_temps=[] # liste pour stocker les valeurs de temps en s
liste_distance = [] # liste pour stocker la distance en mm

t_acquisition = 5.0


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

# Acquition temporelle
temps=0
while temps <= 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)== 2: # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
        temps = float(listeDonnees[0].decode())/1000.0 # après consultation des données, nous choisissons le 1er élément de listeDonnees, conversion en secondes
        distance = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        liste_temps.append(temps)
        liste_distance.append(distance)
        print("temps = %f"%(temps), " s") # affichage de la valeur du temps en partant de 0
        print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm


Data.close()

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

# Utilisation de 'with open' pour assurer la fermeture du fichier
with open('data_arduino.txt', 'w', encoding='utf-8') as fichier:
    fichier.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.

Le code complet pour ce programme d’acquisition temporelle :

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

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

plt.title('d=f(t)') # titre du graphique
plt.plot(liste_temps,liste_distance, 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_distance),max(liste_distance))
plt.xlabel('temps en s')
plt.ylabel('distance en mm')
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=[] # liste pour stocker les valeurs de temps en partant de t=0
liste_distance = [] # liste pour stocker la distance en mm

t_acquisition = 5.0


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

# Acquisition
temps=0
while temps <= 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)== 2: # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
        temps = float(listeDonnees[0].decode())/1000.0 # après consultation des données, nous choisissons le 1er élément de listeDonnees, conversion en secondes
        distance = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees
        liste_temps.append(temps)
        liste_distance.append(distance)
        print("temps = %f"%(temps), " s") # affichage de la valeur du temps en partant de 0
        print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm


Data.close()

plt.title('d=f(t)') # titre du graphique
plt.plot(liste_temps,liste_distance, 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_distance),max(liste_distance))
plt.xlabel('temps en s')
plt.ylabel('distance en mm')
plt.show()

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

# Utilisation de 'with open' pour assurer la fermeture du fichier
with open('data_arduino.txt', 'w', encoding='utf-8') as fichier:
    fichier.writelines(lines)  # création d'un nouveau fichier texte

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 = 6.0
distancemax= 500 # en mm

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)== 2 : # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
        temps = float(listeDonnees[0].decode())/1000.0 # après consultation des données, nous choisissons le 1er élément de listeDonnees, conversion en secondes
        distance = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees

        while temps <= t_acquisition:
            liste_temps.append(temps)
            liste_distance.append(distance)
            print("temps = %f"%(temps), " s") # affichage de la valeur du temps en partant de 0
            print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm
            line.set_data(liste_temps,liste_distance)
            return line,

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()
line, = plt.plot([],[])
plt.xlim(0, t_acquisition)
plt.ylim(0,distancemax)
plt.xlabel('temps en s')
plt.ylabel('distance en mm')
plt.grid()


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

plt.show()

plt.close(fig)
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


#initialisation des listes
liste_temps=[] # liste pour stocker les valeurs de temps en partant de t=0
liste_distance = [] # liste pour stocker la distance en mm

t_acquisition = 6.0
distancemax= 500 # en mm


#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)== 2 : # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
        temps = float(listeDonnees[0].decode())/1000.0 # après consultation des données, nous choisissons le 1er élément de listeDonnees, conversion en secondes
        distance = float(listeDonnees[1].decode()) # après consultation des données, nous choisissons le 2ème élément de listeDonnees

        while temps <= t_acquisition:
            liste_temps.append(temps)
            liste_distance.append(distance)
            print("temps = %f"%(temps), " s") # affichage de la valeur du temps en partant de 0
            print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm
            line.set_data(liste_temps,liste_distance)
            return line,






# 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 :
        # if 'CDC' in p.description :    #pour les utilisateurs de mac
            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_Arduino() #récupération des données

# Création figure
fig=plt.figure()
line, = plt.plot([],[])
plt.xlim(0, t_acquisition)
plt.ylim(0,distancemax)
plt.xlabel('temps en s')
plt.ylabel('distance en mm')
plt.grid()


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

plt.show()

plt.close(fig)
Data.close()

plt.title('d=f(t)') # titre du graphique
plt.plot(liste_temps,liste_distance, 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_distance),max(liste_distance))
plt.xlabel('temps en s')
plt.ylabel('distance en mm')
plt.show()


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

# Utilisation de 'with open' pour assurer la fermeture du fichier
with open('data_arduino.txt', 'w', encoding='utf-8') as fichier:
    fichier.writelines(lines)  # création d'un nouveau fichier texte

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 *