Interfaces Logiciel–Matériel

Acquisition • Traitement • Visualisation temps réel

Introduction

Interface Logiciel ↔ Matériel

Cette formation a pour objectif d’introduire les bases du développement d’interfaces logiciel–matériel avec Python. Les étudiants apprendront à communiquer avec des équipements externes, acquérir des données temps réel, traiter les informations et construire des interfaces graphiques modernes avec PyQt.

Objectifs pédagogiques

Technologies utilisées

💡 Cette approche est utilisée dans de nombreux domaines : instrumentation scientifique, IoT, automatisation, acquisition de données et systèmes temps réel.

📘 Télécharger le récapitulatif du cours

💬 Besoin d’aide ou rencontre un problème ?

Si vous avez des difficultés avec cette formation (installation, code, erreurs ou compréhension), vous pouvez poser vos questions sur notre forum dédié.

👉 Rejoignez la communauté ici : https://tahinaprime.mg/forum/

🧑‍🎓 Il suffit de créer un compte gratuit pour pouvoir publier vos questions et interagir avec les autres étudiants.
Partie 1

Environnement de développement

Objectifs

Mettre en place un environnement Python isolé et reproductible.

Contenu

Anaconda, environnements virtuels, installation et exécution de scripts Python.

Commandes

conda --version
conda create -n interface_tp python=3.10
conda activate interface_tp
python --version

Activités

Partie 2

Communication avec le matériel

Objectifs

Comprendre la communication entre logiciel et matériel via port série.

Contenu

UART, pyserial, flux de données continu.

IMPORTANT

⚠️ Si le port série n’est pas détecté sous Windows, il peut être nécessaire d’installer le pilote USB-série CH340/CH341.

📥 Télécharger le pilote CH341SER.EXE

Ce pilote permet au système de reconnaître correctement certains modules Arduino, capteurs et interfaces USB-UART.
⚠️ Avant d’installer pyserial, il faut toujours activer l’environnement conda du projet.

Activation de l’environnement

conda activate interface_tp

Installation de pyserial

pip install pyserial

Activités

Exemple complet

import serial

# Remplacer COM3 par votre port (ex: /dev/ttyUSB0 sous Linux)
ser = serial.Serial('COM3', 9600, timeout=1)

print("Connexion établie")

while True:
    data = ser.readline().decode().strip()
    if data:
        print("Donnée reçue :", data)

Détection des ports série disponibles

Avant de se connecter à un équipement (Arduino, capteur, etc.), il est utile de vérifierles ports série disponibles sur l’ordinateur. Cela permet d’identifier le bon port(COM3, /dev/ttyUSB0, etc.).

from serial.tools import list_ports

# Liste tous les ports série disponibles
ports = list_ports.comports()

print("Ports série disponibles :")

for port in ports:
    print("Port :", port.device)
    print("Description :", port.description)
    print("Hardware ID :", port.hwid)
    print("------")

Explication

💡 Cette étape est importante pour éviter les erreurs de connexion et choisir le bon portavant d’ouvrir la communication série.

Partie 3

Traitement des données

Objectifs

Transformer des données brutes en informations exploitables.

Contenu

Parsing, validation, conversion numérique.

Activités

Exemple complet

import serial

# ===== PARTIE 1 + 2 + 3  =====

ser = serial.Serial('COM3', 9600, timeout=1)

print("Système initialisé")

while True:
    data = ser.readline().decode().strip()

    if data:
        print("Brut :", data)

        # Traitement des données
        try:
            value = float(data)
            print("Valeur numérique :", value)
        except:
            print("Erreur de conversion")
Partie 4

Lecture continue

Objectifs

Gérer un flux de données en temps réel sans blocage.

Contenu

Threads, boucle continue, architecture modulaire.

Activités

Exemple complet

import serial
import threading

# ===== PARTIE 1 + 2 + 3 + 4  =====

ser = serial.Serial('COM9', 115200, timeout=1)

def read_serial():
    while True:
        data = ser.readline().decode().strip()

        if data:
            print("\nBrut :", data)

            try:
                # Cas où les données contiennent 4 valeurs séparées par des espaces
                values = data.split()

                if len(values) == 4:
                    v1 = float(values[0])
                    v2 = float(values[1])
                    v3 = float(values[2])
                    v4 = float(values[3])

                    print("Valeurs :", v1, v2, v3, v4)
                else:
                    print("Erreur: pas 4 valeurs")

            except Exception as e:
                print("Erreur de parsing :", e)

thread = threading.Thread(target=read_serial)
thread.start()

print("Lecture temps réel activée")
Partie 5

Interface graphique (PyQt)

⚠️ IMPORTANT : Avant d’installer PyQt, assurez-vous d’avoir activé l’environnement conda du projet.

Activation de l’environnement

conda activate interface_tp

Installation

pip install PyQt5

Objectifs

Créer une interface utilisateur pour contrôler et afficher les données.

Contenu

Widgets, signaux/slots, communication UI-backend.

Activités

Exemple minimal PyQt

Exemple minimal utilisant une classe PyQt.

# Importer le module système
import sys

# Importer QWidget et QApplication
from PyQt5.QtWidgets import QApplication, QWidget

# Créer une classe de fenêtre
class App(QWidget):

    # Constructeur de la classe
    def __init__(self):

        # Initialiser QWidget
        super().__init__()

        # Définir le titre de la fenêtre
        self.setWindowTitle("Ma première fenêtre PyQt")

        # Définir la taille de la fenêtre
        self.resize(400, 300)

# Créer l'application Qt
app = QApplication(sys.argv)

# Créer la fenêtre
window = App()

# Afficher la fenêtre
window.show()

# Lancer la boucle principale
sys.exit(app.exec_())

Exemple complet

import sys
import serial
import threading
from PyQt5.QtWidgets import (
    QApplication, QWidget, QPushButton,
    QVBoxLayout, QLabel, QLineEdit, QComboBox
)

# ===== PARTIE 1 + 2 + 3 + 4 + 5 =====

class App(QWidget):
    def __init__(self):
        super().__init__()

        self.ser = None

        # UI
        self.label = QLabel("Aucune donnée")

        self.port_input = QLineEdit()
        self.port_input.setText("COM3")
        self.port_input.setPlaceholderText("Ex: /dev/ttyUSB0")

        self.baudrate_box = QComboBox()
        self.baudrate_box.addItems(["9600", "19200", "38400", "57600", "115200"])

        self.btn = QPushButton("Connecter")
        self.btn.clicked.connect(self.start_connection)

        layout = QVBoxLayout()
        layout.addWidget(QLabel("Port série"))
        layout.addWidget(self.port_input)
        layout.addWidget(QLabel("Baudrate"))
        layout.addWidget(self.baudrate_box)
        layout.addWidget(self.btn)
        layout.addWidget(self.label)

        self.setLayout(layout)

    def start_connection(self):
        port = self.port_input.text()
        baudrate = int(self.baudrate_box.currentText())

        try:
            self.ser = serial.Serial(port, baudrate, timeout=1)
            self.label.setText("Connecté ✔")

            threading.Thread(target=self.read_serial, daemon=True).start()

        except Exception as e:
            self.label.setText(f"Erreur: {e}")

    def read_serial(self):
        while True:
            if self.ser:
                data = self.ser.readline().decode().strip()
                if data:
                    self.label.setText(data)

# Run app
app = QApplication(sys.argv)
window = App()
window.show()
sys.exit(app.exec_())

Ressources / Tutoriel

Pour apprendre PyQt en détail : Documentation officielle PyQt5

Tutoriel pratique (YouTube) : PyQt5 tutorial vidéos

Partie 6

Visualisation temps réel (pyqtgraph)

⚠️ IMPORTANT : Toujours activer l’environnement avant d’installer ou d’exécuter pyqtgraph.

Activation de l’environnement

conda activate interface_tp

Installation

pip install pyqtgraph pyqt5 pyserial

Objectifs

Visualisation des données multi-capteurs en temps réel sous forme de 4 graphiques dynamiques.

Exemple complet

import sys
# ===== IMPORTATION DES LIBRAIRIES =====
import sys                      # Fonctions système (fermer proprement l'application)
import time                     # Gestion du temps (horodatage PC)
import serial                   # Communication série (Arduino, capteurs, etc.)
import threading                # Exécution parallèle (évite blocage de l'interface)
import pyqtgraph as pg          # Bibliothèque de graphes temps réel rapide
from PyQt5 import QtWidgets, QtCore  # Interface graphique Qt (fenêtres, boutons, timer)

# ===== CLASSE PRINCIPALE DE L'APPLICATION =====
class App(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()  # Initialisation de la classe Qt

        # ===== PARAMÈTRE DE FENÊTRE TEMPORELLE =====
        self.window_size = 40  # Afficher uniquement les 40 dernières secondes

        # ===== COMMUNICATION SÉRIE =====
        self.ser = None        # Objet port série (non initialisé)
        self.connected = False # État de connexion

        # ===== BASE DE TEMPS (PC) =====
        self.t0 = time.time()  # Temps de référence (début)
        self.time_data = []    # Liste des temps des mesures

        # ===== DONNÉES DES 4 CAPTEURS =====
        self.data1 = []  # Capteur 1
        self.data2 = []  # Capteur 2
        self.data3 = []  # Capteur 3
        self.data4 = []  # Capteur 4

        # ===== INTERFACE UTILISATEUR =====
        self.port_input = QtWidgets.QLineEdit()  # Champ pour le port série
        self.port_input.setText("COM9")          # Port par défaut

        self.baudrate_box = QtWidgets.QComboBox()  # Liste des débits
        self.baudrate_box.addItems(["9600", "19200", "38400", "57600", "115200"])

        self.btn = QtWidgets.QPushButton("Connecter")  # Bouton connexion
        self.btn.clicked.connect(self.toggle_connection)  # Action bouton

        # ===== FENÊTRE DE GRAPHIQUES =====
        self.win = pg.GraphicsLayoutWidget()  # Conteneur de graphes

        # ----- GRAPHE 1 -----
        self.plot1 = self.win.addPlot(title="Capteur 1")
        self.plot1.setMinimumHeight(100)  # Hauteur minimale
        self.curve1 = self.plot1.plot(pen='r')  # Courbe rouge

        self.win.nextRow()

        # ----- GRAPHE 2 -----
        self.plot2 = self.win.addPlot(title="Capteur 2")
        self.plot2.setMinimumHeight(100)
        self.curve2 = self.plot2.plot(pen='g')  # Courbe verte

        self.win.nextRow()

        # ----- GRAPHE 3 -----
        self.plot3 = self.win.addPlot(title="Capteur 3")
        self.plot3.setMinimumHeight(100)
        self.curve3 = self.plot3.plot(pen='b')  # Courbe bleue

        self.win.nextRow()

        # ----- GRAPHE 4 -----
        self.plot4 = self.win.addPlot(title="Capteur 4")
        self.plot4.setMinimumHeight(100)
        self.curve4 = self.plot4.plot(pen='y')  # Courbe jaune

        # ===== MISE EN PAGE =====
        layout = QtWidgets.QVBoxLayout()  # Disposition verticale

        layout.addWidget(self.port_input)   # Champ port série
        layout.addWidget(self.baudrate_box) # Sélection baudrate
        layout.addWidget(self.btn)          # Bouton connexion
        layout.addWidget(self.win)          # Zone des graphiques

        self.setLayout(layout)  # Appliquer la mise en page

        # ===== TIMER (MISE À JOUR GRAPHIQUE) =====
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.update_plot)  # Appel périodique
        self.timer.start(100)  # 100 ms → 10 FPS

    # =========================
    # GESTION CONNEXION
    # =========================

    def toggle_connection(self):
        # Alterne entre connexion et déconnexion
        if not self.connected:
            self.connect_device()
        else:
            self.disconnect_device()

    def connect_device(self):
        port = self.port_input.text()  # Récupérer port COM
        baudrate = int(self.baudrate_box.currentText())  # Vitesse communication

        try:
            # Ouverture du port série
            self.ser = serial.Serial(port, baudrate, timeout=1)
            self.connected = True

            self.btn.setText("Déconnecter")  # Changer texte bouton

            # Lancer lecture série dans un thread (non bloquant)
            threading.Thread(target=self.read_serial, daemon=True).start()

        except Exception as e:
            print("Erreur connexion :", e)

    def disconnect_device(self):
        # Arrêter lecture
        self.connected = False

        # Fermer port série
        if self.ser:
            self.ser.close()

        self.btn.setText("Connecter")  # Réinitialiser bouton

    # =========================
    # LECTURE SÉRIE (THREAD)
    # =========================

    def read_serial(self):
        # Boucle de lecture continue
        while self.connected and self.ser:
            try:
                line = self.ser.readline().decode().strip()  # Lire ligne

                if line:
                    values = line.split()  # Séparer par espaces

                    # Vérifier 4 valeurs
                    if len(values) == 4:

                        # Temps écoulé depuis le début
                        t = time.time() - self.t0

                        # Stockage du temps
                        self.time_data.append(t)

                        # Stockage des 4 capteurs
                        self.data1.append(float(values[0]))
                        self.data2.append(float(values[1]))
                        self.data3.append(float(values[2]))
                        self.data4.append(float(values[3]))

            except:
                break  # Arrêt en cas d'erreur

    # =========================
    # MISE À JOUR GRAPHIQUE
    # =========================

    def update_plot(self):
        # Vérifier s'il y a des données
        if len(self.time_data) == 0:
            return

        # Dernier temps enregistré
        t_max = self.time_data[-1]

        # Début de fenêtre (40 secondes)
        start_time = t_max - self.window_size

        # Trouver index début fenêtre
        i = 0
        for idx, t in enumerate(self.time_data):
            if t >= start_time:
                i = idx
                break

        # Découper uniquement les 40 dernières secondes
        t = self.time_data[i:]
        d1 = self.data1[i:]
        d2 = self.data2[i:]
        d3 = self.data3[i:]
        d4 = self.data4[i:]

        # Mise à jour des graphes
        self.curve1.setData(t, d1)
        self.curve2.setData(t, d2)
        self.curve3.setData(t, d3)
        self.curve4.setData(t, d4)

        # Fixer fenêtre X (temps)
        self.plot1.setXRange(start_time, t_max)
        self.plot2.setXRange(start_time, t_max)
        self.plot3.setXRange(start_time, t_max)
        self.plot4.setXRange(start_time, t_max)

# =========================
# LANCEMENT DE L'APPLICATION
# =========================

app = QtWidgets.QApplication([])  # Application Qt
window = App()                    # Création fenêtre principale

window.setWindowTitle("Moniteur Multi-Capteurs Temps Réel")  # Titre
window.resize(900, 600)         # Taille fenêtre
window.showMaximized()           # Plein écran

sys.exit(app.exec_())           # Boucle événementielle Qt

Interface professionnelle (amélioration finale)

Interface professionnelle multi-capteurs

Exemple d’interface moderne : panneau de contrôle à gauche, visualisation multi-capteurs en temps réel, design professionnel adapté aux applications industrielles.

🧩 Architecture UI

Éléments nécessaires pour créer cette interface

Pour reproduire cette interface professionnelle, les étudiants doivent maîtriser l’utilisation combinée de plusieurs layouts et widgets afin de construire une application modulaire, claire et évolutive.

📐 Layouts essentiels

🧩 Widgets principaux

💡 Conseil : Pensez en architecture en couches : Acquisition → Traitement → Affichage → Contrôle utilisateur
🚀 Voir le tutoriel complet des Widgets & Layouts PyQt

Interface professionnelle multi-capteurs

Ressources / Tutoriel

📘 Documentation officielle pyqtgraph : pyqtgraph docs
📊 Exemples officiels (très utile pour realtime plots) : GitHub examples
🎥 Tutoriels vidéo : pyqtgraph realtime tutorial