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 tkfrom tkinter import ttkimport threadingclass 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 passif __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 timeimport openhtf as htfclass 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 htffrom openhtf.util import unitsfrom 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 tkfrom tkinter import ttkimport threadingimport openhtf as htffrom tofupilot.openhtf import TofuPilotfrom test_phases import phase_quiescent_current, phase_overcurrent_protectionclass 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.