Communication série en Python

Communication série en Python #

Durant ce TP, nous allons programmer une interface de communication utilisant, sur le port USB, une liaison série RS232 entre :

  • une carte à micro-contrôleur MSP430 ou Arduino (déjà programmée),
  • un PC ou une Raspberry Pi, en Python.

Pour manipuler le port série avec Python, nous utiliserons le module tiers pyserial, dont la documentation se trouve ici : https://pyserial.readthedocs.io/en/latest/

Il y a de toutes petites variations selon que le microcontrôleur utilisé est un MSP430 ou un Arduino. Soyez vigilants.

Branchement du MSP430 #

Pour que la carte MSP430 commence à émettre sur le port série, il suffit de la connecter à la Raspberry ou au PC, à l’aide d’un cable USB. Le programme qui tourne sur le MSP430 peut être relancé à tout moment, en utilisant le bouton Reset, situé sur la même face que le connecter micro-USB.

Branchement de l’Arduino #

Pour que la carte Arduino commence à émettre sur le port série, il suffit de la connecter à la Raspberry ou au PC, à l’aide d’un cable USB. Le programme qui tourne sur l’Arduino peut être relancé à tout moment, en utilisant le bouton Reset. Par ailleurs, les Arduino Nano et Uno redémarrent de toutes façons dès qu’une liaison série est établie.

Ouverture du port série #

L’ouverture du port série nécessite de connaître certains paramètres : nom du port, vitesse de transmission, bits de parité, contrôle de flux…

Sous Windows, le nom du port peut être obtenu (après branchement de la carte) dans le gestionnaire de périphériques. Le nom ressemble à COM1, COM2… Sous Linux (Raspberry Pi), la consultation du journal après branchement permet généralement de connaître le port utilisé par le nouveau périphérique :

$ dmesg
...
[ 2702.474341] usb 2-2: new full-speed USB device number 17 using xhci_hcd
[ 2702.617486] usb 2-2: New USB device found, idVendor=2047, idProduct=0013, bcdDevice= 2.00
[ 2702.617491] usb 2-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 2702.617493] usb 2-2: Product: MSP Tools Driver
[ 2702.617495] usb 2-2: Manufacturer: Texas Instruments
[ 2702.617496] usb 2-2: SerialNumber: AE443A5125000100
[ 2702.622308] cdc_acm 2-2:1.0: ttyACM0: USB ACM device
[ 2702.622950] cdc_acm 2-2:1.2: ttyACM1: USB ACM device

On voit ici que le MSP430 correspond à deux interfaces : ttyACM0 et ttyACM1 (ce sont des fichiers de périphérique, situés dans le répertoire /dev). Il reste à déterminer, parmi ces deux interfaces, celle qui est utilisée par le MSP430 pour communiquer avec l’extérieur.

Lorsqu’on connecte un Arduino, on ne voit qu’un seul périphérique série (par exemple ttyUSB0).

Les autres paramètres de la communication sont :

  • vitesse : 19200 bauds
  • 8 bits de données
  • pas de bit de parité
  • un bit stop

L’ouverture du port série ressemble un peu à l’ouverture d’un fichier :

import serial

ser = serial.Serial('/dev/ttyACM??', 19200, timeout=5, 
                    bytesize=8, parity='N', stopbits=1)

# lecture / écriture

ser.close()

Lecture sur le port série #

Lorsque le port série est ouvert, on peut lire les données qui se présentent, octet par octet :

# ouverture du port 

b = ser.read() # Lecture d'un octet

# fermeture du port

L’opération de lecture est bloquante. Elle ne reste toutefois pas bloquée éternellement, et lorsque le timeout utilisé lors de l’ouverture du port série est dépassé, la méthode read se termine (et renvoie un octet vide si rien n’a été lu).

Il est habituel, en Python, d’utiliser des context managers. Ce sont des objets qui offrent certaines garanties d’initialisation et de finalisation. En ce qui concerne les fichiers, ils permettent par exemple de garantir la fermeture du fichier à la fin du code (que celui-ci se termine proprement ou plante durant l’exécution) :


import serial
import time

with serial.Serial('/dev/ttyACM??', 19200, timeout=5, 
                    bytesize=8, parity='N', stopbits=1) as ser:
    # Dans ce bloc indenté, 
    # on peut disposer de l'objet `ser`
    # Lorsque le bloc se terminera, la liaison sera refermée.
    # Généralement, il faut faire uen pause ici, si on a un Arduino à l'autre
    # bout (pour compenser le reboot) : time.sleep(...)

Notez qu’il n’est pas nécessaire de fermer explicitement ser.

Réalisez un premier programme, nommé read_50_bytes.py qui se contente de lire 50 octets à la suite sur le port série et les affiche proprement à l’écran. Pensez à la tempo juste après l’établissement de la liaison série si vous utilisez un Arduino.

Quel genre de séquence d’octets est envoyée par le microcontrôleur ?

Si vous obtenez l’erreur PermissionDenied à la lecture du port série, c’est peut être que lors d’une lecture précédente, vous n’avez pas refermé le port série, et êtes maintenant dans l’impossibilité de l’ouvrir à nouveau. Dans ce cas, redémarrez le shell Python (qui refermera tous les fichiers ouverts).

Objets de type bytes #

Le type bytes en Python permet de représenter une séquence d’octets. Les objets de type bytes se manipulent un peu comme des chaînes de caractères. Python peut parfois les afficher sous forme de caractères (lorsque l’octet correspond à un caractère ASCII imprimable par exemple) ou comme une valeur numérique.

 # Le préfixe b indique qu'il s'agit d'octets.
 # Il y en a ici 3 : 65 (correcpond à A), 16 (10 en hexadécimal),
 # et 66 (correspond à B)
>>> octets = b'A\x10B'
>>> octets
b'A\x10B' 
>>> octets[0]
65
>>> octets[1]
16
>>> octets[2]
66
>>> hex(octets[1])
'0x10'

Il existe des moyens efficaces pour passer d’une séquence d’octets à une chaîne de caractères et inversement (voir str.encode et str.decode), ou d’une séquence d’octets à un entier, codé sur plusieurs octets, en mode big ou little endian (voir int.from_bytes ou int.to_bytes). Pensez à vous documenter sur ces possibilités avant de vous lancer dans des conversions fastidieuses.

Le type bytes n’est pas mutable (les chaînes de caractères non plus). Il existe toutefois un type bytearray, mutable, et la conversion est assez facile :

>>> octets = b'\x01\x02\x03\x04' # octets est de type bytes, non mutable
>>> m_octets = bytearray(octets) # m_octets est de type bytearray, mutable
>>> m_octets[1] = 42 # <= possible car m_octets est mutable
>>> m_octets
bytearray(b'\x01*\x03\x04')

Protocole de communication #

Un protocole de communication a été implémenté sur le microcontrôleur, et il peut répondre à certaines commandes. Ce protocole échange des trames de plusieurs octets :

  • STX : 0x02 (début de trame)
  • LG : longueur de la trame en octets (sans le checksum)
  • CMD : octet de commande (parfois absent)
  • DATA : de 0 à plusieurs octets de données
  • CHKSUM : checksum de la trame

Le checksum, sur un octet, est la somme (masquée sur 1 octet) de tous les octets de la trame qui précèdent le checksum.

Le protocole implémente plusieurs commandes :

  • 22 : demande d’accusé de réception (ACK request)
  • 24 : demande de température (TEMP request)
  • 128 : réponse à un accusé de réception

Écriture sur le port série #

Sans surprise, on écrit sur la liaison série avec write :

...
# Écriture d'un seul octet (ici 0)
ser.write(b'\x00')
# Écriture de 4 octets (0, 255, 0, 255)
ser.write(b'\x00\xff\x00\xff')
# Avec l'arduino, pensez à vider le buffer de lecture
ser.reset_input_buffer()

Écrivez un programme nommé write_4_bytes.py qui écrit les octets de valeurs (ici données en décimal): 2, 3, 22 et 27 (4 octets), puis lit le port série tant que des octets y sont présents. Notez les octets que vous avez reçu.

En utilisant les détails du protocole de communication indiqués précédemment, à quoi correspondent les octets que vous avez envoyé au microcontrôleur ? Et à quoi correspondent les octets que vous avez reçu en réponse ? Les checksums sont-ils corrects ?

Lecture de la température #

On va se placer dans la situation d’une personne qui doit faire la rétro-ingénierie d’un appareil ou d’un protocole de communication (ici d’un protocole de communication). Vous disposez d’informations sur le protocole (la présence de l’octet START, de l’octet qui code la longueur etc.) mais elles sont incomplètes et vous ne savez pas comment l’appareil va répondre à la demande de température (ni comment la température sera codée : int ? float ? sur combien d’octets ? valeur signée ou non ?).

Écrivez le programme send_frame.py qui envoie une trame de demande de température et affiche les octets reçus en réponse. Vous décoderez manuellement les octets reçus (l’octet CMD n’est pas présent dans la réponse). C’est à vous de trouver comment interpréter les octets reçus, et vous devez expliquer la démarche. Ceci peut éventuellement vous aider : https://fr.wikipedia.org/wiki/Boutisme.

Par ailleurs, la température renvoyée varie un peu entre chaque demande, vous pouvez vous aider de plusieurs lectures. Enfin, la température renvoyée tourne normalement autour de 25°.

Si la température renvoyée par le microcontrôleur était 32°C, quelle serait la trame reçue (vous avez 5 octets à donner) ?

Dans le fichier ask_temp.py, écrivez une fonction qui envoie une demande de température, attend la réponse, la décode, et renvoie la température sous forme numérique (nombre à virgule flottante). Voici une liste de points à traiter (selon le temps dont vous disposez) :

  • vérification de la trame (est-ce bien une réponse de température ?)
  • traitement des erreurs de décodage de la trame
  • vérification du checksum

Fonctionnalité cachée #

Dans la version Arduino, le microcontrôleur répond à d’autres commandes. Êtes-vous capables de les trouver et de comprendre ce qu’elles font ? Trouvez l’easter egg