Example project

When you run the srsgui application, it shows in the console window where the Python is running from, and where srsgui is located. If you go into the directory where srsgui resides, you can find the ‘examples’ directory. When you find a .taskconfig file in “oscilloscope example” directory, Open the file from the menu /File/Open Config of srsgui application. If you plan to modify files in the project, you better copy the whole example directory to where you usually keep your documents for programing. And open the .taskconfig file form the copied directory, then no worries about losing the original files.

As an example project of srsgui, I wanted to use ubiquitous measurement instruments with remote communication available. I happened to have an oscilloscope and a function generator (more specifically, a clock generator) on my desk:

  • Siglent SDS1202XE digital oscilloscope (1 GSa/s, 200 MHz bandwidth), I bought it because of its affordable price and it works nicely!

  • Stanford Research Systems CG635, 2 GHz Clock Generator (Disclaimer: I work for the company).

I built a project that controls both instruments and capture output waveform from the clock generator with the oscilloscope, run FFT and display the waveforms in srsgui application.

Any oscilloscope and function generator will work for this example. If you got interested in srsgui, I bet you can find an oscilloscope and a function generator somewhere in your building.

If you could not, don’t worry. Even without an any instruments, we can generate simulated waveform to demonstrate the usability of srsgui as an organizer for Python scripts and GUI environment for convenient data acquisition and data visualization.

Directory structure

Let’s look at the directory structure of the project.

/Oscilloscope example project directory
    /instruments
        cg635.py
        sds1202.py
    /tasks
        first.py
        second.py
        ...

    oscilloscope example project.taskconfig

This file structure follows the guideline described in Creating file structure for a project. We have two instrument driver scripts for SDS1202XE and CG635 in the subdirectory called instruments, five task scripts in the subdirectory called tasks along with a configuration file in the project root directory.

Project configuration file

The structure of a .taskconfig file is simple and explained in Populating the .taskconfig file

1name: Srsgui example project using an oscilloscope and a clock generator
2
3inst: cg,  instruments.cg635,   CG635
4inst: osc, instruments.sds1202, SDS1202
5
6task: *IDN test,                 tasks.first,  FirstTask
7task: Plot example,              tasks.second, SecondTask
8...

Instrument drivers

CG635

Let’s take a look into the instrument/cg635.py module. Even though it seems long, it has only 5 line if the comment lines removed. If you have a CG635, congratulations! You can use the file as is. If you have any function generator that can change the output frequency, you can use it instead of CG635 in the example. You change the class name and _IdString to match the instrument name, along with _term_char, and find out the command to change frequency, referring to the manual. And save it to a different name. Change the inst: line for ‘cg’ in the .taskconfig file to match the module path and the class name.

1from srsgui import Instrument
2from srsgui.inst import FloatCommand
3
4class CG635(Instrument):
5    _IdString = 'CG635'
6    frequency = FloatCommand('FREQ')

Without redefining availab_interfaces class attribute, you can use the serial communication only. If you want to use GPIB communication, you have to uncomment the available_interfaces in CG635 class.

from srsgui import SerialInterface, FindListInput
from srsinst.sr860 import VisaInterface

available_interfaces = [
    [   SerialInterface,
        {
            'COM port': FindListInput(),
            'baud rate': 9600
        }
    ],
    [   VisaInterface,
        {
            'resource': FindListInput(),
        }
    ],
]

you have to install srsinst.sr860 for VisaInterface class, and PyVISA and its backend library, following PyVisa installation instruction.

Once CG635 is connected, you will see the connection information in the instrument info panel, and you can use it in the terminal, as shown below.

_images/cg-terminal-screen-capture.png

To fix the ‘Not implemented’ warning, Instrument.get_status() need to be redefined. To feel better, we can override it as a CG635 method as following:

def get_status(self):
    return 'Status: OK'

We will use only one command .frequency, or its raw remote command, ‘freq’ in this example. Because ‘cg’ is the default instrument, the first instrument mentioned in the .taskconfig file, you can send any raw remote command without ‘cg:’ prefix if you want to. Both ‘*idn?’ and ‘cg:*idn?’ will return the correct reply.

We use the prefix ‘cg:’ for raw remote command and the prefix ‘cg.’ for Python commands. In the terminal all attribute and method of CG835 calss can be used with prefix ‘cg.’. Because we defined frequency as a FloatCommand, we can use ‘cg.frequency’ property in the terminal and any python scripts, once get_instrument(‘cg’) in a task class. Because qurey_float() is the float query command defined in the Instrument class, you can use it in the terminal.

Actually you can use all the attribute and method defined in CG635 class and its super classes. There is cg.dir() method defined in Component class. It shows all the available components, commands, and method. It helps us to navigate through resources available with the class.

_images/cg-dir-terminal-screen-capture.png

From the terminal, you can control all the instruments, as much as you can do with task scripts. Defining many useful methods in an instrument class provides more controls from the terminal, while you are tweaking to find optimal operation parameters of instruments.

SDS1202

Even though you may not have an SDS1202 oscilloscope that I happened to use for this example, I bet you can find an oscilloscope somewhere in your building. When you get a hold of one, it may have a USB connector only, like a lot of base model oscilloscopes do. It means you have to use USB-TMC interface. In order to do that, you need to install PyVISA amd make it work. You need to uncomment the available_interfaces of SDS1202 class, modify it to fit the specification of your oscilloscope, along with changing to the correct _IdString. And you have to get waveform download working. If you are lucky, you can find a working Python snippet from judicious web search. If not you have to decipher the programming manual of the oscilloscope to make the waveform download working. It may take time, It will be very rewarding for your data acquisition skill set improvement.

Other than the binary waveform download, Communication with an oscilloscope will work OK using text based communication for most of remote commands.

With default available_interfaces of Instrument class, TcpipInterface should be used with port 5025.

The instrument driver for SDS1202 will work with 4 lines of code, just like CG635, before adding the method to download waveforms from the oscilloscope. Add attributes and methods incrementally as you need to use more functions of the instrument.

 1import numpy as np
 2from srsgui import Instrument
 3
 4class SDS1202(Instrument):
 5    _IdString = 'SDS1202'
 6
 7    def get_waveform(self, channel):
 8        ...
 9
10    def get_sampling_rate(self):
11        ...

Once the oscilloscope is connected to the application, you can use the terminal to explore the oscilloscope.

_images/osc-dir-terminal-screen-capture.png

Becasue ‘osc’ is not the default instrument, you have to use the prefix ‘osc:’ with all the raw remote commands you send to the instrument. As shown with ‘osc.dir’, there are many methods avaiable with ‘osc.’ Even osc.get_waveform() is available from the terminal. The terminal kindly tells me that there is a missing argument in a function call, when you use a method incorrectly. You can see osc.get_waveform(channel) method returns two numpy array, if you run it. Since the terminal only allow to use attributes and methods of instruments defined in the configuration file, if you have more useful methods defined for the instrument, you can do more in the terminal. However, you are supposed to run more sophisticated data handling in tasks not from the terminal.

Tasks

How to run a task

Start srsgui application. You can see where the .config file is opened from the console window (here). If you made a copy of the original example from the srsgui package directory, open it again from the correct directory.

If there is no error message in the Console window, connect the function generator and the oscilloscope from the Instruments menu, clicking the instrument name that you want to connect.

Select the first task (*IDN test) from the Tasks menu or otheres in the Tasks menu and click the green arrow in the tool bar to run the task.

The overall structure of a task is described in Writing a task script section. There are 5 tasks are included in the example project. They gradually adds more features on the top of the previous tasks. Hopefully, they show most of Task class usage.

FirstTask

The first task shows:

  • How to use module-level logger for Python logging in a task

  • How to use instruments defined in the configuration file

  • How to use text output to the console window

It is not much different from the bare bone structure shown in the Writing a task script section.

 1from srsgui import Task
 2
 3
 4class FirstTask(Task):
 5    """
 6Query *IDN? to instruments, 'cg' and 'osc' \
 7defined in the configuration file.
 8    """
 9    
10    # No interactive input parameters to set before running 
11    input_parameters = {}
12    
13    def setup(self):
14        # To use Python logging
15        self.logger = self.get_logger(__file__)
16
17        # To use the instrument defined in .taskconfig file
18        self.cg = self.get_instrument('cg')
19        self.osc = self.get_instrument('osc')
20
21        # Set clock frequency tp 10 MHz
22
23        # frequency is define as FloatCommand in CG635 class
24        self.cg.frequency = 10000000
25        self.logger.info(f'Current frequency: {self.cg.frequency}')
26
27        # You can do the same thing with FloatCommand defined.
28        # You can use send() and query_float() with raw remote command
29        # self.cg.send('FREQ 10000000')
30        # self.current_frequency = self.cg.query_float('FREQ?')
31        # self.logger.info(self.current_frequency)
32
33    def test(self):
34        # You can use print() only with one argument.
35        print("\n\nLet's query IDs of instruments!!\n\n")
36
37        # Use query_text for raw remote command query returning string
38        cg_id_string = self.cg.query_text('*idn?')
39        osc_id_string = self.osc.query_text('*idn?')
40        
41        self.logger.info(f"CG *IDN : {cg_id_string}")
42        self.logger.info(f"OSC *IDN : {osc_id_string}")
43
44    def cleanup(self):
45        # We have nothing to clean up
46        pass

Using self.logger sends the logging output to the console window, the master logging file in ~/task-results directory/mainlog-xx.txt.x, and to the task result data file located in ~/task-results/project-name-in-config-file/RNxxx directory.

With get_instrument you can get the instrument defined in the configuration file in a task. Do not disconnect the instrument in the task! Instrument Connectivity is managed in the application level.

It show how much it simplifies remote command set and query transactions by defining frequency attribute using srsgui.inst.commands module.

SecondTask

The second task shows:

  • How to define input_parameters for interactive user input from the application input panel

  • How to get matploglib figure and use it to plot

  • How to send text output to result window using display_result()

  • How to stop the main loop by checking is_running().

 1import time
 2import math
 3
 4from srsgui import Task
 5from srsgui import IntegerInput
 6
 7
 8class SecondTask(Task):
 9    """
10It shows how to use a Matplotlib plot in a task. \
11No hardware connection is required to plot a sine curve.
12    """
13    
14    # Interactive input parameters to set before running 
15    Angle = 'final angle to plot'
16    input_parameters = {
17        Angle: IntegerInput(360, ' degree')
18    }
19    """
20    Use input_parameters to get parameters used in the task 
21    """
22
23    def setup(self):
24
25        # Get a value from input_parameters
26        self.total_angle = self.get_input_parameter(self.Angle)
27
28        # Get the Python logging handler
29        self.logger = self.get_logger(__file__)
30
31        # Get the default Matplotlib figure
32        self.figure = self.get_figure()
33
34        # Once you get the figure, the followings are typical Matplotlib things to plot.
35        self.ax = self.figure.add_subplot(111)
36        self.ax.set_xlim(0, self.total_angle)
37        self.ax.set_ylim(-1.1, 1.1)
38        self.ax.text(0.1 * self.total_angle, 0.5, 'Drawing sine curve...')
39        self.line, = self.ax.plot([0], [0])
40        
41    def test(self):
42        print("\n\nLet's plot!\n\n")
43        
44        x = []
45        y = []
46        rad = 180 / math.pi
47
48        for i in range(self.total_angle):
49            if not self.is_running():  # if the stop button is pressed, stop the task
50                break
51       
52            self.logger.info(f'Adding point {i}')
53
54            # Display in the result panel
55            self.display_result(f'\n\nPlotting {i} degree...\n\n', clear=True)
56
57            # Add data to the Matplotlib line and update the plot
58            x.append(i)
59            y.append(math.sin(i / rad))
60            self.line.set_data(x, y)
61
62            # Figure update should be done in the main thread, not locally.
63            self.request_figure_update(self.figure)
64            
65            # Delay a bit as if a sine function computation takes time.
66            time.sleep(0.01)
67
68    def cleanup(self):
69        self.display_result('Done!')

Using matplotlib is straightforward. No harder than standard plots using figures and axes. Refer to matplotlib documentation on how to use it.

The important differences using matplotlib in srsgui are:
  • You have to get the figure using get_figure(), not creating one on your own.

  • You create plots during setup(), because it is slow process. During test(), you just update data using set_data() or similiar methods for data update.

  • You have use request_figure_update() to redraw the plot, after set_data(). The event loop handler in the main application will update the plot at its earliest convenience.

_images/second-task-screen-capture.png

ThirdTask

The third task uses the oscilloscope only. It gets the number of captures from user input, repeat oscilloscope waveform capture and update the waveform plot. It stops after repeats the number of times selected berfore run, or when the Stop button is pressued. When it runs it captures and display a waveform with 700000 points every 0.2 second over TCPIP communcation.

 1import time
 2from srsgui import Task
 3from srsgui import IntegerInput
 4
 5
 6class ThirdTask(Task):
 7    """
 8It captures waveforms from an oscilloscope, \
 9and plot the waveforms real time.
10    """
11    
12    # Use input_parameters to set parameters before running 
13    Count = 'number of captures'
14    input_parameters = {
15        Count: IntegerInput(100) 
16    }
17    
18    def setup(self):
19        self.repeat_count = self.get_input_parameter(self.Count)
20        self.logger = self.get_logger(__file__)        
21
22        self.osc = self.get_instrument('osc') # use the inst name in taskconfig file
23        
24        # Get the Matplotlib figure to plot in
25        self.figure = self.get_figure()
26
27        # Once you get the figure, the following are about Matplotlib things to plot
28        self.ax = self.figure.add_subplot(111)
29        self.ax.set_xlim(-1e-5, 1e-5)
30        self.ax.set_ylim(-1.5, 1.5)
31        self.ax.set_title('Scope waveform Capture')
32        self.x_data = [0]
33        self.y_data = [0]
34        self.line, = self.ax.plot(self.x_data,self.y_data)
35        
36    def test(self):
37        prev_time = time.time()
38        for i in range(self.repeat_count):
39            if not self.is_running(): # if the Stop button is pressed
40                break
41            
42            # Add data to the Matplotlib line and update the figure
43            t, v = self.osc.get_waveform('C1')  # Get a waveform of the Channel 1 from the oscilloscope
44            self.line.set_data(t, v)
45            self.request_figure_update()
46            
47            # Calculate the time for each capture
48            current_time = time.time()
49            diff = current_time - prev_time
50            self.logger.info(f'Capture time for {len(v)} points of waveform {i}: {diff:.3f} s')
51            prev_time = current_time
52            
53    def cleanup(self):
54        pass

FourthTask

The fourth example is the climax of the examples series. It uses input_parameters to change output frequency of the clock generator interactively, captures waveforms from the oscilloscope, run FFT of the waveforms with Numpy, and plot using 2 matplotlib figures.

by adding the names of figures that you want to use in additional_figure_names, srsgui provides more figures to the task before it starts.

  1import time
  2import numpy as np
  3from srsgui import Task
  4from srsgui import IntegerInput
  5
  6
  7class FourthTask(Task):
  8    """
  9Change the frequency of the clock generator output interactively, \
 10capture waveforms from the oscilloscope, \
 11calculate FFT of the waveforms, \
 12plot the waveforms and repeat until the stop button pressed.  
 13    """
 14
 15    # Interactive input parameters to set before running 
 16    Frequency = 'Frequency to set'
 17    Count = 'number of runs'
 18    input_parameters = {
 19        Frequency: IntegerInput(10000000, ' Hz', 100000, 200000000, 1000),
 20        Count: IntegerInput(10000)
 21    }
 22
 23    # Add another figure for more plots
 24    FFTPlot = 'FFT plot'
 25    additional_figure_names = [FFTPlot]
 26
 27    def setup(self):
 28        self.repeat_count = self.get_input_parameter(self.Count)
 29        self.set_freq = self.input_parameters[self.Frequency]
 30
 31        self.logger = self.get_logger(__file__)
 32        
 33        self.osc = self.get_instrument('osc') # use the inst name in taskconfig file
 34        
 35        self.cg = self.get_instrument('cg')
 36        self.cg.frequency = self.set_freq  # Set cg frequency
 37        
 38        self.init_plots()
 39        
 40    def test(self):
 41        prev_time = time.time()
 42        for i in range(self.repeat_count):
 43            if not self.is_running(): # if the Stop button is pressed
 44                break
 45                
 46            # Check if the user changed the set_frequency 
 47            freq = self.get_input_parameter(self.Frequency)
 48            if self.set_freq != freq:
 49                self.set_frequency(freq)
 50                self.logger.info(f'Frequency changed to {freq} Hz')
 51                self.set_freq = freq
 52                
 53            # Get a waveform from the oscillscope and update the plot 
 54            t, v, sara = self.get_waveform()
 55            self.line.set_data(t, v)
 56            self.request_figure_update()
 57
 58            # Calculate FFT with the waveform and update the plot
 59            size = 2 ** int(np.log2(len(v)))  # largest power of 2 <= waveform length
 60
 61            window = np.hanning(size)  # get a FFT window
 62            y = np.fft.rfft(v[:size] * window)
 63            x = np.linspace(0, sara /2, len(y))
 64            self.fft_line.set_data(x, abs(y) / len(y))
 65
 66            self.request_figure_update(self.fft_fig) 
 67
 68            # Calculate time for each capture
 69            current_time = time.time()
 70            diff = current_time - prev_time
 71            print(f'Waveform no. {i}, {len(v)} points, time taken: {diff:.3f} s')
 72            prev_time = current_time
 73            
 74    def cleanup(self):
 75        pass
 76        
 77    def init_plots(self):
 78        # Once you get the figure, the following are about Matplotlib things to plot
 79        self.figure = self.get_figure()
 80        self.ax = self.figure.add_subplot(111)
 81        self.ax.set_xlim(-1e-6, 1e-6)
 82        self.ax.set_ylim(-1.5, 1.5)
 83        self.ax.set_title('Clock waveform')
 84        self.ax.set_xlabel('time (s)')
 85        self.ax.set_ylabel('Amplitude (V)')
 86        self.x_data = [0]
 87        self.y_data = [0]
 88        self.line, = self.ax.plot(self.x_data, self.y_data)
 89
 90        # Get the second figure for FFT plot.
 91        self.fft_fig = self.get_figure(self.FFTPlot)
 92
 93        self.fft_ax = self.fft_fig.add_subplot(111)
 94        self.fft_ax.set_xlim(0, 1e8)
 95        self.fft_ax.set_ylim(1e-5, 1e1)
 96        self.fft_ax.set_title('FFT spectum')
 97        self.fft_ax.set_xlabel('Frequency (Hz)')
 98        self.fft_ax.set_ylabel('Magnitude (V)')
 99        self.fft_x_data = [0]
100        self.fft_y_data = [1]     
101        self.fft_line, = self.fft_ax.semilogy(self.fft_x_data,self.fft_y_data)        
102
103    def get_waveform(self):
104        t, v = self.osc.get_waveform('c1') # Get Ch. 1 waveform
105        sara = self.osc.get_sampling_rate()
106        return t, v, sara
107        
108    def set_frequency(self, f):
109        self.cg.frequency = f

FifthTask

the fifth examples is to show how to subclass an existing task class to reuse. the method get_waveform() in the fourth example is reimplemented to generate simulated waveform that runs without any real oscilloscope.

Note that the square wave edge calculation is crude, and causing modulation in pulse width that shows side bands in FFT spectrum, if the set frequency is not commensurated with the sampling rate. To generate clean square wave, the rising and falling edges should have at least two points to represent exact phase. Direct transition from low to high without any intermediate points suffers from subtle modulation in time domain, which manifests as side bands in FFT. This is a common problem in digital signal processing. It is not a problem in the real world, because the signal is analog, and the sampling rate is limited by the bandwidth of the signal.

  1import time
  2import numpy as np
  3from srsgui import Task
  4from srsgui import IntegerInput
  5
  6
  7class FourthTask(Task):
  8    """
  9Change the frequency of the clock generator output interactively, \
 10capture waveforms from the oscilloscope, \
 11calculate FFT of the waveforms, \
 12plot the waveforms and repeat until the stop button pressed.  
 13    """
 14
 15    # Interactive input parameters to set before running 
 16    Frequency = 'Frequency to set'
 17    Count = 'number of runs'
 18    input_parameters = {
 19        Frequency: IntegerInput(10000000, ' Hz', 100000, 200000000, 1000),
 20        Count: IntegerInput(10000)
 21    }
 22
 23    # Add another figure for more plots
 24    FFTPlot = 'FFT plot'
 25    additional_figure_names = [FFTPlot]
 26
 27    def setup(self):
 28        self.repeat_count = self.get_input_parameter(self.Count)
 29        self.set_freq = self.input_parameters[self.Frequency]
 30
 31        self.logger = self.get_logger(__file__)
 32        
 33        self.osc = self.get_instrument('osc') # use the inst name in taskconfig file
 34        
 35        self.cg = self.get_instrument('cg')
 36        self.cg.frequency = self.set_freq  # Set cg frequency
 37        
 38        self.init_plots()
 39        
 40    def test(self):
 41        prev_time = time.time()
 42        for i in range(self.repeat_count):
 43            if not self.is_running(): # if the Stop button is pressed
 44                break
 45                
 46            # Check if the user changed the set_frequency 
 47            freq = self.get_input_parameter(self.Frequency)
 48            if self.set_freq != freq:
 49                self.set_frequency(freq)
 50                self.logger.info(f'Frequency changed to {freq} Hz')
 51                self.set_freq = freq
 52                
 53            # Get a waveform from the oscillscope and update the plot 
 54            t, v, sara = self.get_waveform()
 55            self.line.set_data(t, v)
 56            self.request_figure_update()
 57
 58            # Calculate FFT with the waveform and update the plot
 59            size = 2 ** int(np.log2(len(v)))  # largest power of 2 <= waveform length
 60
 61            window = np.hanning(size)  # get a FFT window
 62            y = np.fft.rfft(v[:size] * window)
 63            x = np.linspace(0, sara /2, len(y))
 64            self.fft_line.set_data(x, abs(y) / len(y))
 65
 66            self.request_figure_update(self.fft_fig) 
 67
 68            # Calculate time for each capture
 69            current_time = time.time()
 70            diff = current_time - prev_time
 71            print(f'Waveform no. {i}, {len(v)} points, time taken: {diff:.3f} s')
 72            prev_time = current_time
 73            
 74    def cleanup(self):
 75        pass
 76        
 77    def init_plots(self):
 78        # Once you get the figure, the following are about Matplotlib things to plot
 79        self.figure = self.get_figure()
 80        self.ax = self.figure.add_subplot(111)
 81        self.ax.set_xlim(-1e-6, 1e-6)
 82        self.ax.set_ylim(-1.5, 1.5)
 83        self.ax.set_title('Clock waveform')
 84        self.ax.set_xlabel('time (s)')
 85        self.ax.set_ylabel('Amplitude (V)')
 86        self.x_data = [0]
 87        self.y_data = [0]
 88        self.line, = self.ax.plot(self.x_data, self.y_data)
 89
 90        # Get the second figure for FFT plot.
 91        self.fft_fig = self.get_figure(self.FFTPlot)
 92
 93        self.fft_ax = self.fft_fig.add_subplot(111)
 94        self.fft_ax.set_xlim(0, 1e8)
 95        self.fft_ax.set_ylim(1e-5, 1e1)
 96        self.fft_ax.set_title('FFT spectum')
 97        self.fft_ax.set_xlabel('Frequency (Hz)')
 98        self.fft_ax.set_ylabel('Magnitude (V)')
 99        self.fft_x_data = [0]
100        self.fft_y_data = [1]     
101        self.fft_line, = self.fft_ax.semilogy(self.fft_x_data,self.fft_y_data)        
102
103    def get_waveform(self):
104        t, v = self.osc.get_waveform('c1') # Get Ch. 1 waveform
105        sara = self.osc.get_sampling_rate()
106        return t, v, sara
107        
108    def set_frequency(self, f):
109        self.cg.frequency = f