RustRadio, and Roast My Rust
I’m learning Rust. And I like playing with software defined radio (SDR). So the natural project to take on to learn Rust is to write a crate for making SDR applications. I call it RustRadio.
I have something that works, and seems pretty OK. But before marking a 1.0.0 release I want to see if I can get some opinions on my use of the Rust language. Both in terms of design, and more clippy-like suggestions.
Hence: Roast My Rust. File a github issue, email me, or tweet at me. Tell me I’m doing it wrong.
- RustRadio code: https://github.com/ThomasHabets/rustradio
- RustRadio docs: https://docs.rs/rustradio/latest/rustradio/
- The first application: https://github.com/ThomasHabets/sparslog
What my priorities are
There are two API surfaces in RustRadio; the Block API (for writing blocks), and the Application API (for writing applications that use blocks). I want them to be good, and future proof, so that I don’t have to change every block and every application, after adding a feature or improving the API.
The blocks will need to be thread safe, even though the scheduler is currently single threaded.
For the streams between blocks I’ll eventually want to make a more
fancy, but unsafe
circular buffer, that hands out readable or
writable slices. But that should all be hideable behind the API that
currently just locks the whole stream.
Things I’m currently not happy with
Block API
-
Using a macro (e.g. in
AddConst
) for one-in-one-out blocks seems like it’s a workaround, instead of getting something nice to work using traits directly. But I’ve not gotten a default trait implementation to work, to addwork()
,out()
, andblock_name()
. -
Max stream output capacity is not enforced, or even respected. I’m thinking that there may be a nice trade-off where a block can hand off generated output samples, that don’t fit, onto the framework, instead of needing to duplicate the buffering in all blocks that need it. Once multithreaded they can also just block.
-
Once multithreaded, it’s tricky for a block to correctly handle its input stream growing while being read. Maybe remove the ‘.clear()
method, and it'll be up to the block to get the length before calling
iter(), doing it all under a lock, or mandating a
take(n)` after checking length?
Application API
- Also here I’ve resorted to a macro, where surely a non-macro API improvement could do it? It’s complicated by the fact that blocks have different number and types of stream. Maybe the block could return a stream-provider block? But that sounds like it could mean a lot of boilerplate for the Block API.
Future work, that I think should fit nicely
-
Blocks should, in addition to providing the output streams, provide an
mpsc
channel to update settings. -
A block that acts as the UI, by spinning up a webserver.
IKEA Sparsnäs logger
In addition to the “hello world” of SDR, the FM receiver included in RustRadio as an example, I’ve also made an IKEA Sparsnäs decoder.
IKEA Sparsnäs is a light sensor that you stick to your electricity meter’s blinking light, that sends electricity consumption data every 15 seconds on 868Mhz in a very easy to decode protocol.
I wrote a logger, and it works great.
I can receive it anywhere in my house without problems. If you can get hold of an IKEA Sparsnäs then I highly recommend it.
Two alternate implementations
I actually have three proof of concept implementations. One that is more block oriented, like GNU Radio. That’s the main one.
Another branch is iterator based. I don’t think it’ll work, because of how it would block with locks held, when one tries to read from an empty stream. Even if I started messing with condition variables and such, Rust’s borrow checker would probably (rightly) get in the way, and I think it would end up as a big mess.
A second alternative implementation is stream based, to go
async
. It has a higher chance of working than iterator based, but
would likely be much more complex than standard thread based one.
async
’s great selling point is if you need to
scale to hundreds or thousands of “tasks”, or many I/O bound
tasks. But radio flow graphs will mostly have blocks in the double
digits. And the blocks will be CPU bound. So the task switching
overhead is not there as much.
There’s also plenty of criticism of async Rust, e.g. Async Rust Is A Bad Language.
So while these alternative implementations may be interesting, they’re not the ones I’m aiming for for 1.0.
Maybe the Streams based one can be 2.0, if it turns out I’m wrong…
Resources I’ve found helpful for learning Rust
- https://doc.rust-lang.org/book/
- https://google.github.io/comprehensive-rust
- https://www.lurklurk.org/effective-rust/
- All books listed on https://lborb.github.io/book/official.html
- And sometimes ChatGPT and Bard. They are both pretty bad at answering the actual question, or even writing code that compiles, but they will sometimes spit out syntax or libraries that a beginner is not aware of.
RP-SMA detour
I was stumped briefly about why sparslog was running fine on my laptop, but decoded very poorly on a raspberry pi and a RISC-V VisionFive 2 board, only occasionally decoding a packet.
At first I thought it could be due to the architecture, or problems
with my interfacing with librtlsdr
, dropping samples. But then it
turned out to be a hardware bug. The 868Mhz antennas I bought were
RP-SMA, not SMA. No wonder it had a hard time decoding; the center pin
of the antenna was not connected!
RP-SMA should be illegal. It’s so stupid. It was made “wrong” for devices where you’re not supposed to change the antenna to a better one, but of course you can just buy an RP-SMA antenna, or an RP-SMA/SMA adapter.
But now we’re stuck with two extremely similar but physically incompatible standards. In my case it just meant that the antenna was disconnected. Which at least on a receive-only device like the RTL SDR is safe. But with an RP-SMA radio and SMA antenna you risk physically damaging the radio, the antenna, or both. Transmitting with a disconnected antenna can damage a radio, too.
RP-SMA should die in a fire.