VISA Driver Example¶
In this guide, we’ll run through the process of making a VISA-based driver using the example of a Thorlabs PM100D power meter.
If you’re following along, it may be helpful to look at the PM100D’s commands, listed in the “SCPI Commands” section of its manual [PDF].
First, let’s open the device and play around with it in an ipython shell using pyvisa:
In [1]: import visa
In [2]: rm = visa.ResourceManager()
In [4]: rm.list_resources()
Out[4]:
(u'USB0::0x1313::0x8078::P0009084::INSTR',
u'USB0::0x0699::0x0362::C101689::INSTR',
u'ASRL1::INSTR',
u'ASRL3::INSTR')
Which resource is our power meter? Well, like all well-behaved SCPI instruments, the PM100D supports the *IDN?
command, which asks the device to identify itself. Let’s query the IDN of each of these resources:
In [5]: for addr in rm.list_resources():
...: try:
...: print(addr, '-->', rm.open_resource(addr).query('*IDN?').strip())
...: except visa.VisaIOError:
...: pass
...:
USB0::0x1313::0x8078::P0009084::INSTR --> Thorlabs,PM100D,P0009084,2.4.0
USB0::0x0699::0x0362::C101689::INSTR --> TEKTRONIX,TDS 1001B,C101689,CF:91.1CT FV:v22.11
We’ve used a try-except block here to catch errors from any devices that don’t support the *IDN?
command. We can now see which device is our power meter. Let’s open it and try some of the commands listed in the manual:
In [5]: rsrc = rm.open_resource('USB0::0x1313::0x8078::P0009084::INSTR',
read_termination='\n')
In [6]: rsrc.query('measure:power?')
Out[6]: '4.20021615E-06'
In [7]: rsrc.query('power:dc:unit?')
Out[7]: 'W'
In [8]: rsrc.query('sense:corr:wav?')
Out[8]: '8.520000E+02'
Here we’ve set the resource’s read termination to automatically strip off the newline at the end of each message, to make the output clearer. We can see that our power meter is measuring 4.2 microwatts of optical power and its operation wavelength is set to 852 nm. Let’s change the wavelength:
In [9]: rsrc.write('sense:corr:wav 532')
Out[9]: (20, <StatusCode.success: 0>)
In [10]: rsrc.query('sense:corr:wav?')
Out[10]: '5.320000E+02'
Now let’s get start writing our driver:
from instrumental.drivers.powermeters import PowerMeter
from instrumental.drivers import VisaMixin
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
_INST_PARAMS_ = ['visa_address']
We inherit from PowerMeter
, a subclass of Instrument
, and use the _INST_PARAMS_
class attribute to declare what parameters our instrument needs. We also inherit from VisaMixin
, a mixin class which provides us some useful VISA-related features:
In [1]: from mydriver import *
In [2]: pm = PM100D(visa_address='USB0::0x1313::0x8078::P0009084::INSTR')
In [3]: pm.resource
Out[3]: <'USBInstrument'(u'USB0::0x1313::0x8078::P0009084::INSTR')>
In [4]: pm.query('*IDN?')
Out[4]: 'Thorlabs,PM100D,P0009084,2.4.0\n'
VisaMixin
allows us to construct an instance by providing only a visa_address
parameter, and provides our class with a resource
attribute as well as query
and write
convenience methods. Notice that the message termination isn’t being stripped. We can enable this by setting read_termination
inside the _initialize
method, which is called just after the instance is created:
from instrumental.drivers.powermeters import PowerMeter
from instrumental.drivers import VisaMixin
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
def _initialize(self):
self.resource.read_termination = '\n'
Now let’s add a method to return the measured optical power:
from instrumental import Q_
[...]
class PM100D(PowerMeter, VisaMixin):
def power(self):
"""The measured optical power"""
self.write('power:dc:unit W')
power_W = float(self.query('measure:power?'))
return Q_(power_W, 'W')
This will sets the measurement units to watts, queries the power, and returns it as a unitful Quantity
. Let’s try it out:
In [3]: pm.power()
Out[3]: <Quantity(1.03476232e-05, 'watt')>
Now let’s add a way to get and set the wavelength, but let’s use the SCPI_Facet
convenience function, which allows us to concisely wrap well-behaving SCPI commands:
from instrumental.drivers import VisaMixin, SCPI_Facet
[...]
class PM100D(PowerMeter, VisaMixin):
[...]
wavelength = SCPI_Facet('sense:corr:wav', units='nm', type=float,
doc="Input signal wavelength")
wavelength
here is a Facet
, which is like a suped-up python property
. We’ve told SCPI_Facet
the command to use, and noted that it refers to a float with units of nanometers. Now we let’s see how our new wavelength attribute behaves:
In [4]: pm.wavelength
Out[4]: <Quantity(532.0, 'nanometer')>
In [5]: pm.wavelength = 1064
[...]
DimensionalityError: Cannot convert from 'dimensionless' (dimensionless) to 'nanometer' ([length])
What happened? The Facet ensures that we set the wavelength in units of length, to keep us from making unit conversion errors. We can use either a Quantity or a string that can be parsed by Q_()
In [6]: pm.wavelength = Q_(1064, 'nm')
In [7]: pm.wavelength
Out[7]: <Quantity(1064.0, 'nanometer')>
In [8]: pm.wavelength = Q_(0.633, 'um')
In [9]: pm.wavelength
Out[9]: <Quantity(633.0, 'nanometer')>
In [10]: pm.wavelength = '852 nm'
In [11]: pm.wavelength
Out[11]: <Quantity(852.0, 'nanometer')>
That’s better. Now that we have a basic driver, we need to make sure everything is cleaned up when we close our instrument. For most VISA-based instruments, this isn’t necessary, but the PM100D enters a special REMOTE mode, which locks out the front panel, when you start sending it commands. We use the control_ren()
method of pyvisa.resources.USBInstrument
to disable remote mode:
[...]
class PM100D(PowerMeter, VisaMixin):
[...]
def close(self):
self.resource.control_ren(False) # Disable remote mode
The close()
method can be called explicitly, and it is automatically called when the instrument is cleaned up or the interpreter exits. This way, the power meter will exit remote mode even if our program exits due to an exception.
@Facet(units='W', cached=False)
def power(self):
"""The measured optical power"""
self.write('power:dc:unit W')
return float(self.query('measure:power?'))
from instrumental.drivers.powermeters import PowerMeter
from instrumental.drivers import Facet, SCPI_Facet, VisaMixin, deprecated
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
range = SCPI_Facet('power:dc:range', units='W', convert=float, readonly=True,
doc="The current input range's max power")
auto_range = SCPI_Facet('power:dc:range:auto', convert=int, value={False:0, True:1},
doc="Whether auto-ranging is enabled")
wavelength = SCPI_Facet('sense:corr:wav', units='nm', type=float,
doc="Input signal wavelength")
num_averaged = SCPI_Facet('sense:average:count', type=int,
doc="Number of samples to average")
def close(self):
self._rsrc.control_ren(False) # Disable remote mode