Porting a Raspberry Pi GPIO programming library from Python to Rust


If you are a Raspberry Pi enthusiast, you should try out the excellent pigpio library for your next project! The list of features it supports is amazing!

Pigpio lets you control the GPIO pins of a Raspberry Pi over a network. It is split into two parts: a server which runs on the Rpi and performs low-level control and a client program (written in Python) which communicates with the server over TCP sockets.

I found the Python client to be a good candidate for a re-write in Rust. It was easy to read and understand, and it had some features which made the port non-trivial: the code was passing around references (which means you will definitely have to fight the borrow checker) and there was a bit of threading and network I/O. This was to be my first Rust “learning” project!

The design of the pigpio Python client program

The Python client has two threads in it.

One thread sends commands to the pigpio server (for example: make pin number 18 go HIGH).

The other thread (“notification thread”) is responsible for triggering callbacks. For example, you can request the pigpio server to inform you when a pin changes state (say from low to high). The server delivers this information as a message on a TCP socket. The notification thread waits on this socket and triggers callback functions when it receives the level-change messages from the server.

Porting the code to Rust

The code for the “command” thread was easy to port as it was mostly repetitive - once you write a function which sends a command to “get the logic level on a pin”, other functions like “make a pin go HIGH/LOW”, “set the PWM pulse width” etc are mostly cut-and-paste.

pub fn set_servo_pulsewidth(&self, user_gpio: u32, 
                            pulsewidth: u32) -> i32 {
    to_i32(_pigpio_command(&self.control_stream,
                          _PI_CMD_SERVO, user_gpio,
                           pulsewidth))
}

pub fn get_servo_pulsewidth(&self, 
                            user_gpio: u32) -> i32 {
    to_i32(_pigpio_command(&self.control_stream,
                          _PI_CMD_GPW, user_gpio, 0))

}

The tough part was to get the “callback” framework working properly. The command thread has to push in “handler functions” into a vector and the notification thread will pull out each handler and execute it upon receiving level change notifications from the server.

Here is a fragment of Rust code which traverses a vector of callbacks and invokes the appropriate handler functions (depending on the pin whose state has changed):

callbacks = t.callbacks.lock().unwrap();
for cb in &(*callbacks) {
    if (cb.bit  & changed) != 0{
        new_level = 0;
        if (cb.bit & level) != 0 { 
            new_level = 1;
        }   
        if (cb.edge ^ new_level) != 0 { 
           (cb.func)(cb.gpio, new_level, tick);
        }   
   }   
}

Simple Python classes can be translated to Rust very easily as struct + impl combinations. Thankfully, the pigpio Python code was composed of simple classes only with no inheritance hierarchies or any other fancy stuff. The Rust struct literal syntax leads to very compact code (I didn’t find the Rust code too noisy compared to Python); for example, here is how you create a new “_callback_ADT” data structure:

impl _callback_ADT {
    fn new(gpio: u32, edge: u32,
           func: CallbackFn) -> _callback_ADT {

        _callback_ADT {
            gpio: gpio,
            edge: edge,
            func: func,
            bit: (1 << gpio),
        }
    }
}

Error handling using Option/Result types also results in very compact code; for example, here is how you look up an environment variable “PIGPIO_ADDR” and if it is not defined, return a default value of “localhost”:

let host = env::var("PIGPIO_ADDR")
          .unwrap_or("localhost".to_string());

The std::net::TcpStream library provides high-level functions for network I/O which are as easy to use as their Python equivalents.

The Rust standard library provided all the utility functions required for this small project, except one. The Python code makes use of the struct module at many places. The same functionality was obtained using the byteorder crate.

Using the library

Installation

The source code is at: https://github.com/pcein/rust-rpi.

Download the code to your Raspberry Pi.

The first step is to build and run (as root) the “pigpiod” server program which does all the low level port manipulations.

cd PIGPIO
make pigpiod
sudo ./pigpiod

The Rust client code can be executed on a remote system (PC/Laptop …) or you can run it on the Raspberry Pi itself.

cd rustgpio
cargo run

Before executing ‘cargo run’, set an environment variable:

export PIGPIO_ADDR=localhost

(if running on a remote system, this should be set to the IP address of the Raspberry Pi running pigpiod).

Hello, World

Here is a simple LED blinking program (LED on GPIO18):

extern crate rustgpio;
use rustgpio::pigpio;

fn test_write() {
    let pi = pigpio::Pi::new();
    pi.set_mode(18, pigpio::OUTPUT);

    loop {
        pi.write(18, 1);
        pigpio::sleep_ms(100);
        pi.write(18, 0);
        pigpio::sleep_ms(100);
    }
}

fn main() {
    test_write();
}

Note: Don’t expect to get precise time delays because each `write’ involves sending data over a socket.

Reading from a GPIO pin

Here is a function which configures an internal pull-up on GPIO18 and then reads from it in a loop:

fn test_read() {
    let pi = pigpio::Pi::new();
    pi.set_mode(18, pigpio::INPUT);
    //enable internal pull-up
    pi.set_pull_up_down(18, pigpio::PUD_UP);
    
    loop {
        println!("{}", pi.read(18));
        pigpio::sleep_ms(100);
    }
}

Testing callbacks

This program assigns ‘cbf’ as a handler to be invoked when a falling edge is detected on pin 18. The program then goes in a loop pulling this pin LOW and then HIGH thereby generating falling and rising edges. You can see ‘cbf’ being invoked for each falling edge.


fn cbf(gpio: u32, level: u32, tick: u32) {
    println!("handler called, gpio: {}, tick: {} ...", 
             gpio, tick);
}

fn test_callback() {
    let mut pi= pigpio::Pi::new();
    pi.set_mode(18, pigpio::INPUT);
    pi.set_pull_up_down(18, pigpio::PUD_UP);
    let _ = pi.callback(18, pigpio::FALLING_EDGE, cbf);
    
    loop {
        pi.set_pull_up_down(18, pigpio::PUD_DOWN);
        pigpio::sleep_ms(500);
        pi.set_pull_up_down(18, pigpio::PUD_UP);
        pigpio::sleep_ms(500);
    }
}

Generating PWM signals

Here is a program which creates an LED dimming effect on pin 18 using PWM signals:

fn pwm_up_down() {
    let mut pi = pigpio::Pi::new();
    let mut pwm_val = 0;
    loop {
        while pwm_val < 256 {
            pi.set_pwm_dutycycle(18, pwm_val);
            pwm_val += 1;
            pigpio::sleep_ms(5);
        }
        pwm_val = 255;
        while pwm_val > 0 {
            pi.set_pwm_dutycycle(18, pwm_val);
            pwm_val -= 1;
            pigpio::sleep_ms(5);
        }
    }
}

Conclusion

The Rust port is incomplete (and not well tested) - it can currently do digital I/O, callbacks, SPI access, PWM/servo control. Pigpiod supports a lot of extra functionality (I2C, serial, waveform generation etc).