As part of my PhD research (and last ~six or so years building and deploying industrial mesh networks) I evaluate and use quite a number of low-power RF transceivers, which typically involves a bunch porting C drivers to new platforms and/or declaring bankruptcy and writing new drivers from first principles.
In recent times, I have been able to do some of this in Rust ^_^
A while ago there was some discussion about whether it was possible to build a generic radio transceiver trait. ryankurte/rust-radio is an attempt to do this, based on existing APIs from Riot-OS, Contiki-OS, Tock-OS, as well as my experience building and integrating custom networking stacks in C.
The radio abstraction is designed to provide a simple base interface for radio devices, and provides a set of polling-based traits to be implemented by radio devices, as well as blocking and experimental async wrappers for objects that implement these traits, and a radio mock for testing purposes.
The polling API is intended to be implemented by radio devices, and to provide a simple base for implementation of the more complex / useful APIs. Use of this API requires careful management of timeouts and errors (and a bunch of boilerplate).
// Naievely send some data
// Enter transmit mode
radio.start_transmit(data)?;
// Await completion
while !self.check_transmit()? {
radio.delay_ms(1)
}
// Naievely receive some data
// Enter receive mode
radio.start_receive()?;
// Await reception
while !self.check_receive()? {
radio.delay_ms(1)
}
// Read received data
let mut buff = [0u8; 128];
let mut i = BasicInfo::new(0, 0);
let n = self.get_received(info, buff)?;
Blocking methods are controlled using a BlockingOptions
structure that supports configuration of timeouts and polling periods, and return on completion.
This API is automatically implemented for objects implementing the polling API and should not be manually implemented.
// Receive some data
let mut buff = [0u8; 128];
let mut i = BasicInfo::new(0, 0);
// Receive using a blocking call
let res = radio.do_receive(&mut buff, &mut i, BlockingOptions::default())?;
// Transmit some data
let res = radio.do_transmit(&[0xaa, 0xbb], BlockingOptions::default())?;
assert_eq!(res, Ok(()));
Async methods do not (currently) contain timeout or polling periods, however, this may be introduced using the AsyncOptions
object in future. At the moment the waker is notified every poll
, and I am not yet sure how to mitigate this (either via polling period or using external interrupts to notify the waker).
This API is automatically implemented for objects implementing the polling API and should not be manually implemented.
task::block_on(async {
// Setup buffer and receive info
let mut buff = [0u8; 128];
let mut i = BasicInfo::new(0, 0);
// Receive using a future
let n = match radio.async_receive(&mut i, &mut buff, AsyncOptions::default())
.await?;
// Send using a future
radio.async_transmit(&[0xaa, 0xbb], AsyncOptions::default())
.await?;
});
So far there are three working drivers, one that’s communicating with the device but not yet working, and one that’s only on the TODO list. If you have other suggestions or requests or would like to collaborate, please get in touch!
Each driver contains the driver library (obviously), as well as a command line utility to get started with the devices on embedded linux.
Testing is achieved in two ways, the first is using embedded-hal-mock and travis-ci to test transactions, the second uses buildkite with custom workers and actual hardware, each of these workers is a raspberry pi with a pair of attached radios, allowing end-to-end testing of the drivers (see: ryankurte/rust-radio-sx128x).
So far I have physical hardware for all the devices, however, some work remains on the sx127x tests (hopefully the sx128x ones can be extracted and re-used), and the at84rf212 physical device has lost it’s memory and needs a rebuild.
std::boxed::Box
poll
function, you always need let s = self.get_mut();
to take a mutable reference to self: Pin<&mut Self>
, and the errors are extremely non-obvious if you do not