When building software defined radio flowgraphs it’s common to want to inspect the live data as it flows through various stages. In GNU Radio, and RustRadio, the handoff points are very clear. They are the streams between the blocks.

In GNU Radio you can stop the graph, click a bit in gnuradio-companion, and have a new time sink (or whatever you need) added, and then start the graph up again. But it makes for a lot of stopping and starting. There’s also the drawback that it assumes that the GNU Radio flowgraph is graphical in the first place, and not running headless on a raspberry pi somewhere.

And for large graphs, preemptively adding GUI elements for everything you may possibly want to read at runtime can clutter things up, and waste CPU processing and rendering data you’re not interested in.

Another use case is if you have a constantly running RF sniff process using an SDR, to get a second reader of the same data, to help develop the next version without shutting down the one already running.

So clearly, what you want is to be able to connect to the flowgraph over the network, and start streaming any and all streams in the flowgraph.

First implementation: RustRadio

I showed a screenshot of this in my previous post. After adding the ability to sniff streams it looks about the same, but instead of needing to tee off every stream that may be interesting, and wasting the CPU resources when the UI is not connected, I’ve now changed it to sniff the streams by name.

RustRadio has a pretty easy way to sniff the streams by name using .with_…, and add other stream metadata. E.g. this is most of the receiver for my IKEA sparsnäs decoder:

    let prev = blockchain![
        graph,
        prev,
        FftFilter::new(
            prev,
            rustradio::fir::low_pass_complex(samp_rate, 50000.0, 10000.0, &WindowType::Hamming)?
        )?,
        RationalResampler::new(prev, f32_to_usize(samp_rate_2)?, f32_to_usize(samp_rate)?)?,
        QuadratureDemod::new(
            prev.with_name("filtered").with_sample_rate(samp_rate_2)?,
            1.0
        )?,
        AddConst::new(prev, opt.offset)?,
        ZeroCrossing::new(
            prev.with_name("demodulated")
                .with_sample_rate(samp_rate_2)?,
            samp_rate_2 / baud,
            0.1
        )?,
        BinarySlicer::new(prev.with_name("soft-bits").with_sample_rate(baud)?)?,
    ]
    .with_name("bits")
    .with_sample_rate(baud)?;

But I’m not restricted to using a fixed UI. The RustRadio UI crate has an example UI application that can connect to anything, and lets you show a waterfall of any I/Q stream.

I/Q waterfall with selector

Second implementation: GNU Radio

When I told a friend of mine about this he thought it was very cool. Then he got disappointed when I said it was just for RustRadio, not GNU Radio.

Well, let’s see if an LLM can make it happen. Yup, a few tokens later it works on GNU Radio too.

Now, the code is probably not suitable for merging at all, but it’s just a demo. It’s fine if it’s throwaway code.

Simple GNU radio graph

Note the “IQ Stream Server” block, which can be used for dedicated sinks, and can also “import” all streams in the whole graph.

My demo does not let me add the true sample rate, though, so imported streams get a default fake sample rate.

I can query the available streams at runtime:

$ curl -s http://127.0.0.1:50052/iq/v1/streams | jq  '.[] | .name'
"gr/add_cc/5/0/3"
"gr/analog_sig_source_x_0 [this is the source]/10/0/1"
"gr/blocks_throttle2_0 [throttled]/9/0/2"
"gr/fastnoise_source/6/0/8"
"gr/fir_filter_blk<IN_T,OUT_T,TAP_T>/4/0/5"
"gr/mmse_resampler/3/0/4"
"gr/multiply_cc/8/0/7"
"gr/sig_source/7/0/6"

And I can use the same example I/Q waterfall UI to plot waterfalls from this graph:

Waterfall from the GNU Radio graph

There are more streams in that dropdown than there are blocks. That’s because the Channel Model block is actually a GNU Radio Python hierarchical block, and we’re seeing all the underlying blocks’ streams.

Video demo:

Code

This is built into RustRadio, with the WASM-based UI code in rustradio UI.

The vibecoded changes to GNU Radio are in this hooks branch, and then the IQ streaming blocks using the hooks in the hooks branch of gr-iqstream.