PyVISA vous permet de contrôler des instruments de test (multimètres, oscilloscopes, alimentations, générateurs de signaux) depuis Python en utilisant le même protocole VISA que LabVIEW et TestStand. Si votre instrument dispose d'une interface USB, GPIB, Ethernet ou série, vous pouvez communiquer avec lui via PyVISA. Ce guide couvre l'installation, la connexion, les commandes SCPI et l'intégration des mesures d'instruments dans un test TofuPilot.
Prérequis
- Python 3.8+
- Un instrument de test avec interface USB-TMC, GPIB, Ethernet (LXI) ou série
- Optionnel : runtime NI-VISA (pour le GPIB et certains instruments USB)
Installation
pip install pyvisa pyvisa-pyDeux backends sont disponibles :
| Backend | Installation | Quand l'utiliser |
|---|---|---|
| pyvisa-py | pip install pyvisa-py | Python pur. Fonctionne sous Linux/macOS/Windows. Supporte USB-TMC, Ethernet (TCP/IP), série. Pas besoin de logiciel NI. |
| NI-VISA | Installer depuis ni.com | Nécessaire pour le GPIB. Supporte également USB-TMC et Ethernet. Windows et Linux uniquement. |
Commencez avec pyvisa-py. Passez à NI-VISA uniquement si vous avez besoin du GPIB ou rencontrez des problèmes de compatibilité.
Trouver vos instruments
Chaque instrument VISA possède une chaîne de ressource qui l'identifie. PyVISA peut découvrir automatiquement les instruments connectés.
import pyvisa
rm = pyvisa.ResourceManager("@py") # Utiliser le backend pyvisa-py
# rm = pyvisa.ResourceManager() # Utiliser le backend NI-VISA
# Lister tous les instruments connectés
resources = rm.list_resources()
print(f"{len(resources)} instrument(s) trouvé(s) :")
for r in resources:
print(f" {r}")Formats courants de chaînes de ressource :
| Interface | Chaîne de ressource | Exemple |
|---|---|---|
| USB-TMC | USB0::VID::PID::SERIAL::INSTR | USB0::0x2A8D::0x1301::MY59001234::INSTR |
| Ethernet (LXI) | TCPIP::IP::INSTR | TCPIP::192.168.1.100::INSTR |
| GPIB | GPIB0::ADDR::INSTR | GPIB0::22::INSTR |
| Série | ASRL/dev/ttyUSB0::INSTR | ASRL/dev/ttyUSB0::INSTR |
Se connecter à un instrument
Une fois la chaîne de ressource obtenue, ouvrez une connexion et vérifiez l'identité avec la requête standard *IDN?.
import pyvisa
rm = pyvisa.ResourceManager("@py")
dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")
# Définir le timeout (en millisecondes)
dmm.timeout = 5000
# Requête d'identité de l'instrument
idn = dmm.query("*IDN?")
print(f"Connecté à : {idn.strip()}")
# Sortie : Keysight Technologies,34461A,MY59001234,A.03.01La méthode query() envoie une commande et lit la réponse. Pour les commandes qui ne retournent pas de données, utilisez write().
Bases des commandes SCPI
SCPI (Standard Commands for Programmable Instruments) est le langage que la plupart des instruments modernes utilisent. Les commandes suivent une structure arborescente.
Lire une tension DC
import pyvisa
rm = pyvisa.ResourceManager("@py")
dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")
dmm.timeout = 5000
# Configurer pour la mesure de tension DC, auto-range
dmm.write(":CONF:VOLT:DC AUTO")
# Déclencher une mesure et lire le résultat
dmm.write(":INIT")
voltage = float(dmm.query(":FETCH?"))
print(f"Tension : {voltage:.4f} V")
dmm.close()
rm.close()Commandes SCPI courantes
| Commande | Fonction | Exemple |
|---|---|---|
*IDN? | Identifier l'instrument | Retourne fabricant, modèle, série, firmware |
*RST | Réinitialiser aux paramètres d'usine | Bonne pratique au début du test |
*CLS | Effacer la file d'erreurs | Effacer les erreurs précédentes |
*OPC? | Requête de fin d'opération | Retourne « 1 » quand la dernière commande est terminée |
:CONF:VOLT:DC | Configurer la tension DC | :CONF:VOLT:DC 10 pour le calibre 10 V |
:CONF:CURR:DC | Configurer le courant DC | :CONF:CURR:DC AUTO pour l'auto-range |
:CONF:RES | Configurer la résistance | :CONF:RES AUTO |
:MEAS:VOLT:DC? | Mesurer la tension DC (configurer + déclencher + lire) | Retourne un flottant |
:INIT | Déclencher une mesure | Utiliser avec :FETCH? pour séparer déclenchement/lecture |
:FETCH? | Lire la dernière mesure | Retourne un flottant |
:SYST:ERR? | Lire la file d'erreurs | Retourne le code d'erreur et le message |
Le raccourci :MEAS: configure, déclenche et lit en une seule commande. Utilisez :CONF: + :INIT + :FETCH? quand vous avez besoin de plus de contrôle sur le timing.
Contrôler une alimentation
Les alimentations utilisent des commandes SCPI similaires mais ajoutent le contrôle de sortie.
import pyvisa
import time
rm = pyvisa.ResourceManager("@py")
psu = rm.open_resource("TCPIP::192.168.1.101::INSTR")
psu.timeout = 5000
# Réinitialiser à un état connu
psu.write("*RST")
psu.write("*CLS")
# Configurer le canal 1 : 5 V, limite de courant 500 mA
psu.write(":INST:SEL CH1")
psu.write(":VOLT 5.0")
psu.write(":CURR 0.5")
# Activer la sortie
psu.write(":OUTP ON")
time.sleep(0.5) # Attendre la stabilisation de la sortie
# Lire la tension et le courant réels
actual_voltage = float(psu.query(":MEAS:VOLT?"))
actual_current = float(psu.query(":MEAS:CURR?"))
print(f"Sortie : {actual_voltage:.3f} V, {actual_current:.4f} A")
# Désactiver la sortie
psu.write(":OUTP OFF")
psu.close()
rm.close()Gestion des erreurs
Les instruments communiquent les erreurs via la file d'erreurs SCPI. Vérifiez toujours les erreurs après une séquence de commandes.
import pyvisa
def check_instrument_errors(instr):
"""Lit et signale toutes les erreurs de la file d'erreurs de l'instrument."""
errors = []
while True:
err = instr.query(":SYST:ERR?").strip()
code = int(err.split(",")[0])
if code == 0:
break
errors.append(err)
return errors
rm = pyvisa.ResourceManager("@py")
dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")
dmm.timeout = 5000
# Envoyer des commandes
dmm.write("*RST")
dmm.write(":CONF:VOLT:DC AUTO")
dmm.write(":INIT")
voltage = float(dmm.query(":FETCH?"))
# Vérifier les erreurs
errors = check_instrument_errors(dmm)
if errors:
print(f"Erreurs instrument : {errors}")
else:
print(f"Tension : {voltage:.4f} V (aucune erreur)")
dmm.close()Intégration avec OpenHTF et TofuPilot
Dans un test de production, le contrôle des instruments se fait dans un plug OpenHTF. Le plug gère le cycle de vie de la connexion, et les mesures transitent automatiquement via OpenHTF vers TofuPilot.
import pyvisa
import openhtf as htf
from openhtf.plugs import BasePlug
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
class MultimeterPlug(BasePlug):
"""Plug PyVISA pour multimètre avec gestion automatique du cycle de vie."""
RESOURCE = "TCPIP::192.168.1.100::INSTR"
def setUp(self):
rm = pyvisa.ResourceManager("@py")
self.instr = rm.open_resource(self.RESOURCE)
self.instr.timeout = 5000
self.instr.write("*RST")
self.instr.write("*CLS")
self.logger.info(f"Connecté : {self.instr.query('*IDN?').strip()}")
def measure_dc_voltage(self, range_v: str = "AUTO") -> float:
"""Effectue une mesure unique de tension DC."""
self.instr.write(f":CONF:VOLT:DC {range_v}")
return float(self.instr.query(":MEAS:VOLT:DC?"))
def measure_dc_current(self, range_a: str = "AUTO") -> float:
"""Effectue une mesure unique de courant DC."""
self.instr.write(f":CONF:CURR:DC {range_a}")
return float(self.instr.query(":MEAS:CURR:DC?"))
def measure_resistance(self, range_ohm: str = "AUTO") -> float:
"""Effectue une mesure unique de résistance."""
self.instr.write(f":CONF:RES {range_ohm}")
return float(self.instr.query(":MEAS:RES?"))
def tearDown(self):
self.instr.close()
self.logger.info("Multimètre déconnecté")
@htf.measures(
htf.Measurement("supply_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT)
.doc("Tension du rail 3,3 V"),
htf.Measurement("supply_5v0")
.in_range(4.8, 5.2)
.with_units(units.VOLT)
.doc("Tension du rail 5,0 V"),
htf.Measurement("idle_current")
.in_range(0.01, 0.15)
.with_units(units.AMPERE)
.doc("Courant de repos de la carte"),
)
@htf.plug(dmm=MultimeterPlug)
def test_power_rails(test, dmm):
"""Mesure tous les rails d'alimentation et le courant de repos."""
test.measurements.supply_3v3 = dmm.measure_dc_voltage()
test.measurements.supply_5v0 = dmm.measure_dc_voltage()
test.measurements.idle_current = dmm.measure_dc_current()
@htf.measures(
htf.Measurement("pullup_resistance")
.in_range(4500, 5500)
.with_units(units.OHM)
.doc("Résistance de pull-up I2C (attendu 4,7k)"),
)
@htf.plug(dmm=MultimeterPlug)
def test_pullup_resistors(test, dmm):
"""Vérifie les valeurs des résistances de pull-up I2C."""
test.measurements.pullup_resistance = dmm.measure_resistance()
def main():
test = htf.Test(
test_power_rails,
test_pullup_resistors,
procedure_id="FCT-001",
part_number="PCBA-100",
)
with TofuPilot(test):
test.execute(test_start=lambda: input("Scannez le numéro de série : "))
if __name__ == "__main__":
main()Chaque mesure (tension, courant, résistance) est transmise à TofuPilot avec son nom, sa valeur, ses limites et ses unités. Vous obtenez le suivi FPY, les cartes de contrôle et l'analyse Cpk pour chaque mesure sans code supplémentaire.
Dépannage
| Problème | Cause | Solution |
|---|---|---|
VisaIOError: VI_ERROR_RSRC_NFOUND | Instrument non trouvé | Vérifier le câble, l'IP/USB, exécuter rm.list_resources() |
VisaIOError: VI_ERROR_TMO | Timeout | Augmenter instr.timeout, vérifier si l'instrument est occupé |
VisaIOError: VI_ERROR_CONN_LOST | Connexion perdue | Vérifier le réseau, retenter la connexion |
Réponse vide de query() | L'instrument n'a pas répondu | Ajouter time.sleep() après write, vérifier la syntaxe de la commande |
| Instrument USB non détecté | Pilote manquant | Installer le runtime NI-VISA ou vérifier le module kernel USB-TMC (Linux) |
ValueError: could not convert string to float | Format de réponse inattendu | Afficher la réponse brute, vérifier la présence de messages d'erreur mélangés |