NI TestStand coûte 4 310 $/poste/an, vous enferme sur Windows et nécessite des ingénieurs spécialisés pour la maintenance. Python avec OpenHTF vous offre les mêmes capacités de séquencement de tests avec des outils open source, un support multiplateforme et le contrôle de version. TofuPilot remplace la journalisation en base de données et le Process Model de TestStand avec des analyses structurées prêtes à l'emploi.
Pourquoi les équipes migrent
La décision se résume généralement à trois facteurs :
| Facteur | TestStand | Python + OpenHTF |
|---|---|---|
| Coût de licence | 4 310 $/poste/an | Gratuit |
| Plateforme | Windows uniquement | Windows, Linux, macOS |
| Contrôle de version | Fichiers binaires .seq, difficiles à comparer | Fichiers .py en texte brut, support Git complet |
| CI/CD | Intégrations personnalisées nécessaires | Outillage Python natif |
| Recrutement | Nécessite des ingénieurs formés à TestStand | Tout développeur Python |
| Éditeur de test | Interface propriétaire | N'importe quel éditeur de code |
| Pilotes d'instruments | NI VISA + IVI | PyVISA (mêmes instruments) |
| Gestion des données | Schéma BDD complexe + requêtes personnalisées | TofuPilot (analyses intégrées) |
La migration n'a pas besoin de se faire en une seule fois. La plupart des équipes font fonctionner les deux systèmes en parallèle pendant la transition, en convertissant une procédure de test à la fois.
Concepts TestStand en Python
Chaque concept TestStand a un équivalent direct en Python. Ce mapping couvre les éléments fondamentaux.
Séquences et étapes
Dans TestStand, vous construisez une séquence d'étapes dans l'éditeur de séquence. Dans OpenHTF, vous définissez des phases comme des fonctions Python et les passez à un objet Test.
TestStand :
MainSequence
├── Setup (groupe Precondition)
├── PowerOnSelfTest (étape)
├── FunctionalTest (étape)
└── Cleanup (groupe Postcondition)
Python avec OpenHTF :
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
def setup(test):
"""Équivalent du groupe Setup de TestStand."""
pass # Initialiser le banc, les instruments, etc.
@htf.measures(
htf.Measurement("post_voltage")
.in_range(4.8, 5.2)
.with_units(units.VOLT),
)
def power_on_self_test(test):
"""Équivalent d'une étape NumericLimitTest de TestStand."""
voltage = 5.01 # Remplacer par la lecture instrument
test.measurements.post_voltage = voltage
@htf.measures(
htf.Measurement("firmware_crc")
.equals("0xA3F7B2C1"),
)
def functional_test(test):
"""Équivalent d'une étape StringValueTest de TestStand."""
crc = "0xA3F7B2C1" # Remplacer par la requête au DUT
test.measurements.firmware_crc = crc
def cleanup(test):
"""Équivalent du groupe Cleanup de TestStand."""
pass # Désactiver les sorties, fermer les connexions
def main():
test = htf.Test(
setup,
power_on_self_test,
functional_test,
cleanup,
procedure_id="FCT-001",
part_number="PCBA-100",
)
with TofuPilot(test):
test.execute(test_start=lambda: input("Scanner le numéro de série : "))
if __name__ == "__main__":
main()Types d'étapes
TestStand a des types d'étapes intégrés : NumericLimitTest, StringValueTest, PassFailTest. OpenHTF utilise des objets Measurement avec des validateurs.
| Type d'étape TestStand | Équivalent OpenHTF |
|---|---|
| NumericLimitTest | Measurement("name").in_range(low, high).with_units(unit) |
| StringValueTest | Measurement("name").equals("expected") |
| PassFailTest | Measurement("name").equals(True) |
| MultipleNumericLimitTest | Plusieurs objets Measurement sur la même phase |
| NI Switch / Action | Fonction Python simple (sans décorateur de mesure) |
Partage de données entre phases
TestStand utilise FileGlobals, StationGlobals et Locals pour passer des données entre étapes. Dans OpenHTF, utilisez des plugs avec des attributs d'instance pour partager les données entre phases. Le plug persiste pendant toute l'exécution du test.
from openhtf.plugs import BasePlug
import openhtf as htf
class CalibrationPlug(BasePlug):
"""Stocke les données de calibration partagées entre phases."""
def setUp(self):
self.offset = 0.0
def tearDown(self):
pass
@htf.plug(cal=CalibrationPlug)
def phase_one(test, cal):
"""Stocker les données de calibration pour les phases suivantes."""
cal.offset = 0.023
@htf.plug(cal=CalibrationPlug)
def phase_two(test, cal):
"""Utiliser les données de calibration d'une phase précédente."""
raw_reading = 3.31 # Depuis l'instrument
corrected = raw_reading - cal.offsetModules de code
TestStand utilise des modules de code (DLL, assemblages .NET, VI LabVIEW) pour interfacer avec les instruments et les DUT. OpenHTF utilise des Plugs, qui sont des classes Python avec gestion automatique du cycle de vie.
import pyvisa
from openhtf.plugs import BasePlug
class MultimeterPlug(BasePlug):
"""Équivalent d'un module de code TestStand encapsulant un pilote d'instrument."""
def setUp(self):
"""Appelé une fois avant le test. Comme le point d'entrée Setup de TestStand."""
rm = pyvisa.ResourceManager()
self.instr = rm.open_resource("TCPIP::192.168.1.100::INSTR")
self.instr.timeout = 5000
def measure_voltage(self, channel=1):
"""Interroger le multimètre pour une mesure de tension DC."""
self.instr.write(f":CONF:VOLT:DC AUTO,(@{channel})")
self.instr.write(":INIT")
return float(self.instr.query(":FETCH?"))
def tearDown(self):
"""Appelé après le test. Comme le point d'entrée Cleanup de TestStand."""
self.instr.close()Remplacer la journalisation en base de données de TestStand
Le logger de base de données intégré de TestStand écrit dans SQL Server, Oracle ou Access en utilisant un schéma fixe. Les tables principales (UUT_RESULT, STEP_RESULT, PROP_RESULT, PROP_NUMERICLIMIT, PROP_NUMERIC) nécessitent 5 à 6 JOIN pour une simple requête de mesure.
Le problème du schéma de base de données TestStand
Une requête typique pour obtenir les résultats de test d'une unité dans la base de données TestStand :
-- JOIN de 6 tables pour obtenir les mesures d'un numéro de série
SELECT
u.UUT_SERIAL_NUMBER,
u.UUT_STATUS,
s.STEP_NAME,
s.STATUS,
n.DATA AS measured_value,
nl.LOW AS lower_limit,
nl.HIGH AS upper_limit,
nl.UNITS
FROM UUT_RESULT u
JOIN STEP_RESULT s ON s.UUT_RESULT = u.ID
JOIN PROP_RESULT p ON p.STEP_RESULT = s.ID
JOIN PROP_NUMERICLIMIT nl ON nl.PROP_RESULT = p.ID
JOIN PROP_NUMERIC n ON n.PROP_RESULT = p.ID
WHERE u.UUT_SERIAL_NUMBER = 'SN-5001'
ORDER BY u.START_DATE_TIME DESCCe schéma est rigide. Ajouter des métadonnées personnalisées (version firmware, ID de station, opérateur) signifie modifier le mapping de base de données du Process Model, ce qui est fragile lors des mises à jour de TestStand.
TestStand n'inclut pas d'analyses. FPY, Cpk, cartes de contrôle et Pareto de défaillances nécessitent tous du SQL personnalisé ou un outil tiers.
TofuPilot remplace tout cela
Avec OpenHTF + TofuPilot, vous ne gérez pas de base de données. Les mesures passent directement de votre code de test :
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
@htf.measures(
htf.Measurement("rail_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT),
htf.Measurement("current_draw")
.in_range(0.1, 0.5)
.with_units(units.AMPERE),
)
def power_test(test):
test.measurements.rail_3v3 = 3.31
test.measurements.current_draw = 0.25
def main():
test = htf.Test(power_test, procedure_id="FCT-001", part_number="PCBA-100")
with TofuPilot(test):
test.execute(test_start=lambda: input("Scanner le numéro de série : "))
if __name__ == "__main__":
main()Pas de code de connexion à la base de données. Pas de SQL. Pas de maintenance de schéma.
| Base de données TestStand | TofuPilot |
|---|---|
| JOIN de 6 tables pour les résultats d'une unité | Recherche par numéro de série, historique complet |
| SQL personnalisé pour le FPY | Tendances FPY mises à jour en temps réel |
| Pas de Cpk sans code personnalisé | Cpk par mesure, automatique |
| Pas de cartes de contrôle | Cartes de contrôle avec UCL/LCL |
| Pas de Pareto de défaillances | Pareto de défaillances avec exploration |
| Schéma verrouillé au design de NI | Données structurées, accès API REST |
| SQL Server/Oracle/Access | Cloud ou auto-hébergé |
| Un site à la fois | Multi-site dès le premier jour |
TofuPilot suit automatiquement le FPY, le Cpk, le débit et l'analyse des défaillances. Ouvrez l'onglet Analytics pour voir les tendances de n'importe quelle procédure.
Importer les données historiques de TestStand
Vous avez des années de données de test dans la base de données TestStand. Le SDK Python de TofuPilot vous permet de les importer :
import pyodbc
from tofupilot import TofuPilotClient
client = TofuPilotClient()
conn = pyodbc.connect(
"DRIVER={SQL Server};"
"SERVER=teststand-db;"
"DATABASE=TestStandResults;"
)
cur = conn.cursor()
cur.execute("""
SELECT
u.ID,
u.UUT_SERIAL_NUMBER,
u.UUT_STATUS,
u.START_DATE_TIME,
u.EXECUTION_TIME,
s.STEP_NAME,
s.STATUS,
n.DATA,
nl.LOW,
nl.HIGH,
nl.UNITS
FROM UUT_RESULT u
JOIN STEP_RESULT s ON s.UUT_RESULT = u.ID
LEFT JOIN PROP_RESULT p ON p.STEP_RESULT = s.ID
LEFT JOIN PROP_NUMERICLIMIT nl ON nl.PROP_RESULT = p.ID
LEFT JOIN PROP_NUMERIC n ON n.PROP_RESULT = p.ID
ORDER BY u.START_DATE_TIME
""")
current_uut_id = None
steps = []
for row in cur:
uut_id, serial, status, started, duration, step_name, step_status, value, low, high, unit = row
if current_uut_id and current_uut_id != uut_id:
client.create_run(
procedure_id="FCT-001",
unit_under_test={"serial_number": prev_serial},
run_passed=(prev_status == "Passed"),
started_at=prev_started.isoformat(),
duration=prev_duration,
steps=steps,
)
steps = []
current_uut_id = uut_id
prev_serial = serial
prev_status = status
prev_started = started
prev_duration = duration
step = next((s for s in steps if s["name"] == step_name), None)
if not step:
step = {"name": step_name, "step_passed": step_status == "Passed", "measurements": []}
steps.append(step)
if value is not None:
step["measurements"].append({
"name": step_name,
"measured_value": value,
"unit": unit or "",
"lower_limit": low,
"upper_limit": high,
})
conn.close()Adaptez la chaîne de connexion et le procedure_id à votre configuration. Exécutez ce script une fois pour remplir votre espace TofuPilot avec les tendances historiques.
Process Models
Le Process Model de TestStand gère la saisie du numéro de série, la génération de rapports et la journalisation en base de données. Avec TofuPilot, vous obtenez les trois :
- Saisie du numéro de série via
test.execute(test_start=lambda: input("Scanner : "))ou une interface personnalisée - Génération de rapports automatique (chaque exécution obtient une page détaillée dans TofuPilot)
- Journalisation en base de données à chaque exécution (mesures, limites, réussite/échec, pièces jointes)
- Analyses (FPY, Cpk, cartes de contrôle) calculées automatiquement à partir de vos données
Pas besoin de callbacks de sortie personnalisés ni de connecteurs de base de données.
Stratégie de migration
Phase 1 : Fonctionnement en parallèle (Semaine 1-2)
Gardez TestStand en fonctionnement. Configurez un environnement Python à côté. Convertissez une procédure de test simple (la plus facile, avec le moins d'instruments). Exécutez les deux versions sur les mêmes DUT pour valider que les résultats concordent.
Poste de test
├── TestStand (tests existants)
└── Python + OpenHTF (nouveau test, même DUT)
└── TofuPilot (journalisation des données)
Phase 2 : Pilotes d'instruments (Semaine 2-4)
Convertissez les modules de code TestStand en plugs Python. La plupart des instruments NI fonctionnent avec PyVISA (même couche VISA que TestStand utilise). Les instruments tiers avec support SCPI fonctionnent directement.
| Pilote TestStand | Équivalent Python |
|---|---|
| NI VISA / IVI | PyVISA + pyvisa-py ou backend NI-VISA |
| NI DAQmx | nidaqmx (package Python officiel NI) |
| NI Switch | niswitch (package Python officiel NI) |
| NI DMM | nidmm (package Python officiel NI) |
| Série / UART | pyserial |
| DLL personnalisée | ctypes ou cffi |
Phase 3 : Conversion complète (Semaine 4-8)
Convertissez les procédures de test restantes une à la fois. Commencez par les procédures à plus fort volume (impact maximal sur le débit). Gardez TestStand en solution de secours jusqu'à ce que chaque procédure soit validée.
Phase 4 : Mise hors service (Semaine 8+)
Une fois que toutes les procédures fonctionnent en Python et sont validées par rapport aux résultats TestStand, mettez TestStand hors service. Annulez les renouvellements de licence. Archivez les fichiers .seq.
Ce que vous gagnez
Après la migration, votre infrastructure de test est différente :
- Scripts de test dans Git. Historique complet des différences, revue de code sur les changements de test, branches pour les nouvelles variantes produit.
- CI/CD pour les tests. Exécutez le linting, la vérification de types et les tests unitaires sur les scripts de test avant le déploiement sur les postes.
- Tout OS. Les postes de test peuvent fonctionner sous Linux (moins cher, plus stable pour la production longue durée).
- Tout éditeur. VS Code, PyCharm, vim. Pas d'IDE propriétaire.
- Analyses dès le premier jour. TofuPilot vous donne FPY, Cpk, cartes de contrôle et Pareto de défaillances sans construire d'intégrations de base de données personnalisées.
- La moitié du vivier de recrutement s'ouvre. Tout développeur Python peut contribuer au développement des tests.
Pièges courants
Ne pas tout convertir en même temps
Le plus grand risque de migration est d'essayer de convertir toutes les procédures simultanément. Convertissez-en une, validez-la, passez à la suivante. Le fonctionnement en parallèle est votre filet de sécurité.
Ne pas négliger la validation des instruments
PyVISA communique avec les mêmes instruments, mais le timing et le comportement de déclenchement peuvent différer des pilotes NI VISA. Validez que les mesures concordent entre l'ancien et le nouveau système sur le même DUT. Une différence de mesure de 0,1 % compte lorsque vos limites sont serrées.
Ne pas perdre l'historique de vos données de test
Exportez les données historiques de la base de données TestStand avant la mise hors service. Utilisez le script d'import ci-dessus pour remplir TofuPilot afin de maintenir la traçabilité et l'analyse des tendances de part et d'autre de la frontière de migration.