Guided Tutorial Programming Topics: Difference between revisions
No edit summary |
|||
Line 35: | Line 35: | ||
== Stream Tags == | == Stream Tags == | ||
<merged with Stream Tags usage manual page> | <merged with Stream Tags usage manual page> |
Revision as of 20:18, 13 December 2020
Tags, Messages and PMTs.
Objectives
- Learn about PMTs
- Understand what tags are / what they do / when to use them
- Understand difference between streaming and message passing
- Point to the manual for more advanced block manipulation
Prerequisites
- General familiarity with C++ and Python
- Tutorials:
So far, we have only discussed data streaming from one block to another. The data often consists of samples, and a streaming architecture makes a lot of sense for those. For example, a sound card driver block will constantly produce audio samples once active.
In some cases, we don't want to pipe a stream of samples, though, but rather pass individual messages to another block, such as "this is the first sample of a burst", or "change the transmit frequency to 144 MHz". Or consider a MAC layer on top of a PHY: At higher communication levels, data is usually passed around in PDUs (protocol data units) instead of streams of items.
In GNU Radio we have two mechanisms to pass these messages:
- Synchronously to a data stream, using stream tags
- Asynchronously, using the message passing interface
Before we discuss these, let's consider what such a message is from a programming perspective. It could be a string, a vector of items, a dictionary... anything, really, that can be represented as a data type. In Python, this would not be a problem, since it is weakly typed, and a message variable could simply be assigned whatever we need. C++ on the other hand is strongly typed, and it is not possible to create a variable without knowing its type. What makes things harder is that we need to be able to share the same data objects between Python and C++. To circumvent this problem, we introduce polymorphic types (PMTs).
Polymorphic Types (PMT)
<content merged with PMT page in usage manual>
OK, so now we know all about creating messages - but how do we send them from block to block?
Stream Tags
<merged with Stream Tags usage manual page>
Message Passing
The message passing interface works completely differently than the stream tags. Messages are pure PMTs and they have no offset (this would not make any sense anyway, as they are not connected to an item, which in turn has an offset) and no keys (although we can create key/value pairs by making the PMT a pair or a dictionary). Another difference is that we have a different type of port for messages, which can't be sent to regular streaming ports. We call these new types of ports message ports, as opposed to the kind we've used so far, which we shall call streaming ports if we need to be explicit.
Messages usually aren't used for samples, but rather for packets or data that makes sense to be moved about in a packetized fashion. Of course, there are many cases where both message passing and streaming interfaces can be used, and it's up to us to choose which we prefer.
Here's a simple example of a flow graph using both streaming and messages:
There are several interesting things to point out. First, there are two source blocks, which both output items at regular intervals, one every 1000 and one every 750 milliseconds. Dotted lines denote connected message ports, as opposed to solid lines, which denote connected streaming ports. In the top half of the flow graph, we can see that it is, in fact, possible to switch between message passing and streaming ports, but only if the type of the PMTs matches the type of the streaming ports (in this example, the pink color of the streaming ports denotes bytes, which means the PMT should be a u8vector if we want to stream the same data we sent as PMT).
Another interesting fact is that we can connect more than one message output port to a single message input port, which is not possible with streaming ports. This is due to the asynchronous nature of messages: The receiving block will process all messages whenever it has a chance to do so, and not necessarily in any specific order. Receiving messages from multiple blocks simply means that there might be more messages to process.
What happens to a message once it was posted to a block? This depends on the actual block implementation, but there are two possibilities:
1) A message handler is called, which processes the message immediately.
2) The message is written to a FIFO buffer, and the block can make use of it whenever it likes, usually in the work function.
For a block that has both message ports and streaming ports, any of these two options is OK, depending on the application. However, we strongly discourage the processing of messages inside of a work function and instead recommend the use of message handlers. Using messages in the work function encourages us to block in work waiting for a message to arrive. This is bad behavior for a work function, which should never block. If a block depends upon a message to operate, use the message handler concept to receive the message, which may then be used to inform the block's actions when the work function is called. Only on specially, well-identified occasions should we use method 2 above in a block.
With a message passing interface, we can write blocks that don't have streaming ports, and then the work function becomes useless, since it's a function that is designed to work on streaming items. In fact, blocks that don't have streaming ports usually don't even have a work function.
PDUs
In the previous flow graph, we have a block called PDU to Tagged Stream. A PDU (protocol data unit) in GNU Radio has a special PMT type, it is a pair of a dictionary (on CAR) and a uniform vector type. So, this would yield a valid PDU, with no metadata and 10 zeros as stream data:
pdu = pmt.cons(pmt.make_dict(), pmt.make_u8vector(10, 0))
The key/value pairs in the dictionary are then interpreted as key/value pairs of stream tags.
Adding Message Passing to the Code
Unlike streaming ports, message ports are not defined in the I/O signature, but are declared with the message_port_register_*
functions. Message ports also have identifiers instead of port numbers; those identifiers are also PMTs. To add in- or output ports for messages to a block, the constructor must be amended by lines like these:
// Put this into the constructor to create message ports message_port_register_in(pmt::mp("in_port_name")); message_port_register_out(pmt::mp("out_port_name"));
To set up a function as the message handler for an input message port that is called every time we receive a message, we add line like this:
// Put this into the constructor after the input port definition: set_msg_handler( pmt::mp("in_port_name"), // This is the port identifier boost::bind(&block_class_name::msg_handler_method, this, _1) // [FIXME class name] Bind the class method );
We use Boost's bind
technique to tell GNU Radio about the function we want to call for messages on this input port. If you don't know boost::bind
, don't worry, the syntax is always the same: The first argument is a pointer to the method that is called, the second is this
so the method is called from the correct class and the third argument is _1
, which means the function takes one argument, which is the message PMT.
All message handlers, like the function "msg_handler_method" used above, have the same function prototype and are members of the block's implementation class:
void msg_handler_method(pmt::pmt_t msg);
In a Python block, these lines would look like this:
# Put this into the constructor to create message ports self.message_port_register_in(pmt.intern("in_port_name")) self.message_port_register_out(pmt.intern("out_port_name")) self.set_msg_handler(pmt.intern("in_port_name"), self.msg_handler_method) # No bind necessary, we can pass the function directly
Message inputs and outputs can be connected similarly to streaming ports only using msg_connect
instead of connect
:
tb = gr.top_block() # Send the string "message" once every 1000 ms src = blocks.message_strobe(pmt.to_pmt("message"), 1000) dbg = blocks.message_debug() tb.msg_connect(src, "pdus", dbg, "print")
Note the msg_connect()
call instead of the connect()
function we use for streaming ports.
Example: Chat Application
Let's build an application that uses message passing. A chat program is an ideal use case, since it waits for the user to type a message, and then sends it. Because of that, no Throttle block is needed.
Create the following flowgraph and save it as 'chat_app2.grc':
The ZMQ Message blocks have an Address of 'tcp://127.0.0.1:50261'. Typing in the QT GUI Message Edit Box will send the text once the Enter key is pressed. Output is on the terminal screen where gnuradio-companion was started.
If you want to talk to another user (instead of just yourself), you can create an additional flowgraph with a different name such as 'chat_app3.grc'. Then change the ZMQ port numbers as follows:
- chat_app2
- ZMQ PUSH Sink: tcp://127.0.0.1:50261
- ZMQ PULL Source: tcp://127.0.0.1:50262
- chat_app3
- ZMQ PUSH Sink: tcp://127.0.0.1:50262
- ZMQ PULL Source: tcp://127.0.0.1:50261
When using GRC, doing a Generate and/or Run creates a Python file with the same name as the .grc file. You can execute the Python file without running GRC again.
For testing this system we will use two processes, so we will need two terminal windows.
Terminal 1:
- since you just finished building the chat_app3 flowgraph, you can just do a Run.
Terminal 2: Open another terminal window.
- change to whatever directory you used to generate the flowgraph for chat_app2.
- execute the following command:
python3 -u chat_app2.py
Typing in the Message Edit Box for chat_app2 should be displayed on the Terminal 1 screen (chat_app3) and vice versa.
To terminate each of the processes cleanly, click on the 'X' in the upper corner of the GUI rather than using Control-C.