Small Refactor To Prepare For Writing The Rest API

18 Mar 2017

Over the past few weeks I have been starting to build a raspberry pi zero w based rover.

In my last post I looked at writing a simple cli tool in rust for the rover. Next I wanted to start building a rest api able to control the rover but before I do there are a few things that need to be changed. I decided to separate it out into another post to keep the next one more focused. You can grab the changes made and skip this post by cloning the v0.3 branch or follow on from the previous post which ended on the v0.2 branch.

We will look at separating out the rover code into a library and moving the main.rs into a named binary. This will allow us to keep the rover cli tool while developing the rest api along side it. We will also look at refactoring the rover code to give us more control over the set up and tear down code. Finally we fix an unrelated upstream bug with our image creation.

Creating The Library

Turning our code into a library is trivial. All we need to do is create a file called src/lib.rs, include any crates we use and export everything we want to be available. In our case this is just the rover and error modules.

extern crate sysfs_pwm;
#[macro_use]
extern crate error_chain;

pub mod error;
pub mod rover;

pub use rover::Rover;

We also export the rover::Rover as a convenience allowing us to use rpizw_rover::Rover instead of rpizw_rover::rover::Rover.

Moving The Binary

The default rust binary for a crate is located at src/main.rs just like ours currently is. But you can have additional binaries, or include binaries along side a library, like we want to do, by placing them in src/bin/<bin_name>.rs. You can have as many binaries in there as you wish in a single crate by doing this. Lets rename our binary to rover-cli by moving it to src/bin/rover-cli.rs. We also need to move cli.yml to the same place.

mkdir src/bin
mv src/main.rs src/bin/rover-cli.rs
mv src/cli.yml src/bin/cli.yml

And change the name: rpizw-rover line in src/bin/cli.yml to name: rover-cli.

Then add extern crate rpizw_rover; to the top of the src/bin/rover-cli.rs as it is now effectively an external library as far as the binary is concerned. This means we also need to remove the mod lines and prefix the uses of our modules with rpizw_rover::. We can also remove the crates that we no longer directly use. Below is the diff of these changes.

diff --git a/src/main.rs b/src/bin/rover-cli.rs
similarity index 93%
rename from src/main.rs
rename to src/bin/rover-cli.rs
index 596d005..f0cafaf 100644
--- a/src/main.rs
+++ b/src/bin/rover-cli.rs
@@ -1,13 +1,8 @@
-extern crate sysfs_pwm;
-#[macro_use]
-extern crate error_chain;
+extern crate rpizw_rover;
 #[macro_use]
 extern crate clap;
 
-mod error;
-mod rover;
-
-use error::*;
+use rpizw_rover::error::*;
 
 const PWM_CHIP: u32 = 0;
 const LEFT_PWM: u32 = 0;
@@ -15,7 +10,7 @@ const RIGHT_PWM: u32 = 1;
 
 fn run() -> Result<()> {
     use clap::App;
-    use rover::Rover;
+    use rpizw_rover::Rover;
 
     let yaml = load_yaml!("cli.yml");
     let matches = App::from_yaml(yaml).version(crate_version!()).get_matches();

To build all of the binaries in our project just run cargo build or with the --target ... to cross compile them. To build a specific binary run cargo build --bin <bin_name>. So to cross compile our rover-cli bin for the pi we can run.

cargo build --bin rover-cli --target arm-unknown-linux-gnueabihf

The Refactor

While building the webserver I realised I wanted more control over exporting and unexporting the pwm modules. So I moved the export/set_period calls to their own export function and removed the disable calls from the unexport function. I also made the unexport function take a reference so it does not consume the rover struct that called it.

diff --git a/src/rover.rs b/src/rover.rs
index 3396e2d..b3beffb 100644
--- a/src/rover.rs
+++ b/src/rover.rs
@@ -17,16 +17,20 @@ impl Rover {
     pub fn new(chip: u32, left_pin: u32, right_pin: u32) -> Result<Rover> {
         let left = Pwm::new(chip, left_pin).chain_err(|| "failed to create left motor")?;
         let right = Pwm::new(chip, right_pin).chain_err(|| "failed to create right motor")?;
-        left.export().chain_err(|| "failed to export the left motor pwm channel")?;
-        right.export().chain_err(|| "failed to export the right motor pwm channel")?;
-        left.set_period_ns(PERIOD).chain_err(|| "failed to set period on left motor")?;
-        right.set_period_ns(PERIOD).chain_err(|| "failed to set period on right motor")?;
         Ok(Rover {
             left: left,
             right: right,
         })
     }
 
+    /// Exports and setup the period for the servos.
+    pub fn export(&self) -> Result<()> {
+        self.left.export().chain_err(|| "failed to export the left motor pwm channel")?;
+        self.right.export().chain_err(|| "failed to export the right motor pwm channel")?;
+        self.left.set_period_ns(PERIOD).chain_err(|| "failed to set period on left motor")?;
+        self.right.set_period_ns(PERIOD).chain_err(|| "failed to set period on right motor")
+    }
+
     /// Enables/disables the motor. When disabled they keep their current
     /// speed and their speed can still be set but they will not move until
     /// enabled.
@@ -82,10 +86,8 @@ impl Rover {
     }
 
     /// Unexports the motors so they can no longer be used
-    pub fn unexport(self) -> Result<()> {
-        self.left.enable(false).chain_err(|| "failed to disable left motor")?;
-        self.right.enable(false).chain_err(|| "failed to disable right motor")?;
+    pub fn unexport(&self) -> Result<()> {
         self.left.unexport().chain_err(|| "failed to unexport left motor")?;
         self.right.unexport().chain_err(|| "failed to unexport right motor")
     }

Now we need to add the calls to export and disable in the relevant places in our cli tool, basically after the creation of the rover and before it is unexported.

diff --git a/src/bin/rover-cli.rs b/src/bin/rover-cli.rs
index f0cafaf..bf6a5fa 100644
--- a/src/bin/rover-cli.rs
+++ b/src/bin/rover-cli.rs
@@ -16,6 +16,7 @@ fn run() -> Result<()> {
     let matches = App::from_yaml(yaml).version(crate_version!()).get_matches();
 
     let rover = Rover::new(PWM_CHIP, LEFT_PWM, RIGHT_PWM)?;
+    rover.export()?;
 
     if let Some(_) = matches.subcommand_matches("disable") {
         rover.enable(false)
@@ -36,6 +37,7 @@ fn run() -> Result<()> {
         Ok(())
 
     } else if let Some(_) = matches.subcommand_matches("unexport") {
+        rover.enable(false)?;
         rover.unexport()
     } else {
         println!("{}", matches.usage());

Fixing The Build Tools

Lastly we have renamed or binary from rpizw-rover to rover-cli so we must reflect this change in the create-image script.

diff --git a/create-image b/create-image
index 5d8540c..9c350e8 100755
--- a/create-image
+++ b/create-image
@@ -17,8 +17,8 @@ rpi_tar="ArchLinuxARM-rpi-latest.tar.gz"
 rpi_url="http://archlinuxarm.org/os/${rpi_tar}"
 
 # Check to see if the binary has been built, we check this first to we can bail early.
-if [ ! -f "target/arm-unknown-linux-gnueabihf/release/rpizw-rover" ]; then
-    echo "'target/arm-unknown-linux-gnueabihf/release/rpizw-rover' not found. Have you run 'cargo build --release --target=arm-unknown-linux-gnueabihf'?"
+if [ ! -f "target/arm-unknown-linux-gnueabihf/release/rover-cli" ]; then
+    echo "'target/arm-unknown-linux-gnueabihf/release/rover-cli' not found. Have you run 'cargo build --release --target=arm-unknown-linux-gnueabihf'?"
     exit 1
 fi
 
@@ -63,7 +63,7 @@ tar -xpf "${rpi_tar}" -C ${mount} 2> >(grep -v "Ignoring unknown extended header
 
 # Copy our installation script and other artifacts
 install -Dm755 "${script}" "${mount}/tmp/${script}"
-install -Dm755 "target/arm-unknown-linux-gnueabihf/release/rpizw-rover" "${mount}/usr/local/bin/rpizw-rover"
+install -Dm755 "target/arm-unknown-linux-gnueabihf/release/rover-cli" "${mount}/usr/local/bin/rover-cli"
 
 # Prep the chroot
 mount -t proc none ${mount}/proc

Fixing The ca-certificates-utils Update

There has been a recent change to the upstream package ca-certificates-utils which is detailed here. Until the upstream archlinuxarm update their rootfs we must make a small change to our setup script to stop the initial update from breaking. Add the following to the top of setup just before the first pacman -Syu ....

# Fix for a recent change in ca-certificates-utils this can be removed once upstream rootfs has been update.
# https://www.archlinux.org/news/ca-certificates-utils-20170307-1-upgrade-requires-manual-intervention/
pacman -Syuw --noconfirm
rm -f /etc/ssl/certs/ca-certificates.crt
pacman -Su --noconfirm

Note that you may not require this fix for long, it is just needed until archlinuxarm updates their rootfs images that we download in the create-image script and this might cause issues after they do (at which point we can simply remove the above lines).

Conclusion

We only did some minor tweaks in preparation for building the rovers rest api but will allow me to focus on that in the next post without also worrying about these minor changes. In my next post we will take a look at using the iron web framework to build a simple rest api around our rover module.