Workshop starter for Rust on the Raspberry Pi
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src
.gitignore
Cargo.lock
Cargo.toml
README.md

README.md

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 get Err
  • expect - we can abort the program if we get Err, 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 the Result is Ok or Err

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.