Wednesday, December 11, 2013

Controlling Oscilloscope with Linux Computer

I know it's not something you would need on an everyday basis (rather lifetime basis), but wouldn't it be cool to be able to control your instruments using a script? Or let's say you want to enjoy the comfort of your desktop chair, while remotely controlling and getting data from your instrument in the workshop? With this mindset I started looking for ways to control my oscilloscope with my small eee pc with linux on it (CrunchBang 11, to be exact).


For a windows user you would install whatever software the oscilloscope manufacturer provides, which would place a "visa32.dll" file in your windows directory. This file would make it possible to communicate to the oscilloscope with the VISA API, and pyVISA could be used to control the scope with the programming language Python. For linux, we cannot rely on the luxury of the manufacturer supplying drivers (one can only have faith for the future). At first I tried to find a visa32.dll equivalent, but that didn't lead anywhere. It turns out that a full-worthy solution DOES exist, thanks to someone called Alex Forencich. He has made a pure Python driver based on USBTMC, and a IVI like wrapper on top of that (also in Python).

Before starting to install his work, PyUSB is needed:
sudo apt-get install python-usb

Unfortunately, for some reason, PyUSB is not installed that easily (see link). You also have to run
sudo apt-get install python-pip
sudo pip install --upgrade pyusb 

Now we're ready to install Python USBTMC (look for "Download ZIP"). Navigate to the folder where you downloaded the zip file and run:
unzip python-usbtmc-master.zip
cd python-usbtmc-master
sudo python setup.py install

Python USBTMC is now installed. Clean up by removing folder and zip file:
cd ..
sudo rm -r python-usbtmc-master python-usbtmc-master.zip 

Try it out:
ulf@eee:~$ python
Python 2.7.3 (default, Jan  2 2013, 16:53:07)
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import usbtmc
>>>

All seems fine, but let's not get our hopes up too soon. At this point we need to figure out what the vendor and product ID of the instrument is:
ulf@eee:~$ lsusb
...
Bus 001 Device 004: ID 0957:179a Agilent Technologies, Inc. 

The first ID is the vendor (0957), and the one after the semicolon, the product ID (179a). Note that you need the "0x" in front of the number to indicate that it's hexadecimal (hate to admit it, but this caused me quite some hair loss). Alternatively you can convert it to decimal and use that directly.
ulf@eee:~$ python
Python 2.7.3 (default, Jan  2 2013, 16:53:07) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import usbtmc
>>> instr = usbtmc.Instrument(0x0957, 0x179a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/dist-packages/usbtmc/usbtmc.py", line 165, in __init__
    if self.device.is_kernel_driver_active(0)
  File "/usr/local/lib/python2.7/dist-packages/usb/core.py", line 719, in is_kernel_driver_active
    self._ctx.managed_open()
  File "/usr/local/lib/python2.7/dist-packages/usb/core.py", line 70, in managed_open
    self.handle = self.backend.open_device(self.dev)
  File "/usr/local/lib/python2.7/dist-packages/usb/backend/libusb1.py", line 733, in open_device
    return _DeviceHandle(dev)
  File "/usr/local/lib/python2.7/dist-packages/usb/backend/libusb1.py", line 618, in __init__
    _check(_lib.libusb_open(self.devid, byref(self.handle)))
  File "/usr/local/lib/python2.7/dist-packages/usb/backend/libusb1.py", line 571, in _check
    raise USBError(_str_error[ret], ret, _libusb_errno[ret])
usb.core.USBError: [Errno 13] Access denied (insufficient permissions)
>>> 

Darn... What to do now? Luckily Alex knew about this and had some pointers in his readme file. As indicated by the error message, we're dealing with a permission problem. The instrument has to be added to the usbtmc group. Create a file called usbtmc.rules in the correct folder:
sudo nano /etc/udev/rules.d/usbtmc.rules

Add the following lines to it:
# USBTMC instruments

# Agilent DSO-X 2004A
SUBSYSTEMS=="usb", ACTION=="add", ATTRS{idVendor}=="0957", ATTRS{idProduct}=="179a", GROUP="usbtmc", MODE="0660" 

Create group:
sudo groupadd usbtmc

Add your user to the group (change "ulf" to your own user name):
sudo usermod -a -G usbtmc ulf

Restart the computer and try again:
ulf@eee:~$ python
Python 2.7.3 (default, Jan  2 2013, 16:53:07) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import usbtmc
>>> instr =  usbtmc.Instrument(0x0957, 0x179a)
>>> print(instr.ask("*IDN?"))
AGILENT TECHNOLOGIES,DSO-X 2004A,MY********,02.35.2013061800
>>> 

It works! In fact, you can stop here and be satisfied if you want. At this point you can fully control your instrument. For example, if I want to stop my scope and then invert channel 1, I will write:
instr.write(":STOP")
instr.write(":CHAN1:INV ON")

It's, however, not a very convenient way to control your instrument. The IVI foundation has standardized how to use the C, COM and .NET programming languages to simplify the control (as described here), but does not say anything about Python. Therefore Alex has written his own Python interpretation of the standard, called "Python IVI". There are drivers for quite a few instruments, and luckily my oscilloscope is one of them.

To install, as before, download the zip file, extract, install and clean up:
unzip python-ivi-master.zip
cd python-ivi-master
sudo python setup.py install
cd ..
sudo rm -r python-ivi-master python-ivi-master.zip

Now, try it out (replace ******** with the serial number of your instrument):
ulf@eee:~$ python
Python 2.7.3 (default, Jan  2 2013, 16:53:07)
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import ivi
>>> mso = ivi.agilent.agilentDSOX2004A("USB0::0x0957::0x179a::MY********::INSTR")

If there are no error messages, you're in control! Now, if you want to for example retrieve all data points from on measurement on channel 1, write:
waveform = mso.channels[0].measurement.fetch_waveform()

Or if you simply want to enable channel 4:
mso.channels['channel4'].enabled = True

Coolness achieved.