Test Station Setup

Séquencement et orchestration de tests

Apprenez à construire des séquences de test hardware répétables avec TofuPilot, OpenHTF et Python pour une orchestration de test structurée et automatisée.

JJulien Buteau
intermediate11 min de lecture14 mars 2026

Un test matériel n'est pas une vérification unique. C'est une séquence : mise sous tension, attente du démarrage, mesure des tensions, calibration, vérification des communications, test de stress, mise hors tension. Capturer chaque étape avec ses mesures et son timing donne un enregistrement structuré du flux complet.

Pourquoi le séquencement des tests est important

Exécuter des tests manuellement depuis un instrument de banc fonctionne pour les prototypes. Cela ne fonctionne pas en production. Les tests de production nécessitent :

  • Répétabilité : Chaque unité passe par les mêmes étapes exactement dans le même ordre
  • Rapidité : Pas d'attente pour qu'un opérateur clique sur « suivant »
  • Capture des données : Chaque mesure est enregistrée automatiquement avec ses limites et son verdict
  • Traçabilité : Un enregistrement exact de ce qui a été testé, dans quel ordre, avec quels résultats

L'orchestration de test consiste à définir cette séquence une fois et à l'exécuter de la même manière à chaque fois.

Architecture d'une séquence de test

Une séquence de test bien structurée suit un schéma :

┌─────────────┐ │ Setup │ Mise sous tension, initialisation des instruments, identification du DUT ├─────────────┤ │ Étape 1 │ Mesure ou action avec critères de conformité ├─────────────┤ │ Étape 2 │ Mesure ou action suivante ├─────────────┤ │ ... │ Étapes supplémentaires selon les besoins ├─────────────┤ │ Teardown │ Mise hors tension, libération des instruments, envoi des résultats └─────────────┘

Chaque étape produit des mesures. Chaque mesure a des limites. La séquence s'arrête en cas de défaillance critique ou continue selon votre stratégie.

Construire une séquence de test avec OpenHTF

OpenHTF est un framework Python conçu pour le séquencement de tests matériels. TofuPilot s'intègre en tant que callback de sortie.

Les plugs héritent de BasePlug avec setUp et tearDown (attention à la casse, OpenHTF n'appellera pas setup/teardown), et sont injectés dans une phase avec @htf.plug(nom=ClassePlug).

production_test_sequence.py
97 lines
import timeimport openhtf as htffrom openhtf.plugs import BasePlugfrom tofupilot.openhtf import uploadclass PowerSupplyPlug(BasePlug):    """Contrôle l'alimentation de banc."""    def setUp(self):        self.psu = connect_power_supply()    def set_voltage(self, voltage):        self.psu.write(f"VOLT {voltage}")    def enable_output(self):        self.psu.write("OUTP ON")    def disable_output(self):        self.psu.write("OUTP OFF")    def tearDown(self):        self.disable_output()class DMMPlug(BasePlug):    """Lit les valeurs du multimètre numérique."""    def setUp(self):        self.dmm = connect_dmm()    def measure_voltage(self):        return float(self.dmm.query("MEAS:VOLT:DC?"))    def measure_current(self):        return float(self.dmm.query("MEAS:CURR:DC?"))# Étape 1 : Vérification des rails d'alimentation@htf.PhaseOptions(name="Power Rail Verification")@htf.plug(psu=PowerSupplyPlug, dmm=DMMPlug)@htf.measures(    htf.Measurement("vcc_3v3").in_range(3.25, 3.35).with_units("V"),    htf.Measurement("vcc_1v8").in_range(1.75, 1.85).with_units("V"),    htf.Measurement("vcc_5v0").in_range(4.90, 5.10).with_units("V"),)def power_rail_check(test, psu, dmm):    psu.set_voltage(12.0)    psu.enable_output()    time.sleep(0.5)  # Attendre la stabilisation des rails    test.measurements.vcc_3v3 = dmm.measure_voltage()    # Changer le canal du DMM et mesurer les autres rails    test.measurements.vcc_1v8 = dmm.measure_voltage()    test.measurements.vcc_5v0 = dmm.measure_voltage()# Étape 2 : Consommation de courant@htf.PhaseOptions(name="Current Consumption")@htf.plug(psu=PowerSupplyPlug, dmm=DMMPlug)@htf.measures(    htf.Measurement("idle_current_ma").in_range(30, 60).with_units("mA"),    htf.Measurement("active_current_ma").in_range(80, 150).with_units("mA"),)def current_check(test, psu, dmm):    test.measurements.idle_current_ma = dmm.measure_current() * 1000    trigger_active_mode()    time.sleep(0.2)    test.measurements.active_current_ma = dmm.measure_current() * 1000# Étape 3 : Vérification des communications@htf.PhaseOptions(name="Communication Interfaces")@htf.measures(    htf.Measurement("uart_loopback").equals(True),    htf.Measurement("spi_whoami").equals(0x68),)def comm_check(test):    test.measurements.uart_loopback = verify_uart_loopback()    test.measurements.spi_whoami = read_spi_register(0x75)def main():    test = htf.Test(        power_rail_check,        current_check,        comm_check,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",  # UUID de procédure du dashboard        part_number="PCBA-100",    )    test.add_output_callbacks(upload())    test.execute(lambda: input("Scanner le numéro de série du DUT : "))if __name__ == "__main__":    main()

Installez-le avec pip install "tofupilot[openhtf]". Chaque phase s'exécute dans l'ordre, chaque mesure est capturée, et la séquence complète est envoyée à la fin du test.

Un point que l'exemple passe sous silence : les trois rails sont lus depuis le même appel DMM sans changer de canal. Sur un vrai montage, il faudrait commuter le multiplexeur entre les lectures ou utiliser des canaux distincts, sinon les trois mesures enregistrent le même rail.

Construire une séquence de test avec le SDK Python

Si vous n'utilisez pas OpenHTF, créez l'exécution directement. Chaque étape devient une phase avec ses mesures et son verdict.

sequence_with_client.py
68 lines
import osimport timefrom datetime import datetime, timezonefrom tofupilot.v2 import TofuPilotserial = input("Scanner le numéro de série du DUT : ")phases = []# Étape 1 : Rails d'alimentationstarted = datetime.now(timezone.utc)psu.enable(12.0)time.sleep(0.5)vcc_3v3 = dmm.measure_voltage(channel=1)vcc_1v8 = dmm.measure_voltage(channel=2)rails_ok = 3.25 <= vcc_3v3 <= 3.35 and 1.75 <= vcc_1v8 <= 1.85phases.append({    "name": "Power Rail Verification",    "outcome": "PASS" if rails_ok else "FAIL",    "started_at": started,    "ended_at": datetime.now(timezone.utc),    "measurements": [        {            "name": "VCC 3V3",            "measured_value": vcc_3v3,            "units": "V",            "validators": [                {"operator": ">=", "expected_value": 3.25},                {"operator": "<=", "expected_value": 3.35},            ],        },        {            "name": "VCC 1V8",            "measured_value": vcc_1v8,            "units": "V",            "validators": [                {"operator": ">=", "expected_value": 1.75},                {"operator": "<=", "expected_value": 1.85},            ],        },    ],})# Étape 2 : Vérification fonctionnellestarted = datetime.now(timezone.utc)boot_ok = wait_for_boot(timeout=5)phases.append({    "name": "Boot Sequence",    "outcome": "PASS" if boot_ok else "FAIL",    "started_at": started,    "ended_at": datetime.now(timezone.utc),    "measurements": [        {"name": "Boot Success", "measured_value": boot_ok},    ],})# Envoyer la séquence complèterun_outcome = "PASS" if all(p["outcome"] == "PASS" for p in phases) else "FAIL"with TofuPilot(api_key=os.getenv("TOFUPILOT_API_KEY")) as client:    client.runs.create(        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",        serial_number=serial,        part_number="PCBA-100",        outcome=run_outcome,        phases=phases,    )

Notez que les limites accompagnent la mesure sous forme de validators plutôt que de vivre uniquement dans le if au-dessus. C'est ce qui garde le résultat interprétable plus tard, quand les limites auront changé et que vous regarderez d'anciennes données.

Bonnes pratiques de conception de séquence

PratiquePourquoi
Tester les éléments simples en premierSi un rail est en court-circuit, inutile de perdre du temps sur les communications
Regrouper les mesures liéesToutes les tensions dans une étape, tous les courants dans une autre
Utiliser des noms d'étapes cohérents« Power Rail Verification » partout, pas « Voltage Check » ici et « Rail Test » là
Inclure setup/teardownToujours couper l'alimentation du DUT à la fin, même si le test échoue
Définir des limites sur chaque mesureUne mesure sans limites ne peut être ni suivie ni analysée

Le point sur le teardown mérite d'être souligné. Une phase qui lève une exception laisse l'alimentation active à moins que le tearDown du plug ne la coupe : c'est précisément pourquoi la mise hors tension appartient au plug et non à la fin du corps d'une phase.

Gestion des échecs de séquence

Deux stratégies quand une étape échoue :

Échec rapide (fail-fast) : Arrêter la séquence au premier échec. Pour les tests critiques pour la sécurité ou quand un échec à l'étape 1 rend les suivantes inutiles (un rail en court-circuit rend inutile le test des communications).

Exécuter tout (run-all) : Continuer malgré un échec. Quand vous voulez des données de diagnostic complètes, savoir lesquelles parmi 20 mesures ont échoué aide à l'analyse des causes.

Dans OpenHTF, une phase retourne htf.PhaseResult.STOP pour arrêter la séquence, ou CONTINUE pour poursuivre. La plupart des séquences de production sont en run-all avec un garde-fou fail-fast sur les premières phases liées à la sécurité.

Ce qui est enregistré pour chaque séquence

Chaque exécution de test capture :

DonnéeObjectif
Identifiant de procédureQuelle séquence a été exécutée
Numéro de sérieQuelle unité a été testée
Verdict globalLa séquence a-t-elle réussi ?
Phases avec mesuresChaque étape, chaque mesure, chaque limite
HorodatagesQuand le test a commencé et terminé
Identifiant de stationQuelle station a exécuté le test
DuréeCombien de temps la séquence a pris

Ces données structurées sont ce qui permet le suivi des tendances, la comparaison et l'analyse sur tout votre historique de production.

Plus de guides

Mettez ce guide en pratique