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.
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.
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 = FalseLa 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.
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 = TrueChaque 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.
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.executes'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.