GR4 tutorial: flowgraphs 101: Difference between revisions

From GNU Radio
Jump to navigation Jump to search
(Created page with "This tutorial explains the basics of C++ flowgraphs in GNU Radio 4.0. == Minimal flowgraph == The following is an example of a minimal flowgraph. It connects a Null Source to a Null Sink and runs the flowgraph forever. The code for the flowgraph can be found in [https://github.com/daniestevez/gr4-packet-modem/blob/main/examples/minimal_flowgraph.cpp minimal_flowgraph.cpp]. <syntaxhighlight lang="cpp" line> #include <fmt/core.h> #include <gnuradio-4.0/Graph.hpp> #inclu...")
 
No edit summary
Line 33: Line 33:
Creating a flowgraph in GNU Radio 4.0 begins with the instantiation of a <code>gr::Graph</code> object.
Creating a flowgraph in GNU Radio 4.0 begins with the instantiation of a <code>gr::Graph</code> object.


The <code>emplaceBlock()</code> method of the <code>gr::Graph</code> is used to add blocks to the flowgraph. Note that this method takes a template parameter which specifies the block class. The block classes can be templates, and they often have template parameters to indicate the input and output types. Here we are creating a <code>NullSource<int></code>, which has <code>int</code> as output type, and similarly for the null sink. As in GNU Radio 3.10, blocks can get parameters when they are created. These are passed as a <code>gr::property_map</code> argument to <code>emplaceBlock()</code>. In this case, the null source and null sink don't take any parameters, so <code>emplaceBlock()</code> is called without arguments. We will see an example of blocks with parameters below.
The <code>emplaceBlock()</code> method of the <code>gr::Graph</code> is used to add blocks to the flowgraph. Note that this method takes a template parameter which specifies the block class. The block classes can be templates, and they often have template parameters to indicate the input and output types. Here we are creating a <code>NullSource<int></code>, which has <code>int</code> as output type, and similarly for the null sink. As in GNU Radio 3.10, blocks can get parameters when they are created. These are passed as a <code>gr::property_map</code> argument to <code>emplaceBlock()</code>. In this case, the Null Source and Null Sink don't take any parameters, so <code>emplaceBlock()</code> is called without arguments. We will see an example of blocks with parameters below.


A connection between blocks is done by calling the <code>connect()</code> method of the <code>gr::Graph</code>. Note how the syntax works, using template parameters to specify the name of the ports. In GNU Radio 3.10, block ports have numbers 0, 1, 2, etc. These numbers are specified when calling <code>connect</code>, and 0 is implicit if the number is omitted. In GNU Radio 4.0, all the ports have names which are a string. In simple blocks which only have an input or an output, <code>"in"</code> and <code>"out"</code> are often used as port names. The <code>connect()</code> method checks that the connection is valid (the types of the ports match, etc.) and returns a <code>gr::ConnectionResult</code> value indicating if the connection could be made.
A connection between blocks is done by calling the <code>connect()</code> method of the <code>gr::Graph</code>. Note how the syntax works, using template parameters to specify the name of the ports. In GNU Radio 3.10, block ports have numbers 0, 1, 2, etc. These numbers are specified when calling <code>connect</code>, and 0 is implicit if the number is omitted. In GNU Radio 4.0, all the ports have names which are a string. In simple blocks which only have an input or an output, <code>"in"</code> and <code>"out"</code> are often used as port names. The <code>connect()</code> method checks that the connection is valid (the types of the ports match, etc.) and returns a <code>gr::ConnectionResult</code> value indicating if the connection could be made.
Line 40: Line 40:


After the scheduler is created, its <code>runAndWait()</code> method is called. This is equivalent to the GNU Radio 3.10 <code>run()</code> method. It runs the flowgraph until the flowgraph finishes on its own (because the end of an input file has been reached, the limit on a [[Head]] block has been hit, etc.) The <code>runAndWait()</code> method also returns if there is an error, such as a block raising an unhandled exception. The method returns a [https://en.cppreference.com/w/cpp/utility/expected std::expected], so the <code>has_value()</code> method can be used to check if there was an error.
After the scheduler is created, its <code>runAndWait()</code> method is called. This is equivalent to the GNU Radio 3.10 <code>run()</code> method. It runs the flowgraph until the flowgraph finishes on its own (because the end of an input file has been reached, the limit on a [[Head]] block has been hit, etc.) The <code>runAndWait()</code> method also returns if there is an error, such as a block raising an unhandled exception. The method returns a [https://en.cppreference.com/w/cpp/utility/expected std::expected], so the <code>has_value()</code> method can be used to check if there was an error.
== Block parameters ==
To show how block parameters can be passed when calling <code>emplaceBlock()</code>, we will add a Head block between the Null Source and the Null Sink in the flowgraph above. The relevant lines of code are the following. The complete example is in [https://github.com/daniestevez/gr4-packet-modem/blob/main/examples/head.cpp head.cpp].
<syntaxhighlight lang="cpp" line>
    auto& source = fg.emplaceBlock<gr::packet_modem::NullSource<int>>();
    auto& head =
        fg.emplaceBlock<gr::packet_modem::Head<int>>({ { "num_items", 1000UL } });
    auto& sink = fg.emplaceBlock<gr::packet_modem::NullSink<int>>();
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(source).to<"in">(head)));
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(head).to<"in">(sink)));
</syntaxhighlight>
The argument of the <code>emplaceBlock()</code> method is a <code>gr::property_map</code>, which is defined as <code>std::map<std::string, pmtv::pmt></code>. The [https://github.com/gnuradio/pmt pmtv] library is a replacement for the [[Polymorphic Types (PMTs)]] in GNU Radio 3.10 using modern C++ features. All the simple C++ types that you would expect can be converted to and from a <code>pmtv::pmt</code>. When calling <code>emplaceBlock()</code>, it is often convenient to create the <code>gr::property_map</code> by using C++ initializer lists, as is done in this example. Here the initializer list is <code>{ { "num_items", 1000UL } }</code>. Several parameters of different types can also be passed quite naturally, such as for example:
<syntaxhighlight lang="cpp">
{ { "foo", "var" }, { "amplitude", 37.1f }, { "samples", std::vector<double>{ 1.3, 2.5, -4.7 } } }
</syntaxhighlight>
'''Warning:''' This is the only way of passing parameters to a block on construction. Unlike in GNU Radio 3.10, it is not possible to have a C++ constructor that takes a list of arguments of arbitrary C++ types.
A natural question is how to find the parameters that a block has. We can check the C++ source for the block (here [https://github.com/daniestevez/gr4-packet-modem/blob/main/blocks/include/gnuradio-4.0/packet-modem/head.hpp head.hpp]). The parameters that can be set with a <code>gr::property_map</code> are declared in the reflection macro, which in this case is:
<syntaxhighlight lang="cpp">
ENABLE_REFLECTION_FOR_TEMPLATE(gr::packet_modem::Head, in, out, num_items);
</syntaxhighlight>
Note that the reflection macro also declares all the ports of the block. The parameters must be public class members. We can look at the class declaration to find their types. In this case we have the following:
<syntaxhighlight lang="cpp">
    gr::PortIn<T> in;
    gr::PortOut<T> out;
    uint64_t num_items;
</syntaxhighlight>
'''Warning:''' types are important when constructing the <code>gr::property_map</code> used to pass parameters to the block. In this case, using <code>{ { "num_items", 1000 } }</code> would cause a runtime error, because <code>1000</code> is an <code>int</code>, but the block expects <code>num_items</code> to be a <code>uint64_t</code>. Beware of numeric literals in C++. Often the type needs to be specified explicitly for these.
Another thing to note from this example is that connections are always done between two blocks. Unlike in GNU Radio 3.10, there is no shorthand notation to string connections through a list of multiple blocks.

Revision as of 09:47, 23 August 2024

This tutorial explains the basics of C++ flowgraphs in GNU Radio 4.0.

Minimal flowgraph

The following is an example of a minimal flowgraph. It connects a Null Source to a Null Sink and runs the flowgraph forever. The code for the flowgraph can be found in minimal_flowgraph.cpp.

#include <fmt/core.h>
#include <gnuradio-4.0/Graph.hpp>
#include <gnuradio-4.0/Scheduler.hpp>
#include <gnuradio-4.0/packet-modem/null_sink.hpp>
#include <gnuradio-4.0/packet-modem/null_source.hpp>
#include <boost/ut.hpp>

int main()
{
    using namespace boost::ut;

    gr::Graph fg;
    auto& source = fg.emplaceBlock<gr::packet_modem::NullSource<int>>();
    auto& sink = fg.emplaceBlock<gr::packet_modem::NullSink<int>>();
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(source).to<"in">(sink)));

    gr::scheduler::Simple sched{ std::move(fg) };
    expect(sched.runAndWait().has_value());

    return 0;
}

Here the boost-ut expect() function is used for error handling, since it is also used for unit testing in GNU Radio 4.0, but any other form of error handling can be used.

Creating a flowgraph in GNU Radio 4.0 begins with the instantiation of a gr::Graph object.

The emplaceBlock() method of the gr::Graph is used to add blocks to the flowgraph. Note that this method takes a template parameter which specifies the block class. The block classes can be templates, and they often have template parameters to indicate the input and output types. Here we are creating a NullSource<int>, which has int as output type, and similarly for the null sink. As in GNU Radio 3.10, blocks can get parameters when they are created. These are passed as a gr::property_map argument to emplaceBlock(). In this case, the Null Source and Null Sink don't take any parameters, so emplaceBlock() is called without arguments. We will see an example of blocks with parameters below.

A connection between blocks is done by calling the connect() method of the gr::Graph. Note how the syntax works, using template parameters to specify the name of the ports. In GNU Radio 3.10, block ports have numbers 0, 1, 2, etc. These numbers are specified when calling connect, and 0 is implicit if the number is omitted. In GNU Radio 4.0, all the ports have names which are a string. In simple blocks which only have an input or an output, "in" and "out" are often used as port names. The connect() method checks that the connection is valid (the types of the ports match, etc.) and returns a gr::ConnectionResult value indicating if the connection could be made.

Once all the blocks have been added to the flowgraph and connected, the flowgraph can be run. To do this, it is first necessary to put the flowgraph in a scheduler. GNU Radio 3.10 has a single scheduler implemented in the runtime. In GNU Radio 4.0, schedulers are modular and users can implement their own, so any scheduler could be used here. In this example we use the gr::Scheduler::Simple scheduler, which is included in the GNU Radio 4.0 core. By default it is a single-threaded scheduler, meaning that it runs all of the blocks in the same thread (except for dedicated IO threads). It can also be instantiated as a multi-threaded scheduler that uses a one-thread-per-block approach similar to GNU Radio 3.10.

After the scheduler is created, its runAndWait() method is called. This is equivalent to the GNU Radio 3.10 run() method. It runs the flowgraph until the flowgraph finishes on its own (because the end of an input file has been reached, the limit on a Head block has been hit, etc.) The runAndWait() method also returns if there is an error, such as a block raising an unhandled exception. The method returns a std::expected, so the has_value() method can be used to check if there was an error.

Block parameters

To show how block parameters can be passed when calling emplaceBlock(), we will add a Head block between the Null Source and the Null Sink in the flowgraph above. The relevant lines of code are the following. The complete example is in head.cpp.

    auto& source = fg.emplaceBlock<gr::packet_modem::NullSource<int>>();
    auto& head =
        fg.emplaceBlock<gr::packet_modem::Head<int>>({ { "num_items", 1000UL } });
    auto& sink = fg.emplaceBlock<gr::packet_modem::NullSink<int>>();
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(source).to<"in">(head)));
    expect(eq(gr::ConnectionResult::SUCCESS, fg.connect<"out">(head).to<"in">(sink)));

The argument of the emplaceBlock() method is a gr::property_map, which is defined as std::map<std::string, pmtv::pmt>. The pmtv library is a replacement for the Polymorphic Types (PMTs) in GNU Radio 3.10 using modern C++ features. All the simple C++ types that you would expect can be converted to and from a pmtv::pmt. When calling emplaceBlock(), it is often convenient to create the gr::property_map by using C++ initializer lists, as is done in this example. Here the initializer list is { { "num_items", 1000UL } }. Several parameters of different types can also be passed quite naturally, such as for example:

{ { "foo", "var" }, { "amplitude", 37.1f }, { "samples", std::vector<double>{ 1.3, 2.5, -4.7 } } }

Warning: This is the only way of passing parameters to a block on construction. Unlike in GNU Radio 3.10, it is not possible to have a C++ constructor that takes a list of arguments of arbitrary C++ types.

A natural question is how to find the parameters that a block has. We can check the C++ source for the block (here head.hpp). The parameters that can be set with a gr::property_map are declared in the reflection macro, which in this case is:

ENABLE_REFLECTION_FOR_TEMPLATE(gr::packet_modem::Head, in, out, num_items);

Note that the reflection macro also declares all the ports of the block. The parameters must be public class members. We can look at the class declaration to find their types. In this case we have the following:

    gr::PortIn<T> in;
    gr::PortOut<T> out;
    uint64_t num_items;

Warning: types are important when constructing the gr::property_map used to pass parameters to the block. In this case, using { { "num_items", 1000 } } would cause a runtime error, because 1000 is an int, but the block expects num_items to be a uint64_t. Beware of numeric literals in C++. Often the type needs to be specified explicitly for these.

Another thing to note from this example is that connections are always done between two blocks. Unlike in GNU Radio 3.10, there is no shorthand notation to string connections through a list of multiple blocks.