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 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

   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

      Version: 1.8 (bundled with PyVISA)
      Binary library: Not found
      Version: 0.2
         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
         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

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).


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 view raw
# encoding: utf-8
# See document "Programming Guide" at <>
import sys
import logging
import argparse
import wave #

import visa #


logger = logging.getLogger(__name__)

def usage(progname):
    print 'usage: %s [list|dump]' % progname

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])'detected sample rate of %d' % sample_rate)

    #desc = device.write('C%d: WF? DESC' % channel)

    # the response to this is binary data so we need to write() and then read_raw()
    # to avoid encode() call and relative UnicodeError'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]'data size: %d' % data_size)

    fd =, "w")
        1,               # nchannels
        1,               # sampwidth
        sample_rate,     # framerate
        data_size,       # nframes
        "NONE",          # comptype
        "not compresse", # compname
    fd.close()'saved wave file')

def dumpscreen(device, fileout):'DUMPING SCREEN')

    response = device.read_raw()


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':
    elif args.cmd == 'shell':
        from pyvisa import shell

    device = resources.open_resource(args.device, write_termination='\n', query_delay=0.25)
    idn = device.query('*IDN?')'Connected to device \'%s\'' % idn)

    if args.cmd == 'wave':
        waveform(device, args.out,
    elif args.cmd == 'dumpscreen':
        dumpscreen(device, args.out)
    elif args.cmd == 'template':


The commands are the following


Simply shows the device recognized

$ python list
[000] USB0::62700::60986::SDS10PA1164640::0::INSTR


This is the shell available with pyvisa and by which is possible to quickly test commands

$ python 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,


This is the more useful: dumps the trace stored into the oscilloscope as a wav file.

$ python wave --device USB0::62700::60986::SDS10PA1164640::0::INSTR --out /tmp/wave.wav --channel 1
INFO:__main__:Connected to device '*IDN SIGLENT,SDS1102CML,SDS10PA1164640,
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

trace imported into audacity as wave file


Sometime is useful to dump the LCD screen of the oscilloscope

$ python dumpscreen --device USB0::62700::60986::SDS10PA1164640::0::INSTR --out screen.bmp

SIGLENT dumped screen


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.