Pour pouvoir tester les exemples de ces tutoriels, vous devez installer la bibliothèque PySerial. Sa documentation complète est disponible en ligne : https://pypi.org/project/pyserial/

PySerial est une bibliothèque Python permettant d’ouvrir et de transférer des données par l’intermédiaire des liaisons séries d’un ordinateur. Elle est compatible avec les systèmes d’exploitation Windows, OSX et Linux.

Premiers essais en console

Afin de pouvoir utiliser cette bibliothèque, il faut que vous l’importiez dans votre environnement de travail Python. Cette bibliothèque s’appelle serial. Nous aurons également besoin par la suite d’un outil intégré à cette bibliothèque : list_ports. Pour importer ces deux éléments, vous pouvez saisir les lignes suivantes dans votre console Python :

import serial
import serial.tools.list_ports

Liste des ports séries d’une machine

Les ordinateurs peuvent posséder, de base, plusieurs interfaces séries à disposition de l’utilisateur. Certains périphériques (comme les cartes Nucléo par exemple) sont susceptibles de proposer également une interface de ce type afin de transférer des données. Il est donc indispensable de connaître la liste des ports de communication série disponibles sur votre machine.

Pour cela, vous pouvez saisir dans la console Python la commande suivante :

serial_port = serial.tools.list_ports.comports()

L’objet serial_port est alors une liste contenant tous les objets de type port de communication série de votre machine. Si vous affichez directement cette liste, vous vous apercevrez qu’il n’est pas encore possible d’accéder à l’information des ports de communication. Il va falloir afficher indépendamment chacun des éléments de la liste par la commande suivante (par exemple) :

for port in serial_port:
    print(f"{port}")

Son exécution vous donnera un affichage de ce type :

COM6 - Lien série sur Bluetooth standard (COM6)
COM4 - Lien série sur Bluetooth standard (COM4)

Chaque port de communication série possède un nom. Sous Windows, ce nom est un ensemble des lettres COM suivies d’un numéro, qui est unique. Il est possible d’accèder autrement à ces informations en utilisant des attributs de la classe serial.tools.list_ports.ListPortInfo.

L’exécution des lignes suivantes :

for port in serial_port:
    print(f"{port.name} // {port.device} // D={port.description}")

donnera l’affichage suivant :

COM6 // COM6 // D=Lien série sur Bluetooth standard (COM6)
COM4 // COM4 // D=Lien série sur Bluetooth standard (COM4)

Connexion à un port série

Une fois que vous connaissez le port de communication auquel est connecté votre périphérique, il est alors possible d’ouvrir une connexion avec celui-ci. Pour cela, il suffit d’exécuter la commande suivante, en spécifiant comme premier argument de la fonction Serial le nom du port que vous souhaitez utiliser (l’attribut device de la classe serial.tools.list_ports.ListPortInfo)

ser = serial.Serial("COM6", baudrate=115200)

Cette fonction d’ouverture d’un port de communication possède plusieurs paramètres :

  • port – obligatoire pour spécifier la nom du port auquel se connecter
  • baudrate (int) – la vitesse de transfert des données
  • timeout (float) – pour spécifier une valeur de timeout en seconde

D’autres paramètres sont également disponibles pour spécifier le nombre de bits par transmission, l’ajout d’une détection de parité… Vous pouvez vous référer à la documentation de la bibliothèque PySerial : https://pyserial.readthedocs.io/en/latest/pyserial_api.html

Il est important dans une liaison utilisant le protocole série (ou RS232) de spécifier la vitesse de transmission à laquelle les deux noeuds du réseau doivent transmettre (et recevoir) les informations binaires transmises. Ce protocole est asynchrone, le signal d’horloge, temporisant l’échantillonnage des données, n’est alors pas transmis entre l’émetteur et le récepteur. Par défaut, cette vitesse est de 9600 bauds.

Déconnexion d’un port série

Il est indispensable de libérer le port série une fois que vous avez fini de l’utiliser. La commande pour le faire est la suivante :

ser.close()

Informations relatives à la connexion RS232

Une fois connecté, il est possible de récupérer des informations sur la liaison établie.

La commande suivante donne, par exemple, l’affichage suivant. On retrouve le nom du port, la vitesse de transfert, la parité…

print(ser)
Serial<id=0x2406d398c10, open=True>(port='COM6', baudrate=115200, bytesize=8, parity='N', stopbits=1, timeout=None, xonxoff=False, rtscts=False, dsrdtr=False)

Il est également possible d’accéder indépendamment à chacun de ces éléments en faisant appel à l’attribut correspondant de l’objet ser. Pour obtenir la vitesse de transmission, par exemple, on peut écrire :

print(ser.baudrate)

Transmission d’informations

La liaison série est une liaison asynchrone (sans transmission du signal d’horloge) et duplex (transmission dans les deux directions). L’envoi se fait souvent octet par octet par l’application émettrice (couches liaison et réseau du modèle OSI). La couche protocolaire RS232 émet cependant l’information bit par bit (couche physique du modèle OSI).

Pour plus d’informations sur les différents types de communication, vous pouvez consulter le tutoriel suivant : Faire communiquer deux systèmes ensemble (Nucleo).

Pour plus d’informations sur le fonctionnement du protocole RS232, vous pouvez consulter le tutoriel suivant : Configurer une communication RS232 (Nucleo).

Envoi d’un “message”

L’envoi d’un message se fait caractère par caractère – norme ASCII – ou octet par octet dans le cadre de données numériques.

Ce protocole de bas niveau ne permet que de transmettre un octet de manière “sécurisée”. Pour pouvoir transmettre un ensemble de données de manière “propre”, il va falloir ajouter une couche protocolaire permettant d’identifier les différents paquets transmis (voir Tutoriel XXX ).

Envoi d’un caractère

Pour envoyer un octet par l’intermédiaire de la liaison série établie précédemment, on peut utiliser le code suivant :

ser.write(b'a')

Dans cet exemple, le caractère ‘a’ est transmis sur la liaison. Cette fonction renvoie alors le nombre d’octets transmis sur la liaison série.

On précise ici qu’il s’agit d’une conversion vers un octet (ou byte en anglais) par la syntaxe b’…’.

Envoi d’une chaine de caractères

De la même façon, on peut transmettre une chaine de caractères (ou String) :

ser.write(b'bonjour')

Envoi d’octets

Il est également possible de transmettre une succession d’octets, stockés au préalable dans une liste, ayant des valeurs particulières, de la façon suivante :

dataL = [1, 2, 3, 4, 5]
dataBytes = bytes(dataL)
ser.write(dataBytes)

Dans cette exemple, 5 octets sont envoyés valant 1, 2, 3, 4 et 5.

Envoi de valeurs décimales

Il existe au moins deux méthodes pour transmettre une valeur décimale par l’intermédiaire d’une liaison de ce type :

  • envoyer une chaîne de caractères alphanumériques correspondant à la valeur décimale (pour transmettre 152, on transmettra successivement les caractères ‘1’, ‘5’ et ‘2’)
  • envoyer une valeur décimale “directement” sur le nombre d’octets nécessaires (par exemple, 152 peut être transmis sur un seul octet – car inférieur à la valeur maximale sur un octet – mais 489 devra être transmis sur au moins 2 octets).

Le choix de la méthode de transmission doit être spécifiée et identique des deux côtés de la chaîne de transmission, afin de pouvoir décoder correctement les éléments tranmsis.

Méthode par caractères

A partir d’un entier, il est possible de créer une chaine de caractères par la méthode suivante (en spécifiant l’encodage initial de la chaine de caractères) :

int_a = 152
str_a = str(int_a)
byt_a = bytes(str_a, 'utf-8')

Il est alors possible de transmettre la chaîne de caractères par la même fonction que précédemment :

ser.write(byt_a)

Méthode par décomposition en octets

Afin de pouvoir transmettre des entiers octet par octet, il existe une méthode en python pour pouvoir décomposer ces entiers en une série d’octets (dont le nombre est à préciser). Dans l’exemple suivant, on souhaite décomposer le nombre 489 en 4 octets, dont le premier sera le poids fort (méthode big – contrairement à la méthode little). La méthode write de la classe Serial enverra systématiquement 4 octets dans cet exemple.

int_val = 489
byt_val = int_val.to_bytes(4, 'big')
print(byt_val)
ser.write(byt_val)

Réception d’un “message”

Il est également possible de lire des données provenant du système connecté sur cette liaison par l’intermédiaire de 4 méthodes différents de la classe Serial :

  • read – qui lit un nombre d’octets passé en paramètre
  • read_until – qui lit des caractères jusqu’à un caractère particulier (paramètre expected)
  • readline – qui permet de lire une ligne de caractères (terminée par un caractère de fin de ligne)
  • readlines – qui permet de lire un nombre donné de lignes de caractères et les fournits dans des listes

L’exemple suivant permet de récupérer 5 caractères sous forme d’une liste.

read_bytes = ser.read(5)

Attention ! Si le nombre spécifié de données n’est pas atteinte, la fonction est alors bloquante…

Nous verrons dans un prochain tutoriel qu’il est parfois plus facile de traiter octet par octet afin de pouvoir décider des actions à mener en fonction de leur valeur.

Nombre d’octets reçus

Il existe également des méthodes et attributs permettant de savoir si une donnée a été reçue par l’ordinateur.

L’attribut in_waiting de la classe Serial fournit le nombre d’octets reçu par l’ordinateur. Cet attribut vaut 0 si aucun octet n’a été reçu.

print(ser.in_waiting)

Il est alors possible, lorsqu’on sait qu’on doit recevoir des données de la part du système connecté sur la liaison d’attendre en testant la valeur de cet attribut. Si 4 octets sont attendus, on peut alors exécuter le code suivant :

while(ser.in_waiting != 4):
    print("Wait !")
read_bytes = ser.read(4)

Exemple complet avec une carte Nucléo

Il est possible de paramétrer une liaison série (RS232) sur les cartes STMicroelectronics Nucleo (voir les tutoriels Configurer une liaison Série et Echanger des données entre deux systèmes communiquants – Nucleo ) et de la configurer pour qu’elle renvoie les caractères reçus (mode echo).

Echo mode sur Nucleo

Le programme de test du côté Nucléo est le suivant :

#include "mbed.h"

BufferedSerial      usb_pc(USBTX, USBRX);

void usb_pc_ISR(void){
    char rec_data_pc;
    int rec_length = 0;
    if(usb_pc.readable()){
        rec_length = usb_pc.read(&rec_data_pc, 1);
        usb_pc.write(&rec_data_pc, 1);
        rec_data_pc = 'c';
        usb_pc.write(&rec_data_pc, 1);
    }
}

int main()
{
    usb_pc.set_baud(115200);
    usb_pc.sigio(callback(usb_pc_ISR));
    while (true){}
}

Dans cet exemple, la réception des données séries sur la carte Nucléo se fait par interruption (sigio sur la liaison BufferedSerial). A chaque fois qu’un octet est reçu (fonction usb_pc_ISR), il est alors renvoyé sur la même liaison et le caractère ‘c’ est ajouté.

Transmission d’information depuis Python

import serial
import serial.tools.list_ports

serial_com = 6
serial_com_name = 'COM'+str(serial_com)

# Open the setup serial port
ser = serial.Serial(serial_com_name, baudrate=115200)
# Sending a char to Nucleo Board
ser.write(b'a')    

# Waiting for data sending by Nucleo board
while(ser.in_waiting == 0):
    print("Wait !")

rec_data_nucleo = ser.readline(2)
print(f'Rec = {rec_data_nucleo}')

ser.close()  
Python / PySerial / Premier script