Using and creating drivers for instruments¶
In this section
Drivers are the original impetus for sharing this project. Writing drivers can be fun (the first few times). It exercises the full range of electrical engineering knowledge. It can be a snap, or it can take multiple PhD students several days to realize which cable needed a jiggle. The reward is automated, remote lab control!
The module page lab_instruments
contains all the instruments necessary available in lightlab. If your equipment is available (e.g. a very common Keithley_2400_SM
), then you can use it directly with:
from lightlab.equipment.lab_instruments import Keithley_2400_SM
k = Keithley_2400_SM(name="My Keithley", address="GPIB0::23::INSTR")
if k.isLive():
print("Connection is good")
help(k) # should display all commands available to be used.
The address format for the Instrument is either a VISA-compatible resource name (parsed by pyvisa). In this example, the Keithley instrument is configured to have the address 23, and it is plugged directly to the host. Alternatively, it can be connected to a computer with an instance of the NI Visa Server, in which case the address would be visa://alice.school.edu/GPIB0::23::INSTR
, where alice.school.edu
is the hostname of the computer hosting the Visa Server.
Alternatively, it can be written as prologix://prologix_ip_address/gpib_primary_address[:gpib_secondary_address]
, e.g. prologix://alice.school.edu/23
, for use with the Prologix GPIB-Ethernet controller.
The instrument abstraction¶
In lightlab
, there are two layers of abstraction for instrumentation
Instrument
, such asOscilloscope
Keithley
VISAInstrumentDriver
, such as
An Instrument
refers to a category of instruments that do certain things. A VISAInstrumentDriver
describes how a particular piece of equipment does it. As a rule of thumb, there is a different driver for each model of instrument.
All oscilloscopes have some form of acquiring a waveform, and user code makes use of that abstraction. If you have a scope other than a TEKTRONIX DPO4032, you are on your own with the driver. BUT, if you can make your low-level driver for that scope to meet the abstraction of Oscilloscope
, then your scope will be equivalent to my scope, in some sense. That means all of the rest of the package becomes usable with that scope.
The critical part of an Instrument child class are its essentialMethods
and essentialProperties
. Initialization and book keeping are all done by the super class, and implementation is done by the driver. The driver must implement all of the essential methods and properties, and then the Instrument
will take on these data members as its own.
As in the case of Tektronix_DPO4032_Oscope and Tektronix_DPO4034_Oscope, there is substantial overlap in implementation. We can save a lot of work by abstracting some of the common behavior, which leads to the third major concept of abstract drivers, found in the module:
abstract_drivers
, which includesDPO_Oscope
MultiModalSource
Before writing a fresh driver, check out the abstract ones to see if you can partially use existing functionality (e.g. if you are making one for a DPO4038).
Writing a VISAInstrumentDriver
¶
For new developers, you will likely have instruments not yet contained in lightlab
. We encourage you to write them, test them, and then create a pull request so that others won’t have to re-invent the wheel.
Basics¶
A communication session with a message-based resource has the following commands
- open
- close
- write
- read
- query (a combination of write, then read)
The PyVISA package provides the low level communication. Drivers can be GPIB, USB, serial, or TCP/IP – the main difference is in the address. PyVISA also has a resource manager for initially finding the instrument. lightlab
has a wrapper for this that works with multiple remote Hosts. See Making and changing the lab state for putting a Host in the labstate.
Plug your new instrument (let’s say GPIB, address 23) into host “alice”, then, in an ipython session
> from lightlab.laboratory.state import lab
> for resource in lab.hosts['alice'].list_resources_info():
... print(resource)
visa://alice.school.edu/USB0::0x0699::0x0401::B010238::INSTR
visa://alice.school.edu/TCPIP0::128.112.48.124::inst0::INSTR
visa://alice.school.edu/ASRL1::INSTR
visa://alice.school.edu/ASRL3::INSTR
visa://alice.school.edu/ASRL10::INSTR
visa://alice.school.edu/GPIB0::18::INSTR
visa://alice.school.edu/GPIB0::23::INSTR
That means the instrument is visible, and we know the full address:
> from lightlab.equipment.lab_instruments.visa_connection import VISAObject
> newInst = VISAObject('visa://alice.school.edu/GPIB0::23::INSTR')
> print(newInst.instrID())
KEITHLEY INSTRUMENTS INC.,MODEL 2400, ...
That means the instrument is responsive, and basic communication settings are correct already. Time to start writing.
Troubleshooting 1: Write termination¶
Try this:
> newInst.open()
> newInst.mbSession.write_termination = ''
> newInst.mbSession.clear()
> print(newInst.instrID())
and play around with different line terminations. There are also different options for handshaking to be aware of, as well as baud rate attributes. For debugging at this level, we recommend the NI visaic.
When you find something that works, overload the open
method. Do not try to set these things in the __init__
method.
Troubleshooting 2: No “*IDN?” behavior¶
Some instruments don’t even though it is a nearly universal requirement. In that case, find some simple command in the manual to serve as your “is this instrument alive?” command. Later, overload the instrID
method.
Configurable¶
Many instruments have complex settings and configurations. These are usually accessed in a message-based way with write(':A:PARAM 10')
and query(':A:PARAM?')
. We want to create a consistency between driver and hardware, but
- we don’t care about the entire configuration all the time, and
- it doesn’t make sense to send configuration commands all the time.
Configurable
builds up a minimal notion of consistent state and updates hardware only when it might have become inconsistent. The above is done with setConfigParam('A:PARAM', 10)
and getConfigParam('A:PARAM')
. If you set the parameter and then get it, the driver will not communicate with the instrument – it will look up the value you just set. Similarly, it will avoid setting the same value twice. For example,:
# Very slow
def acquire(self, chan):
self.write(':CH ' + str(chan))
return self.query(':GIVE:DATA?')
# Error-prone
def changeChannel(self, chan):
self.write(':CH ' + str(chan))
def acquire(self):
return self.query(':GIVE:DATA?')
# Good (using Configurable)
def acquire(self, chan):
self.setConfigParam('CH', chan)
return self.query(':GIVE:DATA?')
Both support a forceHardware
kwarg and have various options for message formatting.
Configurable
also has support for saving, loading, and replaying configurations, so you can put the instrument in the exact same state as it was for a given experiment. Save files are human-readable in JSON.
Difference between __init__
, startup
, and open
¶
__init__
- should set object attributes based on the arguments. The
super().__init__
will take care of lab book keeping. It should not callopen
. open
- initiates a message based session. It gets called automatically when
write
orquery
are called. startup
(optional)- is called immediately after the first time the instrument is opened.
How to read a programmer manual¶
You need the manual to find the right commands. You are looking for a command reference, or sometimes coding examples. They are often very long and describe everything from scratch. They sometimes refer to programming with vendor-supplied GUI software – don’t want that. Here is a very old school manual for a power meter. It is 113 pages, and you need to find three commands. Go to the contents and look for something like “command summary.”
which turns into the following driver (complete, simplified). If possible, link the manual in the docstring.
class HP8152(VISAInstrumentDriver):
''' The HP 8152 power meter
`Manual <http://www.lightwavestore.com/product_datasheet/OTI-OPM-L-030C_pdf4.pdf>`_
'''
def startup(self):
self.write('T1')
def powerDbm(self, channel=1):
'''
Args:
channel (int): 1 (A), 2 (B), or 3 (A/B)
'''
self.write('CH' + str(channel))
returnString = self.query('TRG')
return float(returnString)
Newer equipment usually has thousand-page manuals, but they’re hyperlinked.