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

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.

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.

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

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