STM32 Blue Pill with Quectel BC95-G Global NB-IoT Module and 18650 lithium ion battery

Rust Rocks NB-IoT! STM32 Blue Pill with Quectel BC95-G on Apache Mynewt

The year is 2029. Humans are populating the Moon, starting at Moon Base One. Two Moon Base Operators are about to commit another grave mistake in the crop garden of beautiful red tomatoes…

Tomato Crop and NB-IoT Sensors on Moon Base One

Operator 1: After the last IoT calamity I’m glad we switched from MQTT to CoAP Servers connected by NB-IoT. Now that we have added many sensors to our crop garden, our NB-IoT Sensors need to transmit more efficiently. Can you change the CoAP payload from JSON to CBOR?

Operator 2: Sure thing! Lemme look at the specs of the CBOR binary format and program the CBOR encoder in C

Operator 1: It’s already 2029! Why aren’t you using Rust? Just call the Rust Macro that encodes CoAP payloads into JSON and CBOR!

Encoding CoAP Payloads in JSON and CBOR with a Rust Macro. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/rust/app/src/app_network.rs

Operator 2: Rust Macros can do so much? Wow I did not know that! Anyway I have finished the C programming for the new firmware. I’ll push the new firmware to 1,000 sensors now

Operator 1: Great! Hold on… Our IoT Dashboards are going crazyTemperature 80 degrees Celsius? Humidity 25 percent? WHAT HAPPENED??!!

Operator 2: OOPS I think the pointers are corrupted in my C firmware… The Sensor Data was supposed to point to the string “TEMPERATURE”… But it got overwritten by “HUMIDITY”… Should have used safer, simpler Rust instead of C…

Operator 1: The Temperature and Humidity Sensor Data are ALL MIXED UP!!! The sprinklers are spraying water on our tomatoes! The ventilators are blowing cold air on them!

Our beautiful tomatoes are turning into… ICED TOMATOES!!! NOOOOO…

Why Embedded Rust instead of Embedded C?

We’re back in 2019… Hardly anyone writes embedded programs in Rust for microcontrollers (like STM32 Blue Pill), we all use C. But we really should switch to Rust! Moon Base One has given us 2 key reasons…

1️⃣ Complexity: Our gadgets are more powerful than ever before. They can communicate via Bluetooth, WiFi, Zigbee, nRF, LoRa, Sigfox and now NB-IoT. Messaging formats are getting more complicated with CoAP and CBOR. Embedded C wasn’t meant for handling such complexity. Rust is!

2️⃣ Safety: With great power comes great responsibility… says the Great Arachnid. How do we ensure that our complex gadgets run non-stop without crashing? We need something safer than Embedded C to prevent runaway pointers before they happen… Rust can!

But have our gadgets already become so complex and unsafe and affordable that we need Embedded Rust?

Well, take a look at this video demo… https://youtu.be/MgK72dqwDuM

[Watch video on YouTube]

Demo of Embedded Rust on STM32 Blue Pill with Quectel BC95-G Global NB-IoT Module

STM32 Blue Pill with Quectel BC95-G Global NB-IoT Module… Runs a realtime operating system (Apache Mynewt) that polls the internal temperature sensor every 10 seconds concurrently while transmitting CoAP messages over NB-IoT… All built for under $10And managed flawlessly by Embedded Rust!

This article uncovers the secrets of the Embedded Rust program running on that Blue Pill… And how how you can do the same! If you’re new to Embedded Rust, no worries, this will be a friendly and gentle introduction to Embedded Rust… I promise!

Overall Program Flow

This diagram illustrates the overall flow of the Embedded Rust program. We’ll dive into each function now…

Overall flow of our Embedded Rust program

Main Program: lib.rs

Normally the Rust Compiler builds our Rust Programs starting at main.rs. In this case we’re building a Rust Library that will be linked into the Apache Mynewt firmware, so the Rust Compiler starts at lib.rs instead.

First thing in lib.rs#![no_std]. This is absolutely essential for Embedded Rust programs and libraries — it tells the Rust Compiler not to include the Rust Standard Library, which contains code that is unnecessary for embedded devices. Think of the Rust Standard Library as the C standard libraries stdio + stdlib + …

When we specify #![no_std], the Rust Compiler includes a lighter version of the standard library, known as the Rust Core Library. It contains the bare minimum needed to run Rust applications.

Rust Externs and Modules

extern crate declares the external Rust Libraries (known as Crates) that we’ll be using. External Crates are declared in Cargo.toml and don’t need to be declared again here… Unless we’re using macros from these crates.

mod app_sensor tells the Rust Compiler that we have a Rust module named app_sensor, and the module’s source code may be found in app_sensor.rs.

If you remember how namespace and #include “…” work in C++, mod app_sensor translates into something like this in C++…

namespace app_sensor {
#include "app_sensor.rs"
}

Which means that the functions in app_sensor.rs will be imported in the module namespace app_sensor. (We’ll see this later when we study the macro expansions for our program.)

We’ll soon discover that in Rust we use mod often to pull in Rust source files, which may use mod to pull in other source files, … That’s how we assemble a Rust program from a tree of Rust source files, starting at lib.rs or main.rs.

app_sensor and app_network will be covered in the subsequent sections.

Next we tell the Rust Compiler which functions and types to import into this code file (lib.rs). We could write core::panic::PanicInfo in our code. But when we say use core::panic::PanicInfo, we can just refer to it later in our code as PanicInfo.

core refers to the Rust Core Library. cortex_m is an external crate that has useful functions for Arm Cortex-M processors, like triggering debugger breakpoints.

mynewt is a crate that contains Rust wrappers for the Mynewt API. That means we can call the Mynewt system functions written in C — just use the Rust wrappers that have been provided. More about Rust safe wrappers in a while.

In lib.rs we’ll be calling some Mynewt kernel-level functions, as well as the debugging console and the Sensor Network Library that provides NB-IoT networking.

Rust Main Function

This is the main() function — it works just like C.

mynewt::sysinit() is called to start up the Mynewt OS system functions and drivers, like the driver for the Blue Pill internal temperature sensor.

app_sensor::start_sensor_listener() is defined in the app_sensor module (explained below). This function tells Mynewt to poll the internal temperature sensor every 10 seconds.

sensor_network::start_server_transport() tells the Sensor Network Library to start the NB-IoT network.

The NB-IoT Driver starts sending AT commands to the NB-IoT Module (Quectel BC95-G), and waits for successful connection to the NB-IoT network.

This happens in the background, so other functions may continue running, like polling the temperature sensor.

Check Rust Errors with Expect

Note the curious way that we’re calling the functions followed by expect()

app_sensor::start_sensor_listener()
.expect("TMP fail");

This actually means “call the function start_sensor_listener(), and if it FAILS, show the message TMP fail”. So it certainly doesn’t mean that we expect the function to fail!

This is an unusual Rust coding convention. But thankfully we’ll see expect only in the main() function. In other functions we’ll use a simpler way to check for failures… the question-mark operator: ?

Sensor Functions: app_sensor.rs

app_sensor.rs configures Mynewt to poll Blue Pill’s internal temperature sensor every 10 seconds. At the top we define the constants…

▶️ SENSOR_DEVICE: Name of the sensor that we’ll be polling. Each sensor in Mynewt is assigned a name by the sensor’s driver. We’ll be using Blue Pill’s internal temperature sensor, named temp_stm32_0

Note that SENSOR_DEVICE has type Strn, which refers to a null-terminated string. Although Rust has two types of strings (str and String), I created Strn because it works more efficiently with Mynewt.

Unlike Rust strings, Strn strings are always null-terminated. So we prevent excessive copying of strings when passing Strn to Mynewt and back. Strn may be passed directly into Mynewt APIs like this…

//  Set the sensor polling time to 10 seconds.
sensor::set_poll_rate_ms(
&SENSOR_DEVICE, SENSOR_POLL_TIME) ? ;

init_strn!() is a Rust Macro that initialises a Strn and terminates the string with null. How do we know it’s a Rust Macro? Because it has ! in its name. Rust Macros are similar to C Macros, but much more powerful.

▶️ SENSOR_POLL_TIME: How often Mynewt should poll the sensor, in milliseconds. We’ll be polling the temperature sensor every 10 seconds, so we set this value to 10,000.

▶️ TEMP_SENSOR_KEY: When transmitting the temperature sensor value to the server, we’ll use t as the name of the sensor value.

▶️ TEMP_SENSOR_TYPE: This declares the type of sensor data, which will be a raw temperature value. Although we could transmit the temperature as a decimal or floating-point number like 28.9 degrees Celsius, it’s more efficient to transmit the raw temperature as an integer like 1925.

This eliminates the need to install floating-point libraries on the device. We’ll convert the raw temperature into actual temperature at the CoAP Server.

Poll The Sensor with Mynewt Sensor Framework

Mynewt provides an elegant way to manage IoT sensors and it truly shines here. Recall that start_sensor_listener() is called by main() to poll the temperature sensor. How do we instruct Mynewt to poll our temperature sensor every 10 seconds? Easy — just call sensor::set_poll_rate_ms() like above.

In Mynewt’s Sensor Framework, Mynewt is aware of all sensors installed (like temp_stm32_0, our temperature sensor) and the type of sensor data each sensor will return (like raw temperature). So Mynewt is capable of polling sensors for sensor data on its own… But it needs to know what to do with the polled data.

To answer that, we call sensor::mgr_find_next_bydevname() to fetch the sensor object for our temperature sensor. For safety, we check that returned sensor object pointer is not null.

Define Sensor Listener with Rust Struct

Then we define a sensor_listener, the function that Mynewt will call whenever it has fetched the sensor data. We declare to Mynewt that the function (called the Listener Function) accepts temperature sensor values (TEMP_SENSOR_TYPE).

The Listener Function is named handle_sensor_data() and we’ll see it in a while. We call sensor::as_untyped() to do some translation from Rust to C, because callback functions like handle_sensor_data() are defined differently in Rust and C.

If we look at the way listener is defined as a sensor_listener

let listener = sensor_listener { ... };

Looks familiar? sensor_listener is actually a struct type! In C++ we would write this as…

sensor_listener listener = { ... };

structs in Rust work the same way as structs in C and C++. But unlike C and C++, Rust also supports this interesting construct…

let listener = sensor_listener {
sl_sensor_type: ... ,
sl_func: ... ,
// Set other fields to 0
..fill_zero!(sensor_listener)
};

.. means “copy the remaining fields from the following object”. The object after .. happens to be fill_zero!(sensor_listener), a Rust Macro that generates an empty sensor_listener with all fields zeroed out. So the result…

1️⃣ We set listener to an instance of sensor_listener

2️⃣ And we set in listener the sl_sensor_type and sl_func fields shown above…

3️⃣ And we set the remaining fields of listener to 0

This is a handy construct (similar to JavaScript’s … operator) to assure ourselves that the remaining fields in the struct are initialised to some known value. Otherwise the remaining fields will become assigned to some unknown value from the stack and cause runtime errors. In fact, the Rust Compiler refuses to compile this code…

let listener = sensor_listener {
sl_sensor_type: ... ,
sl_func: ... ,
// What about the other fields?
};

Because the compiler knows that sensor_listener contains other fields that we have not initialised. And that’s the really cool thing about Rust — it genuinely cares about our Safety and it will do anything it can to prevent our programs from crashing! We’ll see many examples of Safe Coding in Rust in a while.

Register Sensor Listener with Mynewt Sensor Framework

Finally we call sensor::register_listener() to register our Listener Function for the sensor. Mynewt will call our Listener Function handle_sensor_data() every 10 seconds after fetching the temperature sensor data.

Return Result in Rust

In Rust, functions are expected to return the Result type which contains either a result value or a failure code. Hence start_sensor_listener() is declared as…

pub fn start_sensor_listener() 
-> MynewtResult<()>
{ ... }

Which tells the Rust Compiler that start_sensor_listener() will return a MynewtResult type. What’s the result value for this function? start_sensor_listener() works like a void function in C… it doesn’t return any values. It returns nothing.

In Rust we write “nothing” as (). Therefore the function returns MynewtResult<()>, a generic result that contains either “nothing” or an error code. How do we return “nothing” at the end of the function?

pub fn start_sensor_listener() 
-> MynewtResult<()> {
...
// Return `Ok()` to indicate success.
// This line should not end with a semicolon (;).
Ok(())
}

If this function is expected to return 123 we would write it as Ok(123). But since this function returns “nothing”, we write it as Ok(()). Which means “yes this function has completed successfully, but we don’t have a result value”.

When we return Ok(...) at the end of a function, make sure that we don’t add a semicolon ;. That’s because the Rust Compiler treats a block of code like a sushi conveyor belt that unveils statement after statement… separated by a semicolon ;.

The final statement Ok(...) is the result value for the entire sushi conveyor belt… er… function. In fact, returning Ok(...) like this…

    ...
Ok(123)
}

…is actually equivalent to a return statement plus semicolon…

    ...
return Ok(123);
}

Both are perfectly valid in Rust. But of course I’ll choose the shorter version. When we call functions that return Result, we add a question mark ? like this…

//  Set the sensor polling time to 10 seconds
sensor::set_poll_rate_ms(&SENSOR_DEVICE, SENSOR_POLL_TIME) ? ;

This means “check the result of sensor::set_poll_rate_ms() and if it’s an error, stop the current function and return the error code to the caller”. It’s easy to miss the question mark ? so I make it highly conspicuous by surrounding it with spaces.

Handle Sensor Data with Rust Pattern Matching

handle_sensor_data() is the Listener Function that’s called by Mynewt after it has polled the temperature sensor. It calls convert_sensor_data() to convert the sensor data from Mynewt’s format to our own transmission format. More about this in a while.

Then it calls send_sensor_data() to transmit the converted sensor data. (We’ll learn more in the next section.) send_sensor_data() is called like this…

let result = send_sensor_data(&sensor_value);//  `if let` will assign `error_code` to
// the error code inside `result`
if let Err(error_code) = result {
// Check the error code
if error_code == MynewtError::SYS_EAGAIN {
...
}
}

send_sensor_data() returns the MynewtResult<()> type, which contains…

1️⃣ Either Ok(()), which means no error and nothing to return…

2️⃣ Or Err(error_code), which is an error code

Which is it? Let’s use Rust Pattern Matching! This if let condition…

if let Err(error_code) = result { ... }

…will fail if the result is Ok(()). So that eliminates option 1️⃣. What about the error code for option 2️⃣?

When we match the pattern Err(error_code) with result, we actually bind error_code to the error code inside result.

So subsequently we may check the value of error_code like this…

if error_code == MynewtError::SYS_EAGAIN { ... }

That’s how we use Rust Pattern Matching to check error codes in Results! If we’re not really interested in the error code, just use the question mark ? to check for success or failure.

Convert Sensor Data with Rust Pattern Matching

convert_sensor_data() is a beautiful function that could only happen in Rust, not C. Recall that it converts sensor data from Mynewt’s format to our own transmission format. The structure of the function looks interesting…

fn convert_sensor_data(...) -> SensorValue {    //  Construct and return a new `SensorValue` (without semicolon)
SensorValue {
key: "t",
val: ...
}
}

Remember the Ok(...) trick for returning things without semicolon from the sushi conveyor belt… er… function? We’re using it here! We construct a struct with SensorValue { ... }. Then we return the struct, semicolon-less!

Now look closely what’s inside val

//  Construct and return a new `SensorValue` (without semicolon)
SensorValue {
key: "t",
val: match sensor_type {
// If this is raw temperature...
SENSOR_TYPE_AMBIENT_TEMPERATURE_RAW => {
// Get the raw temperature
let mut raw = fill_zero!(sensor_temp_raw_data);
let rc = unsafe {
sensor::get_temp_raw_data(
sensor_data, &mut raw)
};
// Return the raw temperature
SensorValueType::Uint( raw.strd_temp_raw )
}
// Unknown type of sensor value
// _ => { assert!(false, "sensor type"); ... }
}
}

Does match look familiar? Yes it looks like switch ... case in C! But in Rust, match can return values like this…

    ...
// Return the raw temperature
SensorValueType::Uint( raw.strd_temp_raw )
}

Again we used the disappearing semicolon trick to return a value inside match.

What’s SensorValueType::Uint? That’s an enum. And as expected, Rust enums are more powerful than enums in C… Rust enums can contain values (like raw.strd_temp_raw)!

Remember our friends Ok(...) and Err(...)? They are enums too! Yes enums are shockingly powerful in Rust. (Then again, everything in Rust is shockingly powerful!)

The pattern _ => ... at the end of match is identical to the default construct in C’s switch ... case statement (it matches everything else).

But the Rust Compiler is super intelligent in deducing that sensor_type has one and only one possible pattern: SENSOR_TYPE_AMBIENT_TEMPERATURE_RAW. So the Rust Compiler refuses to accept the default pattern _. (And hence I have commented it out.)

Code Safety in Rust

More reminders from convert_sensor_data() that the Rust Compiler really cares about our Safety…

let mut raw = ...

mut (mutable) declares to the Rust Compiler that raw is a variable that will be changed. The Rust Compiler assumes that all variables are const and will not be changed, unless we declare them as mut.

sensor::get_temp_raw_data(sensor_data, &mut raw)

We write &mut to pass a variable to a function that’s expected to change the variable. So sensor::get_temp_raw_data() is expected to change the value of raw above.

let rc = unsafe { 
sensor::get_temp_raw_data( ... )
};

This ought to alarm us… We are calling a function sensor::get_temp_raw_data() that’s Unsafe! This function is declared unsafe because it’s a C function that could cause our program to crash.

Unlike the other Mynewt C functions, I haven’t constructed a Safe Wrapper yet for sensor::get_temp_raw_data(), so we have to call it the Unsafe way. unsafe is a good way to tag C functions that require closer inspection before calling them.

Network Functions: app_network.rs

app_network.rs contains the functions for transmitting sensor data to the CoAP Server over NB-IoT. We’re using the CoAP Server hosted at thethings.io.

send_sensor_data() is called by handle_sensor_data() to transmit the polled sensor data from the temperature sensor.

It calls sensor_network::get_device_id() to fetch the randomly-generated device ID. This device ID changes each time we restart the device. We’ll transmit the device ID to thethings.io so that we can see the sensor data for our device.

sensor_network::init_server_post() prepares the memory buffers to compose a new CoAP message. In embedded devices like Blue Pill, we have limited RAM so we can’t compose two CoAP messages at the same time. Hence init_server_post() uses a locking Mynewt semaphore to ensure that only one task is composing a CoAP message at any time.

Remember that the connection to NB-IoT is established by a background task. There’s a chance that the connection isn’t ready yet. If it’s not ready, we return Err( MynewtError::SYS_EAGAIN ) to ask the Listener Function to retry later. That’s how we return an error code SYS_EAGAIN as a Result type in Rust.

Here comes the magical part of the entire program… How we actually compose a CoAP Message containing the sensor data… And how we manage Intent vs Implementation

CoAP Intent vs Implementation

The above Rust code shows our Intent clearly… We just want to transmit 2 items of data to the server:

1️⃣ Device ID, which we’ll be using to view our sensor data. (In case there are other devices transmitting at the same time)

2️⃣ Raw Temperature, which is a whole number from 0 to 4095

What about the Implementation? Well that depends which server we’ll be transmitting the message to. For thethings.io, the server expects a JSON Payload in the CoAP Message like this…

{"values":[
{"key":"device",
"value":"010203"},
{"key":"t",
"value":1715}
]}

It’s similar to our Intent, just that the Implementation is different. And the above Rust code handles that! coap!() is a Rust Macro I have written that generates the above JSON Payload given the device_id and val values.

Why is this Intent vs Implementation distinction important?

As I’m writing this, thethings.io is preparing to launch CBOR support for their CoAP Server. As we have seen on Moon Base One, CBOR promises to shrink our CoAP Messages because we will no longer use inefficient JSON Payload encoding… we will use compressed, binary CBOR Payload encoding instead!

Guess What? The coap!() macro is ready to generate CBOR Payloads based on the same code! All we need to do is to replace “@json” by “@cbor” and the rest happens like magic!

Same for your own CoAP Server — you should be able to tweak the coap!() macro to support the encoding format required by your server. This level of Intent vs Implementation separation is not possible with C Macros, only with Rust Macros.

Multitasking with Mynewt

Finally we call sensor_network::do_server_post() to transmit our CoAP Message. The actual transmission is done by a background task so that the we won’t get stuck waiting for the transmission to complete. We display the URL for viewing the sensor data. And we return Ok(()) semicolon-lessly.

Our Rust program reads the sensor and transmits the sensor data concurrently, managed by Mynewt. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/logs/standalone-node.log#L128-L148

We can see that Mynewt plays an important role in managing the realtime tasks on our Blue Pill…

1️⃣ The temperature sensor is polled automatically by Mynewt according to a timer (every 10 seconds)

2️⃣ CoAP Messages are transmitted by Mynewt in a background task

This is possible only because Mynewt is a realtime operating system. It would be very difficult for Rust to do this on bare metal.

Install Embedded Rust and Mynewt

Ready to try out the Embedded Rust code for yourself? Follow the instructions below to install the Embedded Rust + Mynewt build and application files on Windows…

Install Embedded Rust and Apache Mynewt
for Visual Studio Code on Windows

🛈 What is VSCode? Is it related to Visual Studio? How is Microsoft involved? Read this

Hardware Required

STM32 Blue Pill, ST-Link V2, Quectel BC95-G breakout board with antenna, NB-IoT SIM

To run the Embedded Rust program and send sensor data over NB-IoT, we’ll need the following hardware…

1️⃣ STM32 Blue Pill

2️⃣ ST-Link V2 USB Adapter

3️⃣ Quectel BC95-G Global NB-IoT Module (breakout board with antenna). I ordered mine here. The manual in Chinese is here.

4️⃣ NB-IoT SIM from your local NB-IoT network operator

Many thanks to StarHub for sponsoring the NB-IoT SIM that I used for this tutorial!

Connect The Hardware

To connect Blue Pill to Quectel BC95-G and ST-Link, follow the instructions here

Blue Pill connected to Quectel BC95-G and ST-Link

Flash The Firmware To Blue Pill

The next step is to flash the firmware into Blue Pill’s ROM. We’ll need to connect the Blue Pill to the USB port of our computer via an ST-Link V2 adapter.

Blue Pill and ST-Link connected to USB port

1️⃣ Check that the Blue Pill is connected to ST-Link…

And the ST-Link is connected to your computer’s USB port.

Now let’s head back to Visual Studio Code…

2️⃣ Click Terminal → Run Task → [4] Load bluepill_boot

This flashes the bootloader to Blue Pill, to start the Apache Mynewt operating system upon startup. If it shows errors, compare with this flash log.

3️⃣ Click Terminal → Run Task → [5] Load bluepill_my_sensor

This flashes the firmware (containing our Visual Program) to Blue Pill. If it shows errors, compare with this flash log.

Run The Program

1️⃣ Click Debug → Start Debugging

2️⃣ Click View → Output

Select Adapter Output to see the Blue Pill log

3️⃣ The debugger pauses at the line with LoopCopyDataInit

Click Continue or press F5

4️⃣ The debugger pauses next at the main() function.

Click Continue or press F5

The program should now poll the internal temperature sensor every 10 seconds and transmit to thethings.io. Let’s study the Blue Pill execution log…

Check The Log

Excerpt from the Blue Pill log: Reading the sensor and transmitting the sensor data over NB-IoT. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/logs/standalone-node.log#L128-L148

The log from our Blue Pill should look like this.

The contents of the log should be similar to the previous article. To understand the log messages, refer to the section “Check The Log” of this article.

When we Ctrl-Click the URL in the log…

https://blue-pill-geolocate.appspot.com?device=ac913c

Web page with computed temperature

…We see a web page with the computed temperature value of our temperature sensor in degrees Celsius.

That’s because thethings.io has converted the raw temperature into the actual temperature (in degrees Celsius).

We have installed a script at thethings.io that pushes the computed temperature to blue-pill-geolocate.appspot.com, so that we could see the computed temperature.

The URL (and the random number) changes each time we restart the program. More details about the setup for thethings.io may be found in an earlier article.

What’s Next?

Visual Rust, of course! We’ll soon be able to generate the Embedded Rust program in this article by simply dragging and dropping blocks around. Check out the article here…

I have extended the Rust Code Generator for Google Blockly to generate Rust code that’s fairly close to the code that we see in this article.

Embedded Rust can clearly harness our powerful gadgets… It handles complex embedded programs well. And it prevents bugs from crashing our gadgets.

But will we know how to harness the power of Rust?

With great power comes great… Teaching! (That’s where I fit in…)

Could Rust harness its own power, like with auto-generated Safe Wrappers?

Or could we do it Visually… so we can see in a single glance how Rust is controlling our gadgets?

I’m about to find out… Stay tuned!

UPDATE: I have a new article that explains how to reduce the power consumption of our device…

Under Development: Visual Studio Code Extension for Visual Embedded Rust

💎 Advanced Topic: CoAP Macro

The CoAP Macro is defined in rust/mynewt/src/encoding/macros.rs

It’s a Rust Declarative Macro that parses the JSON-like input (sensor data fields) and generates Rust code to encode the sensor data in JSON or CBOR format. Given this input

… And depending on the selector “@json” or “@cbor”, the CoAP Macro expands into the following code…

The CoAP Macro was adapted from the JSON parser in the serde_json crate.

The generated code uses a CoAP Context object to maintain the current JSON and CBOR encoding state: rust/mynewt/src/encoding/coap_context.rs

The JSON encoding code calls the Mynewt JSON API while the CBOR encoding code calls the Mynewt OIC API.

Read more about Rust Declarative Macros

Read more about CoAP Encoding

💎 Advanced Topic: Hosting Rust on Mynewt

We use a custom Build Script to build the Rust application and link it into the Mynewt firmware. (View the Build Log) The script does the following…

  1. Run cargo build to build the Rust application into a library named libapp.rlib
  2. Extract the *.o object files from libapp.rlib (including the main() function from Rust) and external crates. Combine the object files into a new library rustlib.a
  3. Copy rustlib.a into the Mynewt build folder for custom library libs/rust_app
  4. Copy the Rust Core Library libcore-*.rlib from the Rust Compiler into the Mynewt build folder for custom library libs/rust_libcore
  5. Run newt build to build the Mynewt firmware, which includes rust_app and rust_libcore
  6. The Mynewt firmware calls the main() function defined in Rust, so the Rust application runs on startup

The following files may be useful for reference…

Read more about hosting Rust applications on Mynewt

💎 Advanced Topic: Safe Wrappers for Mynewt

Although Rust can call C functions directly, it’s unpleasant to call the Mynewt API with unsafe code. And we need to be very careful when converting pointers between Mynewt and C. Also all strings need to be null-terminated when passing from Mynewt to Rust. That’s why we create Safe Wrappers for the Mynewt API.

Here’s a typical Mynewt API os_task_init() that has been imported into Rust with our gen-bindings.sh script, thanks to bindgen...

…And here’s the Safe Wrapper for that API. Note the following…

  1. Unsafe types like * const c_char have been replaced by the safer version Strn
  2. We validate all Strn strings to confirm that they are null-terminated before passing to Mynewt: arg2.validate()
  3. We convert the Mynewt result code into the standard MynewtResult<()> type

How did we create the Safe Wrappers? They were automatically generated with a Rust Procedural Macro named safe_wrap. That’s why we see this annotation…

#[mynewt_macros::safe_wrap(attr)]

Here is the Rust Doc for the Mynewt API

Read more about Safe Wrappers

💎 Advanced Topic: Auto-Generating Safe Wrappers

The Safe Wrappers were generated by our Rust Procedural Macro named safe_wrap(). If you look at the source code, you’ll see that it’s actually another Rust program, except that it’s invoked during the Rust build, not during runtime.

safe_wrap() takes an extern declaration like this…

…And generates this Safe Wrapper.

It calls the syn crate to parse the extern declaration, and calls the quote crate to generate the Safe Wrapper code.

Yes, safe_wrap() is a Rust program that reads a Rust program and generates another Rust program!

We don’t generate wrappers yet for the entire Mynewt API. The whitelist of Mynewt APIs for generating wrappers is specified here.

We also use fixed rules to determine the namespace for each Mynewt function. Given a Mynewt function like os_task_init(), we use the rules to deduce that os is the namespace and task_init() should be name of the Rust Safe Wrapper.

To test the macros, I created a test-rust-macros project.

Read more about Rust Procedural Macros

💎 Advanced Topic: Generating Rust Bindings for Mynewt

The Mynewt API was imported into Rust using the gen-bindings.sh script. Here is the log.

This script takes the preprocessed C header files from Mynewt and generates extern function declarations in Rust. The script calls bindgen to create the declarations.

Given this C declaration…

…The script generates this Rust extern function declaration.

Only a subset of the Mynewt API is processed. Although bindgen can process *.h header files in selected include directories, it doesn’t work for Mynewt because of its complicated include structure.

I have chosen to let gcc create preprocessed versions of each header file. The gcc options look like this. The preprocessed header file looks like this. The generated Rust declarations is here.

The script passes blacklists and whitelists to bindgen to ensure that there are no duplicate declarations.

Read more about generating Rust bindings