Control a SIGLENT oscilloscope with Python
I'm an happy owner of a SIGLENT SDS1102CML, a entry level digital oscilloscope with which I experiment when I do electronics.
I discovered that is possible to access its functionality using its USB
port,
indeed if you connect this oscilloscope to a Linux system you can see that it recognizes
it
$ lsusb
...
Bus 003 Device 030: ID f4ec:ee3a Atten Electronics / Siglent Technologies
...
Reading the Programming guide (or this one) the interested hacker can found that using VISA i.e. Virtual instrument software architecture it's possible to command this device.
VISA
it's a high-level API used to communicate with instrumentation buses and it's possible
to use with the python language by pyvisa.
VISA Installation
To install it you can simply use pip
$ pip install pyusb pyvisa pyvisa-py
and you can check the installation using
$ python -m visa info
Machine Details:
Platform ID: Linux-4.9.0-3-amd64-x86_64-with-debian-kebab
Processor:
Python:
Implementation: CPython
Executable: .virtualenv/bin/python
Version: 2.7.14+
Compiler: GCC 7.2.0
Bits: 64bit
Build: Dec 5 2017 15:17:02 (#default)
Unicode: UCS4
PyVISA Version: 1.8
Backends:
ni:
Version: 1.8 (bundled with PyVISA)
Binary library: Not found
py:
Version: 0.2
ASRL INSTR:
Please install PySerial to use this resource type.
No module named serial
TCPIP INSTR: Available
USB RAW: Available via PyUSB (1.0.2). Backend: libusb1
USB INSTR: Available via PyUSB (1.0.2). Backend: libusb1
GPIB INSTR:
Please install linux-gpib to use this resource type.
No module named gpib
TCPIP SOCKET: Available
The important thing to check is that you have at least a backend available,
in this case the one named py
was installed with the package pyvisa-py
.
UDEV rules
First of all you need to configure your system to recognize the USB
device and make it
accessible for a normal user; for a system with udev
you can use the following rule
# SIGLENT SDS1102CML
SUBSYSTEMS=="usb", ACTION=="add", ATTRS{idVendor}=="f4ec", ATTRS{idProduct}=="ee3a", GROUP="plugdev", MODE="0660"
saved into /etc/udev/rules.d/
with a file named like 70-siglent.rules
.
You need to tell udev
to reload its rules via sudo udevadm control --reload
.
Obviously your user must be in the right group (in this case plugdev
).
Programming
At this point we can show the first lines necessary to connect to the oscilloscope:
import visa
resources = visa.ResourceManager('@py')
probe = resources.open_resource("USB0::62700::60986::SDS10PA1164640::0::INSTR")
print probe.query("*IDN?")
The ResourceManager
takes as parameter the backend name, if not indicated
the default one is used (i.e. ni
); in our case we have the python backend and
so we need to indicate explicitely.
Once opened the resource using the right identifier you can query the device with commands described into the programming guided linked at the start of the post.
The thing to note when try to write a script is that the query()
call is simply
a wrapper around write()
and read()
calls, and in case of binary data
exception related to encoding can happens. In these cases the read_raw()
method
should be called.
The tricky part is that the responses are half ASCII and half binary data and parsing them could be not foolproof.
Source code
Below you can read an utility script with a few commands implemented
# encoding: utf-8
# See document "Programming Guide" at <https://www.siglentamerica.com/wp-content/uploads/dlm_uploads/2017/10/ProgrammingGuide_forSDS-1-1.pdf>
import sys
import logging
import argparse
import wave # https://docs.python.org/2/library/wave.html
import visa # https://pyvisa.readthedocs.io
logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
def usage(progname):
print 'usage: %s [list|dump]' % progname
sys.exit(1)
def list(r):
results = r.list_resources()
for idx, result in enumerate(results):
print '[%03d] %s' % (idx, result)
def waveform(device, outfile, channel):
sample_rate = device.query('SANU C%d?' % channel)
sample_rate = int(sample_rate[len('SANU '):-2])
logger.info('detected sample rate of %d' % sample_rate)
#desc = device.write('C%d: WF? DESC' % channel)
#logger.info(repr(device.read_raw()))
# the response to this is binary data so we need to write() and then read_raw()
# to avoid encode() call and relative UnicodeError
logger.info(device.write('C%d: WF? DAT2' % (channel,)))
response = device.read_raw()
if not response.startswith('C%d:WF ALL' % channel):
raise ValueError('error: bad waveform detected -> \'%s\'' % repr(response[:80]))
index = response.index('#9')
index_start_data = index + 2 + 9
data_size = int(response[index + 2:index_start_data])
# the reponse terminates with the sequence '\n\n\x00' so
# is a bit longer that the header + data
data = response[index_start_data:index_start_data + data_size]
logger.info('data size: %d' % data_size)
fd = wave.open(outfile, "w")
fd.setparams((
1, # nchannels
1, # sampwidth
sample_rate, # framerate
data_size, # nframes
"NONE", # comptype
"not compresse", # compname
))
fd.writeframes(data)
fd.close()
logger.info('saved wave file')
def dumpscreen(device, fileout):
logger.info('DUMPING SCREEN')
device.write('SCDP')
response = device.read_raw()
fileout.write(response)
fileout.close()
logger.info('END')
def template(device):
response = device.query('TEMPLATE ?')
print response
def configure_opts():
parser = argparse.ArgumentParser(description='Use oscilloscope via VISA')
subparsers = parser.add_subparsers(dest='cmd', help='sub-command help')
parser_a = subparsers.add_parser('list', help='list help')
parser_wave = subparsers.add_parser('wave')
parser_c = subparsers.add_parser('shell', help='VISA shell')
parser_c = subparsers.add_parser('dumpscreen', help='dump screen')
parser_template = subparsers.add_parser('template', help='dump the template for the waveform descriptor')
parser_wave.add_argument('--device', required=True)
parser_wave.add_argument('--out', type=argparse.FileType('w'), required=True)
parser_wave.add_argument('--channel', type=int, required=True)
parser_c.add_argument('--device', required=True)
parser_c.add_argument('--out', type=argparse.FileType('w'), required=True)
parser_template.add_argument('--device', required=True)
return parser
if __name__ == '__main__':
parser = configure_opts()
args = parser.parse_args()
resources = visa.ResourceManager('@py')
if args.cmd == 'list':
list(resources)
sys.exit(0)
elif args.cmd == 'shell':
from pyvisa import shell
shell.main(library_path='@py')
sys.exit(0)
device = resources.open_resource(args.device, write_termination='\n', query_delay=0.25)
idn = device.query('*IDN?')
logger.info('Connected to device \'%s\'' % idn)
if args.cmd == 'wave':
waveform(device, args.out, args.channel)
elif args.cmd == 'dumpscreen':
dumpscreen(device, args.out)
elif args.cmd == 'template':
template(device)
device.close()
The commands are the following
list
Simply shows the device recognized
$ python test_visa.py list
[000] USB0::62700::60986::SDS10PA1164640::0::INSTR
shell
This is the shell available with pyvisa
and by which is
possible to quickly test commands
$ python test_visa.py shell
Welcome to the VISA shell. Type help or ? to list commands.
(visa) list
( 0) USB0::62700::60986::SDS10PA1164640::0::INSTR
(visa) open USB0::62700::60986::SDS10PA1164640::0::INSTR
USB0::62700::60986::SDS10PA1164640::0::INSTR has been opened.
You can talk to the device using "write", "read" or "query.
The default end of message is added to each message
(open) query *IDN?
Response: *IDN SIGLENT,SDS1102CML,SDS10PA1164640,5.01.02.32
wave
This is the more useful: dumps the trace stored into the oscilloscope
as a wav
file.
$ python test_visa.py wave --device USB0::62700::60986::SDS10PA1164640::0::INSTR --out /tmp/wave.wav --channel 1
INFO:__main__:Connected to device '*IDN SIGLENT,SDS1102CML,SDS10PA1164640,5.01.02.32
'
INFO:__main__:detected sample rate of 11250
INFO:__main__:(28, <StatusCode.success: 0>)
INFO:__main__:data size: 20480
INFO:__main__:saved wave file
then you can import the file into audacity
dumpscreen
Sometime is useful to dump the LCD screen of the oscilloscope
$ python test_visa.py dumpscreen --device USB0::62700::60986::SDS10PA1164640::0::INSTR --out screen.bmp
Conclusion
I think in the future I will return to this argument, in particular I want to try to continously read data from the oscilloscope but probably that is not possible for this kind of device, by the way there are a lot of commands that are missing that can be useful to interact with.
Comments
Comments powered by Disqus