Awesome
pi-workshop-rs
This is the workshop starter project for the 'Rust on Raspberry Pi' workshop at the Raspberry Pi 5th Birthday Party. Thanks to Cambridge Consultants for their support.
This material is Copyright Jonathan 'theJPster' Pallant, 2017. It is licensed as CC-BY.
Step 1 - Get the code
The code for this workshop lives on Github, in the repository thejpster/pi-workshop-rs. Open yourself a terminal window and execute these two commands.
Note: If the workshop organisers have prepared your workstation for you, you should find the folder pi-workshop-rs
exists, in which case you can skip the git clone
and just perform the cd
.
~ $ git clone https://github.com/thejpster/pi-workshop-rs
~ $ cd pi-workshop-rs
Step 2 - Check it builds
Unlike Python (but like C or C++), Rust code must be compiled from source into machine code in order to make it executable. The Rust ecosystem uses cargo
as the build system - like make
or scons
in a C world, but more powerful and easier to use.
To build our code and execute our crate
(the Rust term for a program or a library module) in one step, we execute cargo run
.
~/pi-workshop-rs $ cargo run
Updating git repository `https://github.com/thejpster/sensehat-rs`
Compiling void v1.0.2
Compiling cfg-if v0.1.0
Compiling getopts v0.2.14
Compiling bitflags v0.4.0
Compiling byteorder v0.4.2
Compiling byteorder v1.0.0
Compiling libc v0.2.20
Compiling semver v0.1.20
Compiling measurements v0.2.1 (https://github.com/thejpster/rust-measurements#428d1426)
Compiling pulldown-cmark v0.0.3
Compiling bitflags v0.5.0
Compiling rand v0.3.15
Compiling rustc_version v0.1.7
Compiling nix v0.6.0
Compiling tempdir v0.3.5
Compiling skeptic v0.5.0
Compiling i2cdev v0.3.1
Compiling sensehat v0.1.0 (https://github.com/thejpster/sensehat-rs#03e5ae84)
Compiling pi-workshop-rs v0.1.0 (file:///home/pi/pi-workshop-rs)
Finished debug [unoptimized + debuginfo] target(s) in 99.30 secs
Running `target/debug/pi-workshop-rs`
Hello, world!
After a few minutes you will see that cargo
has compiled our code, and any code that we depend upon (like the Sense Hat crate), and any code that that depends on (etc, etc) automatically. Next time you run this command, it won't compile any crates that haven't changed, so it will be quicker.
Note that if you are on an official workshop workstation and the organisers Foundation have pre-compiled the project for you (to save time), or the second time you run the command, you'll see this shorter output.
~/pi-workshop-rs $ cargo run
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/pi-workshop-rs`
Hello, world!
Step 3 - Open the code in an editor.
If you're on a Raspberry Pi, that means that sadly modern editors like Atom, Sublime Text, or VS Code are out of the question. You could go old-school and use Emacs or Vim, or perhaps try Geany (a GTK+ editor with good Rust support).
~/pi-workshop-rs $ geany src/main.rs &
We'll now be editing the code in the editor, then returning to the terminal to run our code.
Step 4 - Make a Sense Hat object
We'll be using the Sense Hat for this workshop - the same board that Tim Peake took to the International Space Station. It provides an 8x8 LED RGB display and some sensors we can read.
In src/main.rs
, you'll see our main function. This is the function that gets called when our program is run. Change the body of the main
function to look like this:
let hat = sensehat::SenseHat::new();
println!("Hello, world");
Make sure you indent your code properly, to keep it neat and tidy.
What this line does it create a new variable, called hat
. The variable is initialised by the new()
function associated with the SenseHat
struct which lives in the sensehat
crate imported at the top of the file.
If you run this code, you'll see the compiler complain that you've made a variable but never used it. Good point, Rust!
Step 5 - Try and read the temperature
The SenseHat
struct has an API very similar to the Python Sense Hat driver.
First up, we notice that the new
function on the SenseHat
struct returns a SenseHatResult
. This is shorthand for Result<SenseHat, SenseHatError>
and what this means is that the function could return one of two things. If it works OK, you get a SenseHat
, which is an object we can use to do things. If it doesn't work OK (perhaps you don't have the I2C drivers loaded, or your on a PC not a Raspberry Pi), then you get a SenseHatError. Rust enforces you to check which you've got before allowed to call any methods on the returned object.
Let's test that theory, by modifying our main.rs
file like this:
let mut hat = sensehat::SenseHat::new();
let temp = hat.get_temperature_from_humidity();
println!("Hello, world");
Oh-oh. The compiler is sad.
~/pi-workshop-rs $ cargo run
Compiling pi-workshop-rs v0.1.0 (file:///home/jgp/work/pi_party/pi-workshop-rs)
error: no method named `get_temperature_from_humidity` found for type `std::result::Result<sensehat::SenseHat, sensehat::SenseHatError>` in the current scope
--> src/main.rs:5:20
|
5 | let temp = hat.get_temperature_from_humidity();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
error: Could not compile `pi-workshop-rs`.
To learn more, run the command again with --verbose.
Rust correctly points out that we have a Result
object, and you can't call get_temperature_from_humidity()
on one of those.
We have a couple of options for checking whether our new()
called was OK or not:
unwrap
- we can abort the program if we getErr
expect
- we can abort the program if we getErr
, and report an error message.if let
- we can execute some code if we get one specific alternative.match
- like a switch statement in C, we can execute different things depending on whether theResult
isOk
orErr
Calling unwrap
is a little unhelpful, because it won't be immediately obvious why the program has aborted. The other options are a little verbose at this stage, so let's use expect
. It's a function which takes a Result
and either returns the type embedded in the Ok
variant, or aborts with the given message.
Change the code to read:
let mut hat = sensehat::SenseHat::new().expect("Couldn't find Sense Hat");
let temp = hat.get_temperature_from_humidity();
println!("Hello, world");
Success! But, unfortunately we didn't actually do anything with our temperature.
~/pi-workshop-rs $ cargo run
Compiling pi-workshop-rs v0.1.0 (file:///home/pi/pi-workshop-rs)
warning: unused variable: `temp`, #[warn(unused_variables)] on by default
--> src/main.rs:5:9
|
5 | let temp = hat.get_temperature_from_humidity();
| ^^^^
Finished debug [unoptimized + debuginfo] target(s) in 7.30 secs
Running `target/debug/pi-workshop-rs`
Hello, world!
Step 6 - Formatting our readings
In other languages, a function like get_temperature_from_humidity()
might give you a number of some sort (maybe a floating point number). But, what would the units be? The comments might tell you, but what if you mis-read them? Issues with misunderstanding the units represented by plain numbers in computer programs have serious consequences.
In Rust, we have a system for richly expressing exactly what our values mean, but without adding any run time overhead. In this case, the get_temperature_from_humidity()
function returns a sensehat::Temperature
object (which is actually re-exported from a fork of the excellent measurements crate). This Temperature
object has methods like as_celsius()
and as_fahrenheit()
which return floating point numbers in the specified units. It also has a default formatting implementation, which picks a unit and puts in the return string.
Let's use that, but first, we have another Result to unwrap (because reading the temperature can fail - say, if you'd unplugged the SenseHat, or the sensor chip was damaged).
let mut hat = sensehat::SenseHat::new().expect("Couldn't find Sense Hat");
let temp = hat.get_temperature_from_humidity().expect("Couldn't read temp");
println!("Hello, world! It's {}", temp);
This gives us:
~/pi-workshop-rs $ cargo run
Compiling pi-workshop-rs v0.1.0 (file:///home/pi/pi-workshop-rs)
Finished debug [unoptimized + debuginfo] target(s) in 7.30 secs
Running `target/debug/pi-workshop-rs`
Hello, world! It's 34.5 °C
Ooh, a unicode degree symbol. It's like we're in the future or something.
Step 7 - Compare two temperatures
There are two temperature sensors on the Sense Hat. Let's get both of them, and see what the difference is. Experience shows they're usually within a degree of each other - but what about your board?
We're adding in an extra function here, so don't just paste this inside main()
like last time - this is the whole file.
extern crate sensehat;
use sensehat::Temperature;
fn compare_temps(first: &Temperature, second: &Temperature) {
let difference = if first > second {
first.as_celsius() - second.as_celsius()
} else {
second.as_celsius() - first.as_celsius()
};
println!("Temperature difference of: {:.1} degrees", difference);
}
fn main() {
let mut hat = sensehat::SenseHat::new().expect("couldn't find Sense Hat");
let temp1 = hat.get_temperature_from_humidity().expect("Reading humidity temp");
let temp2 = hat.get_temperature_from_pressure().expect("Reading pressure temp");
println!("Temp1: {}", temp1);
println!("Temp2: {}", temp2);
compare_temps(&temp1, &temp2);
}
You'll see that declaring and calling functions is relatively straightforward. Here're we're passing the two Temperature
objects in by immutable reference. We've also introduced an if
expression, and taken advantage of the fact that in an expression based language, everything has a return type - even if
!.
Finally, note that we need to manually convert our temperature objects to floating point values in Celsius by hand otherwise the subtraction gives us odd results (because Temperature is actually working in Kelvin internally). But we don't need to do the conversion to simply see which is larger. Because difference
is a plain float, we format it with a single decimal place - just to make the output a little neater.
Finally, it's worth noting that unlike C there are no function declarations required. We could have written main()
above and put compare_temps()
below and it would still work.
Step 8 - Looping and Vectors
Let's grab some repeated pressure readings now, and store them in a vector. We'll need to create a vector, then use a for loop to repeatedly perform the reading and push the data into the vector.
extern crate sensehat;
use sensehat::Temperature;
use std::thread;
use std::time;
fn compare_temps(first: &Temperature, second: &Temperature) {
let difference = if first > second {
first.as_celsius() - second.as_celsius()
} else {
second.as_celsius() - first.as_celsius()
};
println!("Temperature difference of: {:.1} degrees", difference);
}
fn main() {
let mut hat = sensehat::SenseHat::new().expect("couldn't find Sense Hat");
let temp1 = hat.get_temperature_from_humidity().expect("Reading humidity temp");
let temp2 = hat.get_temperature_from_pressure().expect("Reading pressure temp");
println!("Temp1: {}", temp1);
println!("Temp2: {}", temp2);
compare_temps(&temp1, &temp2);
let mut pressures = Vec::new();
for i in 0..20 {
println!("Getting reading {}...", i);
let temp = hat.get_pressure().expect("Reading pressure");
pressures.push(temp);
thread::sleep(time::Duration::from_millis(250));
}
println!("Pressure readings: {:?}", pressures);
}
Rather than the sleep
function taking a floating point number of seconds (or is it milliseconds?) as you would in other languages, here it takes a Duration
object.
Again, we haven't needed to specify the type of our new variable pressures
as the compiler is able to work it out - if we store Pressure
objects in it, it must be a Vec<Pressure>
!
The final println!
uses the :?
'debug' format specifier. This is very useful for dumping the contents of complex objects, like our Vector, and can be implemented automatically by the compiler for any types you create if you ask it nicely.
Step 9 - Go explore...
Now it's time to explore the rest of the SenseHat
API. Can you get the humidity? What does the default formatting for that look like? Can you print out the average atmospheric pressure of the room over the course of 20 seconds, in PSI? What happens if you try and read the temperature in a loop?
Go have fun, with complete type safety behind you every step of the way.