Analog data aquisition

The hycon.aquisition package collects various codes related to data aquisition. Currently, we have a number of pythonic analog2digital data auqisition routines:

  • The Hybrid Controller uC itself offers methods such as hycon.HyCon.HyCon.read_ro_group() and similar. The aquisition is high precision (16bit) but quite limited in time, given the small memory of the uC which is used for store the full aquisition before a transfer is possible (no streaming implemented).

  • DSOs (digital storage oscilloscopes) can be used for high quality and industry standard data aquisition. A particular python example is provided in the siglent_scpi.siglent code. These devices are typically also limited in aquisition time, which is naturally mapped to the display. Furthermore, these devices are always very limited in the number of analog channels, and devices with many channels are expensive.

  • Custom Data loggers provide maximum flexibilty, given the large amount of channels implementable with cheap microcontrollers such as the Teensy. However, these cheap uCs typically only have an average resolution of 10bit. A particular example is given at (see also

When it comes to classical analog computing, one would like to optimize the aquisition for high resolution (16bit), many channels (as many as possible). What is not so important is the sampling rate, since the cutoff frequency of the discrete circuits is rather low (at the order of MHz).

Siglent SCPI

Pythonic API for Siglent Digital Oscilloscope in order to perform data aquisition. This is a python standalone code implementing the SCPI protocol (without further Python libraries) over sockets. The connection is assumed to be established over Ethernet (not USB, despite this should also work, in principle, as the siglent registers as UART device over USB). The code can work in blocking and non-blocking fashion (but is not fully async).

See the programming guide for the reference. This code was tested against an Siglent SDS1104X-E with four 10bit channels.

Here is a picture of such an oscilloscope. On the back, it has an RJ45 ethernet connector and a USB client socket. Whether over USB-UART or TCP, it allows to be programmed with the Standards Commands for Programmable Instruments (SCPI) standard, which is basically a simple command-reply text protocol similar to the one we implement with the hycon.HyCon.

Photo of Siglent

Over ethernet, one can also access the oscilloscope screen via VNC. The oscilloscope provides a web interface which includes an in-browser VNC client and models the interface controls. Both is shown side on side in the UI. The interface knobs are translated to SCPI commands via Javascript. The status indicators are polled. The overall UI is quite heavy, rather slow and generates, in particular, high load on the oscilloscope.

Screenshot of Web Interface


Make sure you never ever run the browser interface at the same time as a telnet session or this script. This will break the oscilloscope randomly and requires you to cold start it. This is a reproducable bug in the Siglent firmware.

It is, however, fine to run a VNC connection to the oscilloscope while running this script.

This script is also executable as standalone and offers an API to contact the siglent and do a single readout, assuming the triggering took place independently.

hycon.aquisition.siglent_scpi.downsample(data, factor)[source]

Downsample all numpy 1d arrays, since query("MEMORY_SIZE 70K") has no effect… TODO: MEMSIZE only works in STOP mode.

hycon.aquisition.siglent_scpi.write_npz(data, fname=None)[source]

Write out NPZ files. See for this numpy specific binary file format. Expects data to be a python dictionary (ideally some np.array).

class hycon.aquisition.siglent_scpi.siglent(host, port=5025)[source]

This is a classy API to the oscilloscope. A usage example intermixed with HyCon controlling may look like this:

import hycon.aquisition.siglent_scpi as sig

hc = AutoConfHyCon("something.yml")

siglent = sig.siglent("")
print("Siglent Status: ", siglent.status())

print("ICs freely running, preparing Siglent trigger...")
#input("Press enter to continue")
prepped = siglent.trigger_single()
siglent.log("Siglent Status: ", siglent.status())
if not prepped:
    raise ValueError("Could not properly prepare the Siglent.")
time.sleep(2) # 2 seconds "OP time".
print("Finished with single run sync. Waiting another second for siglent.")
#input("HC stopped. Press enter to readout siglent.")
data = siglent.read_all()
sig.write_npz(data, f"output.npz")


While this code basically works, the refactoring at PyAnalog-introduction time was not tested. That is, this code is not yet fully tested.

send(md, log_end='\n', blocking=False, sleep_sec=0.5)[source]

Send SCPI command to siglent.


Query the siglent for something. This is basically a send()/recv() cycle, according to the SCPI standard.


A shorthand which returns the number in some readout which looks like a = 123.


Reads out the status bit (INR?) and translates it according to the manual, thus returning a string.


Attention, Querying INR? is NOT a read-only operation but changes the oscilloscope status.


Brings the Oscilloscope in single trig mode and checks the success of this operation (returns True in case of passed check).

read_wf(channel, try_multiples=2)[source]

Read the actual waveform. This can be called after an aquisition took place and the oscilloscope trigger fired. In many times, this will fail and return no data. This results in a thrown ValueError. Otherwise, data recieving is looped (with lot’s of logging since it takes some seconds). When some consistency is reached (all announced data is recieved), will return the raw recieved channel data as 1d list of numbers. That is, these are 10bit numbers.

This function will by default try several times to ask for data, in case the siglent did not properly answer.


Read the actual waveforms by querying the passed channel list. A channel is an integer between 0 and 4, see siglent.channels for the valid channel list. If not provided, all channels are read.

Will do the conversion to voltages, that is, returns proper floats representing a voltage in unit V. Returns a dictionary mapping the channel name to the 1d data.