Migrating from Legacy Systems

Migrer de NI TestStand vers Python

Un guide pratique pour remplacer NI TestStand par Python et OpenHTF pour les tests de fabrication, avec des modèles de migration pas à pas et.

JJulien Buteau
intermediate12 min de lecture14 mars 2026

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 :

FacteurTestStandPython + OpenHTF
Coût de licence4 310 $/poste/anGratuit
PlateformeWindows uniquementWindows, Linux, macOS
Contrôle de versionFichiers binaires .seq, difficiles à comparerFichiers .py en texte brut, support Git complet
CI/CDIntégrations personnalisées nécessairesOutillage Python natif
RecrutementNécessite des ingénieurs formés à TestStandTout développeur Python
Éditeur de testInterface propriétaireN'importe quel éditeur de code
Pilotes d'instrumentsNI VISA + IVIPyVISA (mêmes instruments)
Gestion des donnéesSchéma BDD complexe + requêtes personnaliséesTofuPilot (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 :

migration/sequence_mapping.py
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
NumericLimitTestMeasurement("name").in_range(low, high).with_units(unit)
StringValueTestMeasurement("name").equals("expected")
PassFailTestMeasurement("name").equals(True)
MultipleNumericLimitTestPlusieurs objets Measurement sur la même phase
NI Switch / ActionFonction 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.

migration/shared_data.py
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.offset

Modules 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.

migration/plug_mapping.py
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 :

teststand_query.sql
-- 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 DESC

Ce 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 :

migration/tofupilot_replacement.py
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 TestStandTofuPilot
JOIN de 6 tables pour les résultats d'une unitéRecherche par numéro de série, historique complet
SQL personnalisé pour le FPYTendances FPY mises à jour en temps réel
Pas de Cpk sans code personnaliséCpk par mesure, automatique
Pas de cartes de contrôleCartes de contrôle avec UCL/LCL
Pas de Pareto de défaillancesPareto de défaillances avec exploration
Schéma verrouillé au design de NIDonnées structurées, accès API REST
SQL Server/Oracle/AccessCloud ou auto-hébergé
Un site à la foisMulti-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 :

migration/import_teststand_history.py
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 / IVIPyVISA + pyvisa-py ou backend NI-VISA
NI DAQmxnidaqmx (package Python officiel NI)
NI Switchniswitch (package Python officiel NI)
NI DMMnidmm (package Python officiel NI)
Série / UARTpyserial
DLL personnaliséectypes 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.

Plus de guides

Mettez ce guide en pratique