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 serial. Si un message d’erreur apparaît, installer serial en écrivant dans le shell :
pip install serial
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 :
/*
* Code d'exemple pour un capteur à ultrasons HC-SR04 avec alertes LED.
*/
/* 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;
/* 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() {
/* 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 les résultats en mm*/
Serial.print("distance en mm : ");
Serial.print(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 : 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 distances :
#initialisation des listes
liste_distance = []
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 à 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'distance en mm : 144.67 mm, \r\n'
[b'distance', b'en', b'mm', b':', b'144.67', b'mm,']
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 distance) est donc le 5ème élément de la liste … donc d’indice 4 … 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_distance) 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)
if len(donnee) !=0 : # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
distance = float(donnee[4].decode()) # après consultation des données, nous choisissons le 5ème élément de listeDonnees
liste_distance.append(distance)
print ("distance : ", distance, " 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=['d\n'] #première ligne du fichier txt
for i in range (len (liste_distance)):
line = str(liste_distance[i])+'\n'
lines.append(line)
fichier = open('data_arduino.txt', 'w')
fichier.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_distance = []
# 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
Data =recup_port_Arduino()
# 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 :
distance = float(donnee[4].decode()) # après consulation des données, nous choisissons le 5ème élément de listeDonnees
liste_distance.append(distance)
print ("distance : ", distance, " mm")
Data.close()
#Ecriture dans un fichier txt
lines=['d\n'] #première ligne du fichier txt
for i in range (len (liste_distance)):
line = str(liste_distance[i])+'\n'
lines.append(line)
fichier = open('data_arduino.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.
Cependant, Il vaudrait mieux mesurer le temps directement dans le code Arduino. En effet, le temps mesuré par Python pour chaque récupération de données ne correspond peut-être pas rigoureusement à l’instant de l’acquisition de la donnée. Cependant, cette technique est acceptable pour une acquisition lente.
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
liste_temps_mesure =[] # liste pour stocker le temps"brut"
liste_temps = [] # liste pour stocker un temps qui part de zéro
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_Arduino()
# Acquition temporelle
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: # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
distance = float(listeDonnees[4].decode()) # après consulation des données, nous choisissons le 5è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_distance.append(distance)
print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm
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() # pour arrêter la lecture des données série
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
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_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 :
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
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: # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
distance = float(listeDonnees[4].decode()) # après consulation des données, nous choisissons le 5è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_distance.append(distance)
print("d = %f"%(distance), " mm") # affichage de la valeur de la distance en mm
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() # pour arrêter la lecture des données série
#Ecriture dans un fichier txt
lines=['t\td\n'] #première ligne du fichier txt
for i in range (len (liste_distance)):
line = str(liste_temps[i]) +'\t'+ str(liste_distance[i])+'\n'
lines.append(line)
fichier = open('data_arduino.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('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_mesure =[] # liste pour stocker le temps"brut"
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 :
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
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: # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
distance = float(listeDonnees[4].decode()) # après consulation des données, nous choisissons le 5è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_distance.append(distance)
print("d = %f"%(distance), " mm") # affichage de la valeur de la distance
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()# pour arrêter la lecture des données série
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_distance)):
line = str(liste_temps[i]) +'\t'+ str(liste_distance[i])+'\n'
lines.append(line)
fichier = open('data_arduino.txt', 'w').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)!= 0 : # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
distance = float(listeDonnees[4].decode()) # après consulation des données, nous choisissons le 5è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_distance.append(distance)
print("d = %f"%(distance), " mm") # affichage de la valeur de la distance
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_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
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_distance = [] # liste pour stocker les valeurs de distance
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)!= 0 : # parfois des lignes de données vides peuvent être envoyées, il faut les "écarter"
distance = float(listeDonnees[4].decode()) # après consulation des données, nous choisissons le 5è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_distance.append(distance)
print("d = %f"%(distance), " mm") # affichage de la valeur de la distance
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_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 :
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() # pour arrêter la lecture des données série
#Ecriture dans un fichier txt
lines=['t\td\n'] #première ligne du fichier txt
for i in range (len (liste_distance)):
line = str(liste_temps[i]) +'\t'+ str(liste_distance[i])+'\n'
lines.append(line)
fichier = open('data_arduino.txt', 'w').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 !