Test Station Setup

Créer un panneau de test GUI

Apprenez à créer un panneau de test GUI basé sur tkinter qui contrôle les instruments et exécute des tests OpenHTF avec journalisation TofuPilot.

JJulien Buteau
intermediate8 min de lecture14 mars 2026

Créer un panneau opérateur personnalisé permet à vos techniciens de test d'exécuter des tests sans utiliser la ligne de commande. Ce guide montre comment créer une interface GUI basée sur tkinter qui contrôle les instruments, collecte les numéros de série et exécute des tests OpenHTF avec journalisation TofuPilot.

Prérequis

  • Python 3.8+
  • OpenHTF installé (pip install openhtf)
  • SDK Python TofuPilot installé (pip install tofupilot)
  • Une clé API TofuPilot configurée

Étape 1 : Créer la disposition du panneau principal

Commencez avec une fenêtre tkinter basique qui comprend une saisie de numéro de série, des contrôles d'instrument et une zone d'exécution de test.

panel.py
import tkinter as tk
from tkinter import ttk
import threading


class TestPanel:
    def __init__(self, root):
        self.root = root
        self.root.title("Panneau de test")
        self.root.geometry("480x520")
        self.root.resizable(False, False)

        self.instrument_connected = False

        self._build_instrument_frame()
        self._build_serial_frame()
        self._build_control_frame()
        self._build_status_frame()

    def _build_instrument_frame(self):
        frame = ttk.LabelFrame(self.root, text="Instrument", padding=10)
        frame.pack(fill="x", padx=10, pady=(10, 5))

        self.connect_btn = ttk.Button(
            frame, text="Connecter", command=self._connect_instrument
        )
        self.connect_btn.pack(side="left", padx=(0, 5))

        self.disconnect_btn = ttk.Button(
            frame, text="Déconnecter", command=self._disconnect_instrument, state="disabled"
        )
        self.disconnect_btn.pack(side="left")

        self.instr_status = ttk.Label(frame, text="Déconnecté", foreground="gray")
        self.instr_status.pack(side="right")

    def _build_serial_frame(self):
        frame = ttk.LabelFrame(self.root, text="Unité sous test", padding=10)
        frame.pack(fill="x", padx=10, pady=5)

        ttk.Label(frame, text="Numéro de série :").pack(anchor="w")
        self.serial_var = tk.StringVar()
        self.serial_entry = ttk.Entry(frame, textvariable=self.serial_var, width=30)
        self.serial_entry.pack(fill="x", pady=(2, 0))

    def _build_control_frame(self):
        frame = ttk.Frame(self.root, padding=10)
        frame.pack(fill="x", padx=10)

        self.start_btn = ttk.Button(
            frame, text="Démarrer le test", command=self._start_test, state="disabled"
        )
        self.start_btn.pack(fill="x")

    def _build_status_frame(self):
        frame = ttk.LabelFrame(self.root, text="Statut", padding=10)
        frame.pack(fill="both", expand=True, padx=10, pady=(5, 10))

        self.result_label = ttk.Label(
            frame, text="En attente", font=("Helvetica", 18, "bold"), anchor="center"
        )
        self.result_label.pack(pady=(0, 10))

        self.log_text = tk.Text(frame, height=10, state="disabled", font=("Courier", 10))
        self.log_text.pack(fill="both", expand=True)

    def _log(self, message):
        self.log_text.configure(state="normal")
        self.log_text.insert("end", message + "
")
        self.log_text.see("end")
        self.log_text.configure(state="disabled")

    def _set_result(self, text, color):
        self.result_label.configure(text=text, foreground=color)

    def _connect_instrument(self):
        self._log("Connexion à l'instrument...")
        self.instrument_connected = True
        self.instr_status.configure(text="Connecté", foreground="green")
        self.connect_btn.configure(state="disabled")
        self.disconnect_btn.configure(state="normal")
        self.start_btn.configure(state="normal")
        self._log("Instrument connecté.")

    def _disconnect_instrument(self):
        self._log("Déconnexion de l'instrument...")
        self.instrument_connected = False
        self.instr_status.configure(text="Déconnecté", foreground="gray")
        self.connect_btn.configure(state="normal")
        self.disconnect_btn.configure(state="disabled")
        self.start_btn.configure(state="disabled")
        self._log("Instrument déconnecté.")

    def _start_test(self):
        serial = self.serial_var.get().strip()
        if not serial:
            self._log("Erreur : entrez un numéro de série.")
            return

        self.start_btn.configure(state="disabled")
        self.serial_entry.configure(state="disabled")
        self._set_result("En cours...", "orange")
        self._log(f"Démarrage du test pour {serial}...")

        thread = threading.Thread(target=self._run_test, args=(serial,), daemon=True)
        thread.start()

    def _run_test(self, serial):
        # Placeholder — voir Étape 4 pour l'implémentation complète
        pass


if __name__ == "__main__":
    root = tk.Tk()
    app = TestPanel(root)
    root.mainloop()

Cela vous donne un panneau propre avec des boutons de connexion/déconnexion d'instrument, un champ de numéro de série et une zone de statut qui affiche les logs et les résultats pass/fail.

Étape 2 : Ajouter un plug d'instrument

Créez un plug OpenHTF qui encapsule la communication avec votre instrument. Cet exemple simule une alimentation, mais vous remplacerez l'intérieur par vos commandes SCPI ou série réelles.

instrument_plug.py
import time
import openhtf as htf


class PowerSupplyPlug(htf.plugs.BasePlug):
    """Plug pour contrôler une alimentation de banc."""

    ADDRESS = "GPIB0::5::INSTR"

    def setUp(self):
        self._connected = False
        self._voltage = 0.0
        # Remplacer par une vraie connexion instrument (ex. : pyvisa)
        self._connected = True

    def set_voltage(self, voltage):
        if not self._connected:
            raise RuntimeError("Alimentation non connectée")
        self._voltage = voltage
        time.sleep(0.3)

    def measure_current(self):
        if not self._connected:
            raise RuntimeError("Alimentation non connectée")
        time.sleep(0.2)
        return 0.125

    def tearDown(self):
        if self._connected:
            self.set_voltage(0.0)
            self._connected = False

La méthode tearDown s'exécute automatiquement lorsque le test se termine, afin que l'instrument revienne toujours dans un état sûr.

Étape 3 : Définir les phases de test OpenHTF

Écrivez des phases de test qui utilisent le plug d'instrument pour prendre des mesures. Notez que l'injection de plug utilise le décorateur @htf.plug.

test_phases.py
import openhtf as htf
from openhtf.util import units
from instrument_plug import PowerSupplyPlug


@htf.plug(psu=PowerSupplyPlug)
@htf.measures(
    htf.Measurement("supply_voltage_setpoint").with_units(units.VOLT),
    htf.Measurement("quiescent_current")
        .in_range(minimum=0.05, maximum=0.25)
        .with_units(units.AMPERE),
)
def phase_quiescent_current(test, psu):
    """Mesurer le courant de repos à la tension nominale."""
    psu.set_voltage(3.3)
    test.measurements.supply_voltage_setpoint = 3.3

    current = psu.measure_current()
    test.measurements.quiescent_current = current


@htf.plug(psu=PowerSupplyPlug)
@htf.measures(
    htf.Measurement("overcurrent_detected").equals(True),
)
def phase_overcurrent_protection(test, psu):
    """Vérifier que le DUT déclenche la protection contre les surintensités à haute tension."""
    psu.set_voltage(5.5)
    test.measurements.overcurrent_detected = True

Chaque phase se concentre sur un seul élément. Les validateurs de mesure (.in_range, .equals) indiquent à OpenHTF ce qui constitue un pass ou un fail.

Étape 4 : Connecter la GUI à l'exécution OpenHTF

Maintenant connectez la méthode _run_test du panneau pour exécuter le test OpenHTF avec journalisation TofuPilot.

panel_with_test.py
import tkinter as tk
from tkinter import ttk
import threading
import openhtf as htf
from tofupilot.openhtf import TofuPilot
from test_phases import phase_quiescent_current, phase_overcurrent_protection


class TestPanel:
    def __init__(self, root):
        self.root = root
        self.root.title("Panneau de test")
        self.root.geometry("480x520")
        self.root.resizable(False, False)

        self.instrument_connected = False

        self._build_instrument_frame()
        self._build_serial_frame()
        self._build_control_frame()
        self._build_status_frame()

    def _build_instrument_frame(self):
        frame = ttk.LabelFrame(self.root, text="Instrument", padding=10)
        frame.pack(fill="x", padx=10, pady=(10, 5))

        self.connect_btn = ttk.Button(
            frame, text="Connecter", command=self._connect_instrument
        )
        self.connect_btn.pack(side="left", padx=(0, 5))

        self.disconnect_btn = ttk.Button(
            frame, text="Déconnecter", command=self._disconnect_instrument, state="disabled"
        )
        self.disconnect_btn.pack(side="left")

        self.instr_status = ttk.Label(frame, text="Déconnecté", foreground="gray")
        self.instr_status.pack(side="right")

    def _build_serial_frame(self):
        frame = ttk.LabelFrame(self.root, text="Unité sous test", padding=10)
        frame.pack(fill="x", padx=10, pady=5)

        ttk.Label(frame, text="Numéro de série :").pack(anchor="w")
        self.serial_var = tk.StringVar()
        self.serial_entry = ttk.Entry(frame, textvariable=self.serial_var, width=30)
        self.serial_entry.pack(fill="x", pady=(2, 0))

    def _build_control_frame(self):
        frame = ttk.Frame(self.root, padding=10)
        frame.pack(fill="x", padx=10)

        self.start_btn = ttk.Button(
            frame, text="Démarrer le test", command=self._start_test, state="disabled"
        )
        self.start_btn.pack(fill="x")

    def _build_status_frame(self):
        frame = ttk.LabelFrame(self.root, text="Statut", padding=10)
        frame.pack(fill="both", expand=True, padx=10, pady=(5, 10))

        self.result_label = ttk.Label(
            frame, text="En attente", font=("Helvetica", 18, "bold"), anchor="center"
        )
        self.result_label.pack(pady=(0, 10))

        self.log_text = tk.Text(frame, height=10, state="disabled", font=("Courier", 10))
        self.log_text.pack(fill="both", expand=True)

    def _log(self, message):
        self.log_text.configure(state="normal")
        self.log_text.insert("end", message + "
")
        self.log_text.see("end")
        self.log_text.configure(state="disabled")

    def _set_result(self, text, color):
        self.result_label.configure(text=text, foreground=color)

    def _connect_instrument(self):
        self._log("Connexion à l'instrument...")
        self.instrument_connected = True
        self.instr_status.configure(text="Connecté", foreground="green")
        self.connect_btn.configure(state="disabled")
        self.disconnect_btn.configure(state="normal")
        self.start_btn.configure(state="normal")
        self._log("Instrument connecté.")

    def _disconnect_instrument(self):
        self._log("Déconnexion de l'instrument...")
        self.instrument_connected = False
        self.instr_status.configure(text="Déconnecté", foreground="gray")
        self.connect_btn.configure(state="normal")
        self.disconnect_btn.configure(state="disabled")
        self.start_btn.configure(state="disabled")
        self._log("Instrument déconnecté.")

    def _start_test(self):
        serial = self.serial_var.get().strip()
        if not serial:
            self._log("Erreur : entrez un numéro de série.")
            return

        self.start_btn.configure(state="disabled")
        self.serial_entry.configure(state="disabled")
        self._set_result("En cours...", "orange")
        self._log(f"Démarrage du test pour {serial}...")

        thread = threading.Thread(target=self._run_test, args=(serial,), daemon=True)
        thread.start()

    def _run_test(self, serial):
        try:
            test = htf.Test(
                phase_quiescent_current,
                phase_overcurrent_protection,
                procedure_id="PCB-FUNC-001",
                part_number="PCB-2400",
            )

            with TofuPilot(test):
                test_record = test.execute(test_start=lambda: serial)

            outcome = test_record.outcome.name

            if outcome == "PASS":
                self.root.after(0, self._set_result, "PASS", "green")
                self.root.after(0, self._log, f"Test RÉUSSI pour {serial}.")
            else:
                self.root.after(0, self._set_result, "FAIL", "red")
                self.root.after(0, self._log, f"Test ÉCHOUÉ pour {serial}.")

        except Exception as e:
            self.root.after(0, self._set_result, "ERREUR", "red")
            self.root.after(0, self._log, f"Erreur : {e}")

        finally:
            self.root.after(0, self._reset_controls)

    def _reset_controls(self):
        self.serial_entry.configure(state="normal")
        self.serial_var.set("")
        if self.instrument_connected:
            self.start_btn.configure(state="normal")


if __name__ == "__main__":
    root = tk.Tk()
    app = TestPanel(root)
    root.mainloop()

Quelques points à noter ici :

  • test.execute s'exécute de manière synchrone dans le thread d'arrière-plan, afin que la GUI reste réactive.
  • root.after(0, ...) planifie les mises à jour de la GUI sur le thread principal. Tkinter n'est pas thread-safe, donc vous ne pouvez pas appeler les méthodes des widgets directement depuis un thread de travail.
  • TofuPilot(test) encapsule l'exécution et envoie automatiquement l'enregistrement du test, y compris toutes les mesures et résultats.

Plus de guides

Mettez ce guide en pratique