Migrating from Legacy Systems

Migrer de NI VeriStand vers Python

Découpez un système VeriStand entre ce qui reste sur le moteur temps réel et ce qui passe en plugs, phases et mesures Python, avec l'effet sur les licences.

JJulien Buteau
advanced14 min de lecture22 septembre 2026
Validez un test avant de déployer.
Test existant
Connecter les résultats
Comparer un test
Déployer

NI VeriStand, ce sont deux produits vendus comme un seul. Le premier est un moteur d'exécution temps réel : il fait tourner des modèles de plant et mappe les E/S sur une cible PXI ou CompactRIO à cadence fixe. Le second est tout ce qui entoure ce moteur : profils de stimulus, workspace, journalisation TDMS, alarmes, API d'automatisation .NET et fichiers de résultats. Python remplace bien la seconde partie. Il ne remplace la première que dans des cas précis, et un plan de migration qui ne sépare pas les deux finit soit par reconstruire une plateforme temps réel par accident, soit par conserver des licences dont il n'a plus besoin.

Ce guide découpe un système VeriStand en couches, indique lesquelles passent en Python, lesquelles restent sur le moteur, et lesquelles sont supprimées plutôt que portées.

Ce que contient un système VeriStand

CoucheArtefact VeriStandDevenir
Moteur temps réelSystem definition (.nivssdf), mapping des E/S matérielles, modèles compilés, custom devicesReste, ou disparaît parce qu'il n'a jamais été nécessaire
Logique de test déterministeReal-time sequences (.nivsseq), profils de stimulus (.nivsstimprof)Porté : niveristand pour les parties déterministes, phases simples pour le reste
Configuration réactiveAlarmes, procédures, calculated channelsScindé : la protection reste, les critères réussite/échec deviennent des validateurs
Interaction opérateurÉcrans du workspacePorté : identification de l'unité et phases d'operator UI
JournalisationFichiers TDMS de l'outil Data Logging ou du profilPorté : mesures multidimensionnelles, fichier brut en pièce jointe
AutomatisationAppels TestStand, macros, scripts API .NETPorté : un plug VeriStand et des phases
RésultatsTDMS sur un partage, rapports DIAdem, SQL maisonNon porté : le moteur envoie les exécutions

La suite du guide parcourt ces couches dans l'ordre où une migration les touche généralement. La première décision, celle du moteur, conditionne toutes les autres.

Décider d'abord : avez-vous besoin du moteur ?

Il n'existe pas d'équivalent open source du moteur temps réel VeriStand. Une machine Linux avec le patch PREEMPT_RT, FMPy pour l'exécution des modèles et votre propre code d'E/S peut le remplacer, mais c'est la construction d'une plateforme, pas une migration, et cela occupe une équipe plusieurs trimestres. La vraie question est de savoir dans laquelle de ces trois situations vous êtes.

Du vrai HIL. Un modèle de plant tourne entre 1 et 10 kHz, la boucle est fermée à travers des E/S matérielles, l'unité testée est un calculateur ou un contrôleur, et l'injection de défauts se fait sur les lignes capteurs. Le moteur reste. Ce qui passe en Python, c'est la logique de test, la journalisation, l'automatisation et les résultats. C'est le cas courant et l'essentiel de ce guide le concerne.

Du travail de banc arrivé sur VeriStand parce qu'il était installé. Une alimentation, une carte DAQ et un multimètre, pas de modèle de plant, pas de boucle à fermer. Le moteur ne fait rien ici que nidaqmx et PyVISA ne fassent, et il coûte une licence de déploiement par banc. Python parle directement aux instruments. Voir Retirer le moteur plus bas.

Du model-in-the-loop sous Windows. Une licence VeriStand PC qui fait tourner un FMU contre une séquence de test, sans cible temps réel. FMPy charge le même FMU sur l'hôte, et la précision temporelle de Windows suffit pour cette classe de test parce qu'il n'y avait pas de cible déterministe au départ.

La conséquence sur les licences compte, parce que c'est la ligne budgétaire qui justifie généralement la migration. Un siège de développement VeriStand Full est un abonnement. Un banc qui ne fait qu'exécuter une system definition préconfigurée a besoin d'une licence Operator, achetée une fois. Après migration, le développement se fait en Python sur n'importe quelle machine, et chaque banc qui garde son moteur a besoin d'une licence Operator et de rien d'autre. NI ne publie pas de liste de prix complète et les prix varient selon la région et l'accord, donc les chiffres ci-dessous sont des prix catalogue distributeurs de 2023 à 2025, utiles seulement comme ordres de grandeur.

LicenceTypeOrdre de grandeur
VeriStand FullDéveloppement, par siège, abonnement annuel3 400 à 4 000 USD ou EUR par an
VeriStand PCDéveloppement sans cible temps réel, annuel2 400 par an
VeriStand OperatorDéploiement, par banc, perpétuelle3 000 en une fois

Une équipe avec trois sièges Full et quatre bancs finit typiquement avec quatre licences Operator et un siège Full conservé pour ouvrir l'archive.

Le plug VeriStand

Si le moteur reste, le premier morceau de Python à écrire est un plug qui possède la connexion vers lui. Le paquet niveristand s'installe avec pip et enveloppe l'API cliente .NET livrée avec VeriStand, donc il tourne sur l'hôte Windows qui parle déjà à la cible.

procedure.yaml
plugs:  - name: HIL Rig    key: rig    python: plugs.veristand:VeriStandRig    scope: station    config:      sysdef: "C:\\HIL\\Powertrain\\Powertrain.nivssdf"      gateway: "localhost"      log_dir: "C:\\HIL\\Logs"
plugs/veristand.py
20 lines
from niveristand.legacy import NIVeriStandclass VeriStandRig:    def __init__(self, sysdef: str, log_dir: str, gateway: str = "localhost", deploy_timeout_ms: int = 120000):        self.log_dir = log_dir        NIVeriStand.LaunchNIVeriStand()        NIVeriStand.WaitForNIVeriStandReady()        self._ws = NIVeriStand.Workspace2(gateway)        # deploy=True pousse la system definition sur la cible ; environ une minute sur PXI        self._ws.ConnectToSystem(sysdef, True, deploy_timeout_ms)    def get(self, channel: str) -> float:        return self._ws.GetSingleChannelValue(channel)    def set(self, channel: str, value: float) -> None:        self._ws.SetSingleChannelValue(channel, value)    def __del__(self):        self._ws.DisconnectFromSystem("", True)

Deux détails du plug portent l'essentiel de la valeur.

La portée est station. Déployer une system definition sur une cible PXI prend de l'ordre d'une minute, et un plug station le fait une fois, quand la première exécution en a besoin, puis garde la connexion entre les exécutions. Chaque unité testée après la première démarre immédiatement. Si un banc doit être redéployé entre deux unités, la portée execution le fait, au prix du temps de déploiement par exécution.

Les chemins de canaux sont l'interface. Un canal VeriStand s'adresse par son chemin dans la system definition, comme Targets/Controller/Simulation Models/Models/Engine/Outports/RPM, ou par un alias comme Aliases/DesiredRPM. Les alias sont le meilleur contrat, parce qu'un changement de modèle ou de matériel qui déplace un canal casse un chemin mais pas un alias. Définissez des alias pour tout ce que le test touche et gardez le fichier .nivsalias à côté de la procédure.

Profils de stimulus et real-time sequences

Un profil de stimulus est ce que VeriStand a de plus proche d'une séquence de test : il règle des canaux, attend, vérifie des conditions et journalise. Une real-time sequence en est la partie déterministe, compilée et exécutée sur le moteur. Ils se portent par deux chemins différents, et se tromper de chemin est la façon la plus courante de rendre un test HIL migré plus lent ou moins fiable.

Déterministe sur le moteur. niveristand convertit une fonction Python décorée en real-time sequence et l'exécute sur la cible avec run_py_as_rtseq. La fonction garde les garanties temporelles de VeriStand, au prix des restrictions de VeriStand : les valeurs sont des enveloppes typées accessibles par .value, les comparaisons ne prennent que deux opérandes, et le corps ne peut utiliser que la bibliothèque niveristand, parce qu'il est compilé, pas interprété.

sequences/rpm_step.py
from niveristand import NivsParam, nivs_rt_sequencefrom niveristand.clientapi import ChannelReference, DoubleValuefrom niveristand.library import wait_until_settled@NivsParam("setpoint", DoubleValue(0), NivsParam.BY_VALUE)@nivs_rt_sequencedef rpm_step(setpoint):    desired = ChannelReference("Aliases/DesiredRPM")    actual = ChannelReference("Aliases/ActualRPM")    settled = DoubleValue(0)    desired.value = setpoint.value    # borne haute, borne basse, fenêtre de tolérance, timeout : tout est évalué au tick du moteur    settled.value = wait_until_settled(actual, 9999999, setpoint.value - 50, 25, 10)    return settled.value
phases/rpm_response.py
from niveristand import run_py_as_rtseqfrom sequences.rpm_step import rpm_stepdef rpm_response(phase, measurements, rig):    result = run_py_as_rtseq(rpm_step, rtseq_params={"setpoint": 2500})    if result != 0:        phase.fail("Le régime ne s'est pas stabilisé en 10 s")        return    measurements.actual_rpm = rig.get("Aliases/ActualRPM")

Côté hôte dans une phase. Régler une consigne, attendre quelques secondes et lire une valeur en régime établi n'a pas besoin du timing du moteur. L'aller-retour par le gateway est de quelques dizaines de millisecondes, et pour un test dont les tolérances se comptent en secondes c'est invisible. Ce chemin n'a aucune restriction : n'importe quelle bibliothèque, n'importe quelle structure de données, des points d'arrêt dans un débogueur normal.

La règle qui sépare les deux : si la séquence réagit à un signal dans un pas, ou si son exigence temporelle est inférieure à environ 100 ms, elle reste déterministe. Tout le reste passe dans une phase, là où il devient facile à lire et à tester.

Un profil de stimulus qui mélangeait les deux, ce que font la plupart, devient une phase par étape de test, avec les parties déterministes sous forme de fonctions de séquence que la phase appelle. depends_on remplace l'ordre des étapes du profil, et timeout sur chaque phase remplace le timeout d'étape du profil.

Alarmes, procédures et calculated channels

Ces trois fonctions sont configurées dans la system definition et exécutées par le moteur, et elles servent deux objectifs différents que VeriStand ne distingue pas.

La protection. Une alarme sur le courant batterie qui déclenche une procédure coupant l'alimentation protège le matériel en un tick moteur. Cela reste dans la system definition, parce que Python sur l'hôte ne peut pas réagir à temps, et parce qu'une protection qui dépend d'un test en cours n'est pas une protection.

Les critères réussite/échec. Une alarme sur la température du liquide de refroidissement qui existe pour que l'opérateur voie un voyant rouge quand le test devrait échouer n'est pas une protection, c'est une limite. Elle devient un validateur sur le canal journalisé, où elle est versionnée avec le test, appliquée à l'identique sur chaque banc et enregistrée avec le résultat :

procedure.yaml
23 lines
main:  - name: Thermal Soak    python: phases.thermal_soak    timeout: 25m    measurements:      - name: Coolant Temperature        title: Coolant Temperature        x_axis:          legend: Time          unit: s        y_axis:          - legend: Temperature            key: temperature            unit: °C            aggregations:              - type: max                validators:                  - operator: "<="                    expected_value: 105              - type: mean                validators:                  - operator: "<="                    expected_value: 92

Les calculated channels suivent la même scission. Celui qui alimente un modèle ou une alarme reste. Celui qui n'existait que pour journaliser ou afficher une valeur dérivée, comme la puissance à partir de la tension et du courant, est une ligne de NumPy dans la phase qui lit le journal.

Journalisation : du TDMS aux mesures

VeriStand journalise en TDMS, soit en continu via l'outil Data Logging, soit par étape depuis un profil de stimulus. Ces fichiers sont l'enregistrement brut et les garder est la bonne décision. Ce qui change, c'est que le test extrait maintenant ce dont il a besoin du fichier, le valide, et attache l'original.

phases/thermal_soak.py
31 lines
import timefrom pathlib import Pathimport numpy as npfrom nptdms import TdmsFiledef thermal_soak(measurements, attach, log, rig):    # L'outil Data Logging de la system definition démarre un journal TDMS à 1 kHz    # tant que Aliases/LogTrigger est haut, dans le dossier donné en config du plug    rig.set("Aliases/HeaterEnable", 1)    rig.set("Aliases/LogTrigger", 1)    time.sleep(20 * 60)    rig.set("Aliases/LogTrigger", 0)    rig.set("Aliases/HeaterEnable", 0)    log_path = max(Path(rig.log_dir).glob("*.tdms"), key=lambda p: p.stat().st_mtime)    tdms = TdmsFile.read(log_path)    channel = tdms.groups()[0]["CoolantTemp"]  # les noms suivent la configuration du journal    temperature = channel[:]    time_s = channel.time_track()    # 1 kHz sur 20 min, c'est 1,2 M de points ; le graphique en a besoin d'environ mille    step = max(1, len(temperature) // 1000)    measurements.coolant_temperature.x_axis = time_s[::step].tolist()    measurements.coolant_temperature.y_axis.temperature = temperature[::step].tolist()    measurements.coolant_temperature.y_axis.temperature.aggregations.max = float(np.max(temperature))    measurements.coolant_temperature.y_axis.temperature.aggregations.mean = float(np.mean(temperature))    attach.file(log_path, "thermal_soak.tdms")    log.info(f"{len(temperature)} échantillons journalisés, max {np.max(temperature):.1f} °C")

Les agrégations sont calculées sur les données à pleine cadence, et le graphique est décimé. Cette distinction est ce qui rend le résultat fiable : un maximum calculé après décimation peut manquer un pic, et un graphique d'un million de points est illisible dans n'importe quel outil. Le TDMS brut reste attaché à l'exécution pour les cas où quelqu'un a besoin du pic lui-même.

npTDMS lit les fichiers TDMS sans aucun logiciel NI installé, donc la même phase tourne sur une machine d'analyse Linux contre des journaux archivés, ce qui est aussi la façon de valider les données historiques contre les nouvelles limites avant la bascule.

Workspace : operator UI et supervision

Un écran de workspace VeriStand fait deux choses. Il affiche les canaux en direct pendant que le banc tourne, et il collecte les saisies opérateur : quelle unité est sur le banc, si le fixture est fermé, s'il faut continuer après un avertissement.

La seconde fonction migre. L'identification de l'unité est déclarée dans le bloc unit: et demandée avant le début du test, et les confirmations deviennent des phases d'operator UI dans la procédure, donc versionnées et enregistrées comme tout le reste. Un bouton du workspace qui lançait un profil devient le démarrage de l'exécution.

La première fonction n'a pas besoin de migrer le premier jour. Un banc qui garde son moteur peut garder son workspace ouvert pour la supervision en direct, et l'operator UI tourne à côté. Une fois que l'exécution est diffusée vers le tableau de bord, la vue en direct y couvre l'essentiel de ce à quoi servait le workspace, et le workspace devient alors un outil de débogage plutôt que l'écran de l'opérateur.

Résultats : ne pas porter

Le chemin des résultats dans une installation VeriStand, c'est généralement un dossier TDMS sur un partage, un script DIAdem qui en fait un PDF, et parfois un insert SQL écrit à la main. C'est la partie sur laquelle les équipes ont le plus investi et celle qui ne devrait pas survivre.

Le moteur envoie l'exécution : procédure, unité, phases, mesures avec leurs validateurs, pièces jointes, verdict. Les exécutions sont mises en file hors ligne quand le banc perd le réseau et vidées à la reconnexion avec l'horodatage d'origine. Un changement de limite est une modification de procedure.yaml, déployée sur tous les bancs à la fois, plutôt qu'un script DIAdem édité à trois endroits.

Quand un format de rapport précis est un livrable contractuel, générez-le à partir des données stockées via l'API, une fois, hors du banc. Mettre le générateur de rapport dans le test signifie que chaque banc a besoin de DIAdem et qu'un bug de mise en forme fait échouer une unité conforme. Pour des années de journaux TDMS existants, Importer des données de test historiques via l'API couvre la reprise, et la phase npTDMS ci-dessus en est l'étape d'extraction.

Retirer le moteur

Cette section s'applique aux deuxième et troisième situations : le travail de banc qui n'a jamais eu besoin de déterminisme, et le model-in-the-loop sous Windows. Le plug du banc est remplacé par des plugs pour les instruments, et la correspondance est en gros une bibliothèque par appareil.

Élément VeriStandÉquivalent Python
Périphérique NI-DAQmx dans la system definitionnidaqmx
Port CAN ou LIN NI-XNETpython-can avec l'interface nixnet, cantools pour le DBC
DMM PXI, switch PXInidmm, niswitch
Alimentation ou charge SCPIPyVISA
Modèle Simulink compiléRéexporter en FMU, exécuter avec FMPy
FMUFMPy
Custom device écrit en LabVIEWRéécrire en plug, ou garder le moteur

La dernière ligne est la limite honnête. Un custom device est du code LabVIEW qui tourne dans le moteur, souvent pour un protocole ou un générateur de signal que NI ne livre pas. S'il fait du travail temps réel, c'est la raison de rester dans la première situation. S'il enveloppe un instrument, c'est un plug.

L'autre limite est le timing. Python sous Windows dort avec une gigue de l'ordre de la milliseconde, et une boucle logicielle au-dessus d'environ 100 Hz n'est pas fiable depuis une phase. Pour un test de banc qui lit un régime établi chaque seconde, c'est sans importance. Pour une boucle de régulation, c'est la raison d'être du moteur, et aucune quantité de Python ne le remplace.

Le model-in-the-loop migre proprement parce que FMPy exécute le même FMU que la licence VeriStand PC exécutait, dans le processus, et la phase pilote la co-simulation pas à pas :

plugs/plant.py
31 lines
from fmpy import extract, read_model_descriptionfrom fmpy.fmi2 import FMU2Slaveclass PlantModel:    def __init__(self, fmu_path: str, step_s: float = 0.001):        self._desc = read_model_description(fmu_path)        self._refs = {v.name: v.valueReference for v in self._desc.modelVariables}        self._fmu = FMU2Slave(            guid=self._desc.guid,            unzipDirectory=extract(fmu_path),            modelIdentifier=self._desc.coSimulation.modelIdentifier,        )        self._fmu.instantiate()        self._fmu.setupExperiment(startTime=0.0)        self._fmu.enterInitializationMode()        self._fmu.exitInitializationMode()        self._t = 0.0        self._dt = step_s    def step(self, inputs: dict[str, float]) -> None:        self._fmu.setReal([self._refs[k] for k in inputs], list(inputs.values()))        self._fmu.doStep(currentCommunicationPoint=self._t, communicationStepSize=self._dt)        self._t += self._dt    def get(self, name: str) -> float:        return self._fmu.getReal([self._refs[name]])[0]    def __del__(self):        self._fmu.terminate()        self._fmu.freeInstance()

Un FMU qui se comporte mal emporte le processus Python avec lui, parce que c'est le code C du fournisseur qui tourne dans le processus. Le moteur exécute chaque plug dans son propre processus, donc un modèle qui plante fait échouer la phase et est relancé au lieu d'arrêter le poste.

Un ordre de migration qui fonctionne

  1. Classez le système dans le tableau des couches, une ligne par artefact. La décision sur le moteur découle de la première colonne : si rien n'a besoin d'une boucle déterministe, prévoyez le départ du moteur.
  2. Écrivez le plug VeriStand et exécutez un profil de stimulus existant depuis une phase, avec le résultat envoyé. C'est une journée de travail et cela valide le banc, le gateway et le chemin d'envoi avant que quoi que ce soit soit porté.
  3. Portez les profils de stimulus un par un. Les parties déterministes deviennent des fonctions de séquence niveristand, le reste devient du code de phase. Exécutez l'ancien profil et la nouvelle phase sur la même unité et comparez les journaux TDMS avant de retirer le profil.
  4. Sortez les critères réussite/échec des alarmes et des scripts DIAdem vers des validateurs. Gardez les alarmes de protection dans la system definition et marquez-les comme telles.
  5. Remplacez l'interaction du workspace par le bloc unit: et des phases d'operator UI. Laissez le workspace ouvert pour la supervision jusqu'à ce que le tableau de bord la couvre.
  6. Supprimez le chemin des résultats. Accordez-vous avec le responsable des enregistrements qualité sur ce que signifie « les résultats sont dans le nouveau système » avant cette étape, parce que cette conversation est le chemin critique, pas le code.
  7. Re-licenciez. Une licence Operator par banc qui garde son moteur, un siège Full pour ouvrir l'archive, et rien sur les bancs qui ont perdu le moteur.

L'étape 3 est celle qui prend le temps, et la comparaison qu'elle contient n'est pas optionnelle. Une phase côté hôte qui a remplacé une séquence déterministe produira des chiffres plausibles avec un timing légèrement différent, et la comparaison TDMS est la seule chose qui l'attrape.

Points clés

  • VeriStand est un moteur temps réel plus tout ce qui l'entoure. Python remplace le second ; le premier reste pour du vrai HIL et disparaît pour le travail de banc qui n'en a jamais eu besoin.
  • Le moteur est atteint par un seul plug de portée station, donc la system definition se déploie une fois et chaque unité après la première démarre immédiatement.
  • Les séquences avec des exigences temporelles sous environ 100 ms restent déterministes via niveristand. Tout le reste devient une phase.
  • Les alarmes de protection restent dans la system definition. Les alarmes qui étaient des critères réussite/échec deviennent des validateurs.
  • Le TDMS reste l'enregistrement brut. Les agrégations sont calculées à pleine cadence, les graphiques sont décimés, et le fichier est attaché à l'exécution.
  • La plomberie des résultats, les rapports DIAdem et les inserts SQL ne sont pas portés. Le passage à des licences Operator par banc est le résultat budgétaire.

Plus de guides

Mettez ce guide en pratique