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 crazy… Temperature 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
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 $10… And 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.
From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/rust/app/src/lib.rs#L24-L37
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.
From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/rust/app/src/lib.rs#L38-L45
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
From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/rust/app/src/lib.rs#L46-L74
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.
Quectel BC95-G driver sends AT commands to connect to NB-IoT. From https://github.com/lupyuen/stm32bluepill-mynewt-sensor/blob/rust-nbiot/logs/standalone-node.log#L90-L100
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
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…
- Run
cargo build
to build the Rust application into a library namedlibapp.rlib
- Extract the
*.o
object files fromlibapp.rlib
(including themain()
function from Rust) and external crates. Combine the object files into a new libraryrustlib.a
- Copy
rustlib.a
into the Mynewt build folder for custom librarylibs/rust_app
- Copy the Rust Core Library
libcore-*.rlib
from the Rust Compiler into the Mynewt build folder for custom librarylibs/rust_libcore
- Run
newt build
to build the Mynewt firmware, which includesrust_app
andrust_libcore
- 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…
- Rust Doc for the application
- Disassembly of the Rust Application build
- Disassembly of the Rust Crates
- Disassembly of the entire firmware
- Memory map of the firmware
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…
-
Unsafe types like
* const c_char
have been replaced by the safer versionStrn
- We
validate all
Strn
strings to confirm that they are null-terminated before passing to Mynewt:arg2.validate()
- 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