I continued working on a side-project I built a year ago with Rust, and it is now at a point at which I can write something about it.

When you own cats, you also own a litter box for said cats which needs to be emptied regularly (at least once a day for two adult cats). Failing this, the cats will give you a look which, in human language, means as much as “I will murder you in your sleep” which is not a very pleasant feeling.

Since I noticed that I tended to forget to empty the litter box rather frequently, I set out to build an annoying device which would ~help me remember~ bother me enough to empty it.

The device and its features

Since I still had (and still have) quite a few Raspberry PI LED strips lying around, I opted to use those to build a visual, color-coded reminder to be placed somewhere near the litter box.

A Raspberry Pi with a LED strip which changes color and eventually starts blinking in red

The main features of the Cat Litter Reminder are as follows:

  • the LED strip should cycle through a few colors (light green, dark green, orange, red) after some time elapses (respectively 8, 12 and 24 hours)
  • failing this, it should start blinking in red (after 26 hours)
  • when a push button is pressed, the state should be reset. In the video above, it is visible that I appear to have misplaced the push button.
  • an additional feature request from my wife was introduced a few weeks after the launch of the device: to turn off the lights at night (a bright red light blinking when getting up in the middle of the night is a bit of a nuisance)

For the first iteration of the project, we’ll therefore need to detect a button press, drive the LED strip and implement a bit of logic around this. Let’s start.

Detecting a button press

In order to detect a button press, we’ll connect one of the free GPIO pins with ground and a 230 Ω resistor.

Raspberry PI GPIO with wires at Ground and pin 5

Using the gpiod crate, we can read the state of the button, like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use gpiod::{Chip, Options};

const GPIO_BUTTON_PIN: u32 = 5;

let chip: Chip = Chip::new("gpiochip0").expect("Cannot open GPIO");
let opts = Options::input([GPIO_BUTTON_PIN]);
let inputs = &chip.request_lines(opts)?;
let values = inputs.get_values([false; 1])?;

// the value is false when the circuit is closed
let is_button_pushed = !values[0]

// do something with this knowledge

Using this approach it is possible to detect when the push button (which I really need to find back) is pushed and released. For simplicity in this project we’ll simply act on a button push (without waiting for a release).

Changing the LED colors

Custom LED strip with connectors to a Raspberry PI GPIO line

The LED strips are using WS281X LEDs. There are several crates out there for driving these LEDs from Rust, I ended up using the rs_ws281x crate, i.e. the Rust bindings for rpi-ws281x library.

At this point you may be wondering why we’re using a whole GPIO line in order to drive the LEDs, since it only requires one pin to drive the strip. The reason is that those strips were specifically designed in order to allow building small Raspberry PI towers for creating distributed systems demonstrations.

A stack of Raspberry PIs with custom LED strips

Access to the LED strip is provided through a Controller which allows to set the color of each individual LED on the strip:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const NUM_LEDS: i32 = 10;
const LED_PIN: i32 = 18;

let controller = ControllerBuilder::new()
    .freq(800_000)
    .dma(10)
    .channel(
        0, // Channel Index
        ChannelBuilder::new()
            .pin(Self::LED_PIN)
            .count(Self::NUM_LEDS)
            .strip_type(StripType::Ws2812)
            .brightness(100) // default: 255
            .build(),
    )
    .build()
    .expect("Could not initialize LED controller")

This controller gives us access to the LEDs in the form of a mutable slice pointing to the color array to be written to the LEDs. In other words, we have to iterate over the slice and set each color individually. Since in our case we want to set all the LEDs to the same color, we’ll just write a function to do just that.

First we’ll wrap the Controller into a custom struct:

1
2
3
4

pub struct RPILedController {
    controller: Controller
}

And then implement a function to set the strip color:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
impl LedController for RPILedController {

    fn set_all_to(&mut self, color: RawColor) -> () {
        let leds = self.controller.leds_mut(0);
        for led in leds {
            *led = color
        }
        self.controller.render().expect("Failed to change LED strip color");
    }
}

Note that RawColor is a type alias for an array of 4 elements:

1
pub type RawColor = [u8; 4];

For example, in order to obtain orange on the strip, we need to use:

1
const ORANGE: RawColor = [0, 60, 255, 0];

Don’t ask me what the color coding of the strip is. It’s not RGB, it’s not CMYK. It’s just weird.

Now that we’ve got programmatic access to the hardware, let’s go and implement the logic for tying everything together.

Tying it all together

The control program holds the following two pieces of information in its state:

  • the timestamp of the last button push. This is assumed to be the time at which the litter box was cleaned the last time (in practice, it could also be the time at which someone got too annoyed at the annoying red blinking pattern)
  • whether the LED strip is currently on or off, which is required in order to implement the annoying red blinking pattern

The current state of the LED strip is computed based on the time elapsed since the last button push. We use an enum with an impl block to represent this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(PartialEq)]
enum LEDStripState {
    LightGreen,
    DarkGreen,
    Orange,
    Red,
    BlinkingRed,
}

impl LEDStripState {
    fn state_from_duration(duration: &Duration) -> Self {
        match duration.num_seconds() {
            0..=7 => LightGreen,
            8..=11 => DarkGreen,
            12..=23 => Orange,
            24..=25 => Red,
            _ => BlinkingRed
        }
    }

    fn controller_color(&self) -> RawColor {
        // returns the color array required to change the LED strip color
    }
}

Grouping the state and the functions associated with it this way makes (in my opinion) for cleaner code.

We hold the state (together with what’s required to drive the LED strip and read the button state) in a struct:

1
2
3
4
5
6
struct Reminder {
    chip: Chip,
    controller: RPILedController,
    last_cleaning_time: DateTime<Utc>,
    is_strip_on: bool
}

Finally, the implementation of the main loop is the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const BLINK_DELAY: time::Duration = time::Duration::from_millis(500);
const LOOP_DELAY: time::Duration = time::Duration::from_millis(1000);

impl Reminder {
    fn run(&mut self) {
        loop {
            self.reset_state_if_button_pushed();

            let now = Utc::now().with_timezone(&Vienna);
            let is_night = now.hour() >= 22 || now.hour() < 7;
            let time_elapsed = Utc::now().signed_duration_since(self.last_cleaning_time);
            let current_state = LEDStripState::state_from_duration(&time_elapsed);

            if is_night && self.is_strip_on {
                // go dark
                self.controller.set_all_to(RPILedController::BLACK);
                self.is_strip_on = false;
            } else {
                if current_state == BlinkingRed {
                    if self.is_strip_on {
                        self.controller.set_all_to(RPILedController::BLACK);
                        self.is_strip_on = false;
                    } else {
                        self.controller.set_all_to(RPILedController::RED);
                        self.is_strip_on = true;
                    }
                } else {
                    self.controller.set_all_to(LEDStripState::controller_color(&current_state));
                }
            }

            if current_state == BlinkingRed {
                sleep(BLINK_DELAY);
            } else {
                sleep(LOOP_DELAY);
            }
        }
    }

    // ...
}

Note that with this approach, we’re going to be setting the LED strip color at each loop iteration, regardless of it having changed or not. This could be improved by including the previous LED strip state as part of the state, but in practice I found that there doesn’t seem to be an adverse effect on the strip (and I kind of suspect the driver to not actually do anything when changing the color to the same color).

What’s next

I built the first version of this project over a year ago when I set out to learn Rust. When I finally decided to write this post, I took a hard look at the old code, threw most of it out, and wrote what is shown in this article - which is much simpler than the first attempt (I think this is a good sign).

You can find the complete source code on GitHub.

What’s next for this project is to create a networked version: having another one of these devices in my office, as well as a few more of them sprinkled all around the flat would help to be really, really annoyed by the red blinking when failing to clean the cat litter box in time (believe it or not, I got used to the red blinking a few weeks, which kind of defeats the entire purpose of this endeavour).

Stay tuned!