TutorialsWritePythonApplications
NOTE: This tutorial has been deprecated in GR 3.8.
Welcome, GNU Radio beginners. If you are reading this tutorial, you probably already have some very basic knowledge about how GNU Radio works, what it is and what it can do - and now you want to enter this exciting world of Open Source digital signal processing (DSP) yourself.
This is a tutorial on how to write applications for GNU Radio in Python using the version 3.7 API. It is no introduction to programming, software radio or signal processing, nor does it cover how to extend GNU Radio by creating new blocks or adding code to the source tree. If you have some background in the mentioned topics and are starting to work with GNU Radio, this probably is the correct tutorial for you. If you don't know what a Software Radio is or what a FIR filter does, you should probably go a few steps back and get a more solid background on signal processing theory. But don't let this discourage you - the best way to learn something is by trying it out.
Although this tutorial is designed to make your introduction to GNU Radio as easy as possible, it is not a definitive guide. In fact, I might sometimes simply not tell the real truth to make explanations easier. I might even contradict myself in later chapters. Usage of brain power is still necessary to develop GNU Radio applications.
Preliminaries
Before you get started with this tutorial, make sure your GNU Radio installation is ready and working. You don't necessarily need a USRP, but some kind of source and sink (USRP, audio or other hardware) is helpful, although not strictly required. If the GNU Radio examples work (such as dial_tone.py in gr-audio/examples/python), you're ready to go.
You should also have some background in programming - but don't worry if you've never programmed Python, it is a very easy language to learn.
Understanding flow graphs
Before we start banging out code, first we need to understand the most basic concepts about GNU Radio: flow graphs (as in graph theory) and blocks. Many GNU Radio applications contain nothing other than a flow graph. The nodes of such a graph are called blocks, and the data flows along the edges.
Any actual signal processing is done in the blocks. Ideally, every block does exactly one job - this way GNU Radio stays modular and flexible. Blocks are written in C++; writing new blocks is not very difficult (but explained elsewhere).
The data passing between blocks can be of any kind - practically any type of data you can define in C++ is possible. In practice, the most common data types are complex and real short or long integers and floating point values as most of the time, data passing from one block to the next will either be samples or bits.
Examples
In order to illuminate this diffuse topic a little, let's start with some examples:
Low-pass filtered audio recorder
+-----+ +-----+ +----------------+ | Mic +--+ LPF +--+ Record to file | +-----+ +-----+ +----------------+
First, an audio signal from a microphone is recorded by your PCs sound card and converted into a digital signal. The samples are streamed to the next block, the low pass filter (LPF), which could be implemented as an FIR filter. The filtered signal is passed on to the final block, which records the filtered audio signal into a file.
This is a simple, yet complete flow graph. The first and last block serve a special purpose: they operate as source and sink. Every flow graph needs at least one source and sink to be able to function.
Dial tone generator
+------------------------+ | Sine generator (350Hz) +---+ +------------------------+ | +------------+ +---+ | | Audio sink | +---+ | +------------------------+ | +------------+ | Sine generator (440Hz) +---+ +------------------------+
This simple example is often called the "Hello World of GNU Radio". Other than the first example, it has two sources. The sink, on the other hand, has two inputs - in this case for the left and right channel of the sound card. Code for this example is available at gr-audio/examples/python/dial_tone.py.
QPSK Demodulator
+-------------+ +----------------+ +------------------+ | USRP Source +--+ Frequency sync +--+ Matched filter | +-------------+ +----------------+ +-----------+------+ | COMPLEX SAMPLES +-------------+------+ | Symbol demodulator | +-------------+------+ | COMPLEX SYMBOLS +-----------------+ +-----------------+ +------+------+ | Source decoder +--+ Channel decoder +--+ Bit mapping | +--------+--------+ +-----------------+ +-------------+ | BITS +--------+--------+ | Application | DATA +-----------------+
This example is a bit more sophisticated, but should look quite familiar to RF engineers. In this case, the source is a USRP which is connected to an antenna. This kind of source sends complex samples to the following blocks.
The interesting part about this kind of flow graph is that the data types change during the flow graph: at first, complex baseband samples are passed along. Then, complex symbols are gathered from the signal. Next, these symbols are turned into bits which again are processed further. Finally, the decoded bits are passed to some application which makes use of the data.
Walkie Talkie
+--------------+ +------------------+ +---------+ +------------+ | USRP Source +--+ NBFM Demodulator +--+ Squelch +--+ Audio Sink | +--------------+ +------------------+ +---------+ +------------+ +--------------+ +----------------+ +------------+ | Audio Source +----------+ NBFM Modulator +---------+ USRP Sink | +--------------+ +----------------+ +------------+
This applications consists of two separate flow graphs, both running in parallel. One of them deals with the Tx path, the other with the Rx path. This kind of application would require some extra code (outside the flow graphs) to mute one path while the other is active. Both flow graphs still require at least one source and sink, each. You can find a GNU Radio application that does this (only a bit more sophisticated) at gr-uhd/examples/python/usrp_nbfm_ptt.py.
Summary
This concludes the chapter about flow graphs. Here's a quick summary about the most vital points you really need to know:
- All signal processing in GNU Radio is done through flow graphs.
- A flow graph consists of blocks. A block does one signal processing operation, such as filtering, adding signals, transforming, decoding, hardware access or many others.
- Data passes between blocks in various formats, complex or real integers, floats or basically any kind of data type you can define.
- Every flow graph needs at least one sink and source.
A first working code example
Next step is to find out how to write those flow graphs in real Python. Let's start by examining some code line-by-line. If you are familiar with Python, you can probably skip some of the explanations, but don't rush to the next section yet - the explanations are both for Python and GNU Radio beginners.
The following code example represents the flow graph from example 2. It is actually a slightly modified version of the code example you can find in gr-audio/examples/python/dial_tone.py.
#!/usr/bin/env python3 from gnuradio import gr from gnuradio import audio, analog class my_top_block(gr.top_block): def __init__(self): gr.top_block.__init__(self) sample_rate = 32000 ampl = 0.1 src0 = analog.sig_source_f(sample_rate, analog.GR_SIN_WAVE, 350, ampl) src1 = analog.sig_source_f(sample_rate, analog.GR_SIN_WAVE, 440, ampl) dst = audio.sink(sample_rate, "") self.connect(src0, (dst, 0)) self.connect(src1, (dst, 1)) if __name__ == '__main__': try: my_top_block().run() except [[KeyboardInterrupt]]: pass
The first line should look familiar to anyone with some Unix or Linux background: It tells the shell that this file is a Python file and to use the Python interpreter to run this file. You need this line if you want to run this file directly from the command line.
Lines 3 and 4 import necessary Python modules to run GNU Radio. The import
command is similar to the #include
directive in C/C++. Here, three modules from the gnuradio package are imported: gr
, audio
, and analog
. The first module, gr
, is the basic GNU Radio module. You will always have to import this to run a GNU Radio application. The second loads audio device blocks, and the third is where the blocks related to analog signal functionality and modulation are located. There are many GNU Radio modules, a short list of modules will be presented later on.
Lines 6-17 define a class called my_top_block
which is derived from another class, gr.top_block
. This class is basically a container for the flow graph. By deriving from gr.top_block
, you get all the hooks and functions you need to add blocks and connect them.
Only one member function is defined for this class: the function __init__()
, which is the constructor of this class. In the first line of this function (line 8), the parent constructor is called (in Python, this needs to be done explicitly. Most things in Python need to be done explicitly; in fact, this is one main Python principle).
Next, two variables are defined: sample_rate
and ampl
. These will control sampling rate and amplitude of the signal generators.
Before explaining the next lines, have another look at the sketched flow graph chart in the previous section: it consists of three blocks and two edges. The blocks are defined in lines 13-15: Two signal sources are generated (called src0
and src1
). These sources continuously create sine waves at given frequencies (350 and 440Hz) and a given sampling rate (here 32kHz). The amplitude is controlled by the ampl variable and set to 0.1. The prefix "f" of the block type analog.sig_source_f
indicates the output is of type float
, which is a good thing because the audio sink accepts floating point samples in the range between 1 and +1. These kind of things must be taken care of by the programmer: although GNU Radio does some checks to make sure the connections make sense, there is still some things that must be taken care of manually. For example, if you wanted to feed integer samples to audio.sink
, GNU Radio would throw an error but if you would set the amplitude in the above example to anything larger than 1, you would get a distorted signal without receiving an error.
The signal sink is defined in line 15: audio.sink()
returns a block which acts as a soundcard control and plays back any samples piped into it. As in the blocks beforehand, the sampling rate needs to be set explicitly, even though this was set already for the signal sources. GNU Radio cannot guess the correct sampling rate from the context, as it is not part of the information flow between blocks.
Lines 16 and 17 connect the blocks. The general syntax for connecting blocks is self.connect(block1, block2, block3, ...)
which would connect the output of block1
with the input of block2
, the output of block2
with the input of block3
and so on. You can connect as many blocks as you wish with one connect()
call. Here, a special syntax is necessary because we want to connect src0
with the first input of dst
and src1
with the second one. self.connect (src0, (dst, 0))
does exactly this: it specifically connects src0
to port 0 of dst
. (dst, 0)
is called a "tuple" in Python jargon. In the self.connect()
call it is used to specify the port number. When the port number is zero, the block may be used alone. An equivalent command to the one in line 16 would thus have been
self.connect((src0, 0), (dst, 0))
That's all there is to create a flow graph. The last 5 lines do nothing but start the flow graph (line 22). The try
and except
statements simply make sure the flow graph (which would otherwise run infinitely) are stopped when Ctrl+C is pressed (which triggers a KeyboardInterrupt
Python exception).
For Python-beginners, two more remarks should not be left out: As you might have noticed, the class my_top_block is run without creating an instance beforehand. In Python, this is a quite common thing to do, especially if you have a class which would only get one instance anyway. However, you could just as well create one or more instances of the class and then call the run()
method on the instance(es).
Second, the indenting is part of the code and not, like in C++, simply for the programmers convenience. If you try and modify this code, make sure you don't start mixing tabs and spaces. Every level must be consistently indented.
If you want to go on with this tutorial, you should first get a more solid Python background. Python documentation can be found at the Python web site http://www.python.org/, or a library of your choice. A good place to start for people with prior programming experience is http://wiki.python.org/moin/BeginnersGuide/Programmers .
Summary
- You need to import required GNU Radio modules with the
from gnuradio import
command. You always need the modulegr
. - A flow graph is contained in a class which itself is derived from
gr.top_block
. - Blocks are created by calling functions such as
analog.sig_source_f()
and saving the return value to a variable. - Blocks are connected by calling
self.connect()
from within the flow graph class - If you don't feel comfortable writing some basic Python code now, have a break and go through some Python tutorials.
The next section will give a more detailed overview about writing GNU Radio applications in Python.
Coding Python GNU Radio Applications
The example above already covers quite a lot of how to write Python GNU Radio applications. This chapter and the next will try to show the possibilities of GNU Radio applications and how to use them. From now on, there is no need to linearly read these chapters section-for-section, it probably makes more sense to go over the titles and find out what you want to know.
GNU Radio Modules
GNU Radio comes with quite a lot of libraries and modules. You will usually include modules with the following syntax:
from gnuradio import MODULENAME
Some modules work a bit differently, see the following list on the most common modules.
gr | The main GNU Radio library. You will nearly always need this. | |||
analog | Anything related to analog signals and analog modulation | |||
audio | Soundcard controls (sources, sinks). You can use this to send or receive audio to the sound cards, but you can also use your sound card as a narrow band receiver with an external RF frontend. | |||
blocks | Everything else. If it doesn't fit in another category, it's probably here. | |||
channels | Channel models for simulation. | |||
digital | Anything related to digital modulation. | |||
fec | Anything related to Forward Error Correction (FEC). | |||
fft | Anything related to FFTs. | |||
filter | Filter blocks and design tools like firdes and optfir. | |||
plot_data | Some functions to plot data with Matplotlib | |||
qtgui | Graphical tools to plot data (time, frequency, spectrogram, constellation, histogram, time raster) using the QT library. | |||
trellis | Blocks and tools for building trellis, trellis coded modulation, and other FSMs. | |||
vocoder | Anything dealing with voice coding/decoding. | |||
wavelet | Anything dealing with wavelets. |
| ||
eng_notation | Adds some functions to deals with numbers in engineering notation such as @100M' for 100 * 10^6'. | |||
eng_options | Use from gnuradio.eng_options import eng_options to import this feature. This module extends Pythons optparse module to understand engineering notation (see above).
|
|||
gru | Miscellaneous utilities, mathematical and others. |
This is by far not a complete list, nor are the descriptions of the modules very useful by themselves. GNU Radio code changes a lot, so creating a static documentation would not be very sensible. GNU Radio uses Doxygen and Sphinx to dynamically create documentation of the APIs.
Instead, you will have to use the good old Star Wars motto to delve further into the details of the modules: "Use the source!". If you feel GNU Radio should really already have some functionality you want to use, either browse through the module directory Python uses or go through the source directory of GNU Radio. In particular, pay attention to the directories starting with gr-
in the source directory, such as gr-trellis. These produce their own code and, consequently, their own modules.
Of course, Python itself comes with a lot of modules, some of which are extremely useful - if not necessary - to write GNU Radio applications. Check the Python documentation and the SciPy website for more information.
Choosing, defining and configuring blocks
GNU Radio comes with an abundance of pre-defined blocks, so for beginners, it is often quite confusing to find the correct blocks for their applications and set them up correctly. Doxygen and Sphinx are used to automatically generate documentation for the C++ and Python APIs (C++ Manual, Python Manual.
You can also generate this documentation locally so it always matches the version you have installed. If you have Doxygen and Sphinx installed, CMake takes care of this automatically.
Learning how to use these documentations is a major part of learning how to use GNU Radio!
Let's get practical. Here's the three lines from the previous example which define the blocks:
src0 = analog.sig_source_f (sample_rate, analog.GR_SIN_WAVE, 350, ampl) src1 = analog.sig_source_f (sample_rate, analog.GR_SIN_WAVE, 440, ampl) dst = audio.sink (sample_rate, "")
Here's a simplified version of what happens when this code is executed: First, a function called sig_source_f
in the module analog
is executed. It receives four function arguments:
- sample_rate, which is a Python variable,
- analog.GR_SIN_WAVE, which is a constant defined in the
analog
module (defined here, - 350, a normal literal constant that's the frequency of the sine wave (relative to the sample rate),
- ampl, another variable to set the amplitude of the sine wave.
This function creates a class which is subsequently assigned to src0
. The same happens on the other two lines, although the sink is fetched from a different module (audio
).
So how did I know which block to use and what to pass to analog.sig_source_f()
? This is where the documentation comes in. If you use the Sphinx-generated docs, click on "gnuradio.analog". Proceed to "Signal Sources". You will find a list of signal generators, including the sig_source_*
family. The suffix defines the data type at the output:
- f = float
- c = complex float
- i = int
- s = short int
- b = bits (actually an integer type)
These suffixes are used for all types of blocks, e.g. filter.fir_filter_ccf()
will define an FIR filter with complex input, complex output and float taps, and blocks.add_const_ss()
will define a block which adds incoming short values with another, constant, short int.
Even if you don't want to touch C+, it's worth having a look at the Doxygen-generated documentation as well, since most of the blocks are actually written in C+ but then exported to Python.
At this point, it is worth having a closer look behind the curtains of GNU Radio. The reason you can easily use the blocks - written in C++ - in your Python code is because GNU Radio uses a tool called SWIG to create an interface between Python and C+. Every block in C+ comes with a creating function, called gr::component::block::make(***)
(gr::analog::sig_source_f::make()
in the example mentioned above). This function is always documented on the same page as the matching class, and this function is what gets exported to Python, so analog.sig_source_f()
in Python calls gr::analog::sig_source_f::make()
in C++. For the same reason, it takes the same arguments - that's how you know how to initialize a block in Python.
Once you're browsing the Doxygen documentation of the class gr::analog::sig_source_f
, you might notice many other class methods, such as set_frequency()
. These functions get exported to Python as well. So if you have created a signal source and want to change the frequency (say your application has a user frequency control) you can use this method on your Python defined block:
# We're in some cool application here src0 = analog.sig_source_f (sample_rate, analog.GR_SIN_WAVE, 350, ampl) # Other, fantastic things happen here src0.set_frequency(880) # Change frequency
will change the frequency of the first signal generator to 880Hz.
Hopefully, GNU Radio documentation will grow and become more and more complete. But to completely understand the workings of blocks in detail, you will probably have to have a look at the code sooner or later, no matter how good the documentation gets.
Connecting blocks
Use the connect()
method of gr.top_block
to connect blocks. Some things are worth mentioning:
- You can only connect inputs and outputs if the data types match. If you try to connect a float output with a complex input, you will get an error.
- One output can be connected to several inputs; you don't need an extra block to duplicate signal paths.
These are basic rules for connecting blocks and they work in most cases. However, when mixing data types some more notes are worth mentioning.
- GNU Radio checks if input and output types match by checking their size. If you happen to connect up ports with different types but the same size, you will most definitely get data junk.
- When processing single bits, be careful. In some cases, you will work with binary data in a usual sense, in other cases you want to handle a specific number of bits at a time. Have a look at the
packed_to_unpacked*
andunpacked_to_packed*
blocks for this. - Be careful with dynamic ranges. When you're using float or complex data types, you have a larger range than you'll ever need concerning the machine, but some sinks and sources have specific ranges you need to stick to. For example, audio sinks require samples within -1 and will clip anything outside this interval. The USRP sink on the other hand needs samples in the-32767 range (signed 16 bit values) because that's the dynamic range of the DAC.
Hierarchical blocks
Sometimes it makes sense to combine several blocks into a new block. Say you have several applications which all have a common signal processing component which consists of several blocks. These blocks can be combined into a new block, which in turn can be used in your applications is if it were a normal GNU Radio block.
Example: Say you have two different flow graphs, FG1 and FG2. Both use - among others - the blocks B1 and B2. You want to combine them to a hierarchical block called HierBlock
:
+-------------------+ | +-----+ +----+ | --+--+ B1 +--+ B2 +--+--- | +-----+ +----+ | | HierBlock | +-------------------+
This is what you do: create a flow graph which derives from gr.hier_block2
and use self
as source and sink:
class HierBlock(gr.hier_block2): def __init__(self, audio_rate, if_rate): gr.hier_block2.__init__(self, "HierBlock", gr.io_signature(1, 1, gr.sizeof_float), gr.io_signature(1, 2, gr.sizeof_gr_complex)) B1 = blocks.block1(...) # Put in proper code here! B2 = blocks.block2(...) self.connect(self, B1, B2, self)
As you can see, creating a hierarchical block is very similar to creating a flow graph with gr.top_block
. Apart from using self
as source and sink, there is another difference: the constructor for the parent class (called in line 3) needs to receive additional information. The call to gr.hier_block2.+init+()
takes four parameters:
- self (which is always passed to the constructor as first argument),
- a string with an identifier for the hierarchical block (change at your convenience),
- an input signature and an
- output signature.
The last two require some extra explanation unless you have already written your own blocks in C++. GNU Radio needs to know what types of input and output the block uses. Creating an input/output signature can be done by calling gr.io_signature()
, as is done here. This function call takes 3 arguments:
- minimum number of ports,
- maximum number of ports and
- size of the input/output elements.
For the hierarchical block HierBlock
, you can see that it has exactly one input and one or two outputs. The incoming objects are of size float
, so the block processes incoming real float values. Somewhere in B1 or B2, the data is converted to complex float values, so the output signature declares outgoing objects to be of size gr.sizeof_gr_complex
. The 'gr.sizeof_float@ and gr.sizeof_gr_complex
are equivalent to the C++ return values of the sizeof()
call. Other predefined constants are
- gr.sizeof_int
- gr.sizeof_short
- gr.sizeof_char
Use gr.io_signature(0, 0, 0) to create a null IO signature, i.e. for defining hierarchical blocks as sources or sinks.
That's all. You can now use HierBlock
as you would use a regular block. For example, you could put this code in the same file:
class FG1(gr.top_block): def __init__(self): gr.top_block.__init__(self) ... # Sources and other blocks are defined here other_block1 = blocks.other_block() hierblock = HierBlock() other_block2 = blocks.other_block() self.connect(other_block1, hierblock, other_block2) ... # Define rest of FG1
Of course, to make use of Pythons modularity, you could also put the code for HierBlock
in an extra file called hier_block.py
. To use this block from another file, simply add an import directive to your code:
from hier_block import HierBlock
and you can use HierBlock
as mentioned above.
Examples for hierarchical blocks:
gr-uhd/examples/python/fm_tx_2_daughterboards.py gr-uhd/examples/python/fm_tx4.py gr-digital/examples/narrowband/tx_voice.py
Multiple flow graphs
In some cases, you might want to have completely separate flow graphs, e.g. for receive and transmit paths (see the example 'Walkie-Talkie' above). Currently (June 2008), it is not possible to have multiple top_blocks running at the same time, but what you can do is create full flow graphs as hierarchical blocks using gr.hier_block2
like in the section above. Then, create a top_block to hold the flow graphs.
Example:
class transmit_path(gr.hier_block2): def __init__(self): gr.hier_block2.__init__(self, "transmit_path", gr.io_signature(0, 0, 0), # Null signature gr.io_signature(0, 0, 0)) source_block = blocks.source() signal_proc = blocks.other_block() sink_block = blocks.sink() self.connect(source_block, signal_proc, sink_block) class receive_path(gr.hier_block2): def __init__(self): gr.hier_block2.__init__(self, "receive_path", gr.io_signature(0, 0, 0), # Null signature gr.io_signature(0, 0, 0)) source_block = blocks.source() signal_proc = blocks.other_block() sink_block = blocks.sink() self.connect(source_block, signal_proc, sink_block) class my_top_block(gr.top_block): def __init__(self): gr.top_block.__init__(self) tx_path = transmit_path() rx_path = receive_path() self.connect(tx_path) self.connect(rx_path)
Now, when you start my_top_block
, both flow graphs are started in parallel. Note that the hierarchical blocks have explicitly no inputs and outputs defined, they have a null IO signature. Consequently, they don't connect to self
as source or sink; they rather define their own sources or sink (just as you would do when defining a hierarchical block as source or sink). The top block simply connects the hierarchical blocks to itself, but does not connect them up in any way.
Examples for multiple flow graphs:
gr-uhd/examples/python/usrp_nbfm_ptt.py
GNU Radio extensions and tools
GNU Radio is more than blocks and flow graphs - it comes with a lot of tools and code to help you write DSP applications.
A collection of useful GNU Radio applications designed to aid you is in gr-utils/.
Browse the source code in gnuradio-runtime/python/gnuradio (and other gr-<component>/python directories) to find utilities you can use in your Python code such as filter design code, modulation utilities, and more.
Controlling flow graphs
If you have followed the tutorial so far, you will have noticed that a flow graph has always been implemented as a class, derived from gr.top_block
. The question remains on how to control one of these classes.
As mentioned before, deriving the class from gr.top_block brings along all the functionality you might need. To run or stop an existing flow graph, use the following methods:
run()
|
The simplest way to run a flow graph. Calls start(), then wait(). Used to run a flow graph that will stop on its own, or to run a flow graph indefinitely until SIGINT is received. | |||
start()
|
Start the contained flow graph. Returns to the caller once the threads are created. | |||
stop()
|
Stop the running flow graph. Notifies each thread created by the scheduler to shutdown, then returns to caller. | |||
wait()
|
Wait for a flow graph to complete. Flowgraphs complete when either (1) all blocks indicate that they are done, or (2) after stop has been called to request shutdown. | |||
lock()
|
Lock a flow graph in preparation for reconfiguration. | |||
unlock()
|
Unlock a flow graph in preparation for reconfiguration. When an equal number of calls to lock() and unlock() have occurred, the flow graph will be restarted automatically. |
See the documentation for gr::top_block
for more details.
Example:
class my_top_block(gr.top_block): def __init__(self): gr.top_block.__init__(self) ... # Define blocks etc. here if __name__ == '__main__': my_top_block().start() sleep(5) # Wait 5 secs (assuming sleep was imported!) my_top_block().stop() my_top_block().wait() # If the graph is needed to run again, wait() must be called after stop ... # Reconfigure the graph or modify it my_top_block().start() # start it again sleep(5) # Wait 5 secs (assuming sleep was imported!) my_top_block().stop() # since (assuming) the graph will not run again, no need for wait() to be called
These methods help you to control the flow graph from the outside. For many problems this might not be enough: you don't simply want to start or stop a flow graph, you want to reconfigure the way it behaves. For example, imagine your application has a volume control somewhere in your flow graph. This volume control is implemented by inserting a multiplier into the sample stream. This multiplier is of type blocks.multiply_const_ff
. If you check the documentation for this kind of of block, you will find a function blocks.multiply_const_ff.set_k()
which sets the multiplication factor.
You need to make the settings visible to the outside in order to control it. The simplest way is to make the block an attribute of the flow graph class.
Example:
class my_top_block(gr.top_block): def __init__(self): gr.top_block.__init__(self) ... # Define some blocks self.amp = blocks.multiply_const_ff(1) # Define multiplier block ... # Define more blocks self.connect(..., self.amp, ...) # Connect all blocks def set_volume(self, volume): self.amp.set_k(volume) if __name__ == '__main__': my_top_block().start() sleep(2) # Wait 2 secs (assuming sleep was imported!) my_top_block.set_volume(2) # Pump up the volume (by factor 2) sleep(2) # Wait 2 secs (assuming sleep was imported!) my_top_block().stop()
This example runs the flow graph for 2 seconds and then doubles the volume by accessing the amp
block through a member function called set_volume()
. Of course, one could have accessed the amp
attribute directly, omitting the member function.
Hint: making blocks attributes of the flow graph is generally a good idea as it makes extending the flow graph with extra member functions easier.
Non-flow graph centered applications
Up until now, GNU Radio applications in this tutorial have always been centered around the one class derived from gr.top_block
. However, this is not necessarily how GNU Radio needs to be used. GNU Radio was designed to develop DSP applications from Python, so there's no reason to not use the full power of Python when using GNU Radio.
Python is an extremely powerful language, and new libraries and functionalities are constantly being added. In a way, GNU Radio extends Python with a powerful, real-time-capable DSP library. By combining this with other libraries you have immense functionality right there at your fingertips. For example, by combining GNU Radio with SciPy, a collection of scientific Python libraries, you can record RF signals in real time and do extensive mathematical operations off line, save statistics to a database and so on - all in the same application. Even expensive engineering software such as Matlab might become unnecessary if you combine all these libraries.
Advanced Topics
If you have really read the previous sections, you already know enough to write your first Python GNU Radio applications. This section will address some slightly more advanced functionalities for Python GNU Radio applications.
Dynamic flow graph creation
For most cases, the aforementioned way to define flow graphs is completely adequate. If you need more flexibility in your application, you might want to have even more control over the flow graph from outside the class.
This can be achieved by taking the code out of the +init+()
function and simply using gr.top_block
as a container. Example:
... # We are inside some application tb = gr.top_block() # Define the container block1 = blocks.some_other_block() block2 = blocks.yet_another_block() tb.connect(block1, block2) ... # The application does some wonderful things here tb.start() # Start the flow graph ... # Do some more incredible and fascinating stuff here
If you are writing some application which needs to dynamically stop a flow graph (reconfigure it, re-start it and so) on this might be a more practical way to do it.
Examples for this kind of flow graph setup:
gr-uhd/apps/hf_explorer/hfx.py
Command Line Options
Python has its own libraries to parse command line options. See the documentation for the module optparse
to find out how to use it.
GNU Radio extends optparse by new command line option types. Use @from gnuradio.eng_option import eng_option' to import this extension. With eng_option, you have the following types:
eng_float | Like the original float option, but also accepts engineering notation like 101.8M | |||
subdev | Only accepts valid subdevice descriptors such as A:0 (To specify a daughterboard on a USRP) | |||
intx | Only accepts integers |
If your application supports command line options, it would be ever so nice if you could stick to the GNU Radio conventions for command line options. You can find these (along with more hints for developers) in README.hacking.
Nearly every GNU Radio example uses this feature. Try dial_tone.py for an easy example.
Graphical User Interfaces
If you are a Python expert and also have some experience in writing GUIs for Python (using whatever GUI toolkit you like), you might not even need this section. As mentioned before, GNU Radio merely extends Python with DSP routines - so if you like, just go ahead and write a GUI application, add a GNU Radio flow graph to it and define some interfaces to carry GNU Radio information to your application and vice versa. If you want to plot your data, you could use Matplotlib or Qwt.
However, sometimes you simply want to write a quick GUI application without bothering with setting up widgets, defining all the menus etc. GNU Radio comes with some predefined classes to help you write graphical GNU Radio applications.
These modules are based on wxWidgets (or to be precise, wxPython), a platform-independent GUI toolkit. You will need some background in wxPython - but don't worry, it is not that complicated and there are several tutorials available on the net. Check the wxPython website for documentation (http://www.wxpython.org/).
To use the GNU Radio wxWidgets tools, you need to import some modules:
from gnuradio.wxgui import stdgui2, fftsink2, slider, form
Here, 4 components were imported from the gnuradio.wxgui submodule. Here's a quick list of the modules (again, not necessarily complete. You will have to browse the modules or the source code in gr-wxgui/python).
stdgui2 | Basic GUI stuff, you always need this | |||
fftsink2 | Plot FFTs of your data to create spectrum analyzers or whatever | |||
scopesink2 | Oscilloscope output | |||
waterfallsink2 | Waterfall output | |||
numbersink2 | Displays numerical values of incoming data | |||
form | Often used input forms (see below) |
Next, we have to define a new flow graph. This time, we don't derive from gr.top_block
but from stdgui2.std_top_block
:
class my_gui_flow_graph(stdgui2.std_top_block): def __init__(self, frame, panel, vbox, argv): stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
As you can see, there's another difference: the constructor gets a couple of new parameters. This is because a stdgui2.std_top_block
does not only include flow graph functionality (it is derived from gr.top_block itself), but also directly creates a window with some basic components (like a menu). This is good news for all of those who just want to quickly hack a graphical application: GNU Radio creates the window and everything, you just need to add the widgets. Here's a list of what you can do with these new objects (this probably won't mean much to you if you have no idea about GUI programming):
frame | The wx.Frame of your window. You can get at the predefined menu by using frame.GetMenuBar() | |||
panel | A panel, placed in @frame', to hold all your wxControl widgets | |||
vbox | A vertical box sizer (wx.BoxSizer(wx.VERTICAL) is how it is defined), used to align your widgets in the panel | |||
argv | The command line arguments |
Now you have all you need to create your GUI. You can simply add new box sizers and widgets to vbox, change the menu or whatever. Some typical functions have been simplified further in the GNU Radio GUI library form
.
form has a great number of input widgets: form.static_text_field()
for static text field (display only), form.float_field()
, to input float values, form.text_field()
to input text, form.checkbox_field()
for checkboxes, form.radiobox_field()
for radioboxes etc. Check the source code of gr-wxgui/python/form.py for the complete list. Most of these calls pass most of their arguments to the appropriate wxPython objects, so the function arguments are quite self-explanatory.
See one of the examples mentioned below on how to add widgets using form.
Probably the most useful part of gnuradio.wxgui
is the possibility to directly plot incoming data. To do this, you need one of the sinks that come with gnuradio.wxgui
, such as fftsink2
. These sinks work just as any other GNU Radio sink, but also have properties needed for use with wxPython. Example:
from gnuradio.wxgui import stdgui2, fftsink2 # App gets defined here ... # FFT display (pseudo-spectrum analyzer) my_fft = fftsink2.fft_sink_f(panel, title="FFT of some Signal", fft_size=512, sample_rate=sample_rate, ref_level=0, y_per_div=20) self.connect(source_block, my_fft) vbox.Add(my_fft.win, 1, wx.EXPAND)
First, the block is defined (fftsink2.fft_sink_f
). Apart from typical DSP parameters such as the sampling rate, it also needs the panel
object which is passed to the constructor. Next, the block is connected to a source. Finally, the FFT window (my_fft.win
) is placed inside the vbox
BoxSizer to actually display it. Remember that a signal block output can be connected to any amount of inputs.
Finally, the whole thing needs to be started. Because we need an wx.App()
to run the GUI, the start-up code is a bit different from a regular flow graph:
if __name__ == '__main__': app = stdgui2.stdapp(my_gui_flow_graph, "GUI GNU Radio Application") app.MainLoop()
stdgui2.stdapp()
creates the wx.App
with my_gui_flow_graph
(the first argument). The window title is set to "GUI GNU Radio Application".
Examples for simple GNU Radio GUIs:
gr-uhd/apps/uhd_fft gr-audio/examples/python/audio_fft.py ./gr-uhd/examples/python/usrp_am_mw_rcv.py
And many more.
What next?
Young Padawan, no more there is I can teach you. If you have any more questions on how to write GNU Radio applications in Python, there are still a number of resources you can use:
- Use the source. Especially the examples in gr-<component>/examples/ and gr-utils/ can be very helpful.
- Check the mailing list archives. Chances are very high your problem has been asked before.