MetaProgramming Ruby to Manage Raspberry PI GPIO

In this post, we’re going to examine how we can easily describe all of the possible states for an indicator (think LED light) on a device and how we can most cleanly represent that definition by using metaprogramming ruby to manage the Raspberry Pi GPIO pin state.

What is metaprogramming?

The term sounds fancy, but one can think of it simply as the act of defining code that defines code. There are many examples of this in Ruby on Rails. Anytime you use a macro that will define additional methods, you are metaprogramming ruby.

We’re not just going to use methods someone else has defined to make our life easier, we’re going to write the methods ourselves. In fact, we’re going to create an entire DSL (Domain Specific Language) for modeling an Indicator.

What is a GPIO Pin?

Raspberry PI GPIO pins are specific to a Raspberry Pi, and stand for “General Purpose Input Output” pin. They are physically on the device, and can either be on, off, or what we’ll call a “nil” value. When programming on the RaspberryPI, we can use software to inspect the value of the pin at any given time, as well as change the value.

In our application, setting the value of a pin is actually providing voltage to an LED indicator, so by changing the value from off to on for particular pin(s), we can change the color of an indicator.

Indicator Truth Table

First, let’s examine a quick truth table that shows what state the indicator will be in, when the corresponding Raspberry Pi GPIO pins are configured.

Indicator Name | State  | PIN 1 | PIN 2 | PIN 3 | PIN 4 | PIN 5 |
-----------------------------------------------------------------
Status         | OFF    |       |       |       |       |       |
               | BLUE   | ON    |       |       |       |       |
               | GREEN  | ON    | ON    |       |       |       |
Motor          | OFF    |       |       |       |       |       |
               | YELLOW |       |       | ON    |       |       |
               | RED    |       |       | ON    | ON    |       |
Power          | OFF    |       |       |       |       |       |
               | GREEN  |       |       |       |       | ON    |

In the above table, we have 3 different indicators. “Status” can be off, blue or green; “Motor” can be off, yellow or red; “Power” can be either off or green.

Overall Software Design

For this post, we’re going to just be focusing on a piece of the overall system to manage indicators on the device. The entire solution includes a long-running ruby process that acts as a monitor, to update the Raspberry Pi GPIO pin values every second.

The monitor process was built to simply schedule a Sidekiq worker repeatedly, with the Sidekiq worker actually inspecting the desired state and manipulating the value of GPIO pins so that the physical LED reflects that desired state.

We’re going to use redis to store the state of each indicator at any given point in time, have a service to place an indicator into a particular state (think write to redis) and then the Sidekiq worker will ask redis for the current state and configure the actual hardware.

Simplest Attempt

Our first attempt should indeed be the simplest, so we’re going to write a single class to represent the “Status” indicator. Once we have this written, we can begin abstracting as things become clearer.

In the above example, there are a few things I’d like to point out:

  • I’ve already introduced some concept of defining the possible states, and what those look like, as class level methods
  • We’ve provided the beginning of a method to transition to a particular state

Using this indicator, we might write something like this elsewhere in our codebase to update the state of the indicator.

indicator = StatusIndicator.new
indicator.transition!(StatusIndicator::CHARGED)

We’ll keep this interface as we refactor and refine how we define our Indicator behavior below.

Introducing a Superclass

Knowing that I have more indicators to write (in this example two more, but our real world application had several more), I already know I’m going to want to share as much logic as possible by using inheritance.

With that in mind, I’ll rewrite to introduce a parent class and a subclass.

The most natural refactor to introduce a base class looks something like this:

Examining this example, I can see that for each indicator I’m going to be defining a hash of states descriptions. I also need to have code that defines how to actually configure each Raspberry Pi GPIO pin to place the indicator into service.

Adding a DSL

With the goal of making it easy to define additional indicators, all of which need to define states and how those states are actually rendered, let’s look at introducing the following DSL to define an indicator succinctly.

Let’s take a look at the above class in a few sections to get a better idea of what is going on here.

Defining the States

In the above example, most of the magic occurs within the “define” class-level method that accepts a block.

define do |router|
  router.on OFF, color: :off, blinks: false
end

In this snippet, we’ve introduced a simple way to define what the indicator should look like when it is off. This has replaced the class methods of “renderings” and “states” that we had previously.

Defining GPIO Representation

We didn’t bother coding a way to map a state to actual GPIO values in our simplest examples, but we’ve included that mapping here.

define do |router|
  router.when :blue do
    enable_pin(GPIO_PIN_BLUE)
    disable_pin(GPIO_PIN_GREEN)
  end
end

DSL Benefits

It should be immediately clear, but readability has increased tremendously when viewing our indicator. The class is simply concerned with defining the possible states, and how those would be rendered.

The GPIO definition also maps nicely back to our truth table. You can see that in order to render a blue color, we expect the blue pin to be enabled, and the green pin to be disabled.

Implementing with Meta-Programming

In order to make the magic above happen, we’ll have to define several class methods and attributes to ensure that we can configure our indicator subclass with such ease.

In this parent class, we are creating several features for each subclass to use. The first is the definition of the “define” method:

def self.define(&block)
  router = Router.new
  block.call(router)
  self.state_machine = router
end

This simply provides a way for the subclass to configure an instance of the router class, which is stored on the parent class as a class attribute to be accessed later (called “state_machine”).

The router is a complex little beast, responsible for defining the available states, how those are rendered, and a method to actually perform the rendering.

Below we can see that we’re expecting every Indicator subclass to be able to render itself as off, so that’s included as a default state in the router.

class Indicator::Router
  def initialize
    on(Indicator::OFF, color: :off, blink: false)
  end
end

When defining what an Indicator should do to transition to a state, the router simply accepts a block to be called later on.

class Indicator::Router
  def when(color, &block)
    renderings[color.to_s] = block
  end
end

Now that the router contains the definition of all states and how to render, the Indicator itself defines a method that wraps this up nicely so rendering an Indicator from a particular state is easy.

class Indicator
  def render(status)
    if visually_off?(status)
      turn_off
      return
    end
    case status.color
    when 'red' then turn_red
    when 'green' then turn_green
    when 'yellow' then turn_yellow
    when 'blue' then turn_blue
    end
  end
end

The implementation of “turnred” or “turnblue” is defined dynamically (it’s meta-programming, right?) by finding the block stored earlier when calling “when” on the router, and executing that block.

class Indicator
  %w(off red green yellow blue).each do |state|
    define_method("turn_#{ state }") do
      render_proc = state_machine.renderings[state]
      raise "Unknown how to render color: #{ state } for LED: #{ self.class.name }" if render_proc.nil?
      instance_exec(&render_proc)
    end
  end
end

Putting it all Together

To put all this together, we’ve made it really easy to instantiate a “Router” instance, configure the router, and store that per child class. Then when calling “transition!” or “render” on the child class, the implementation looks at the state_machine definition from the router to execute a proc or lookup details from a Hash to interface with the actual hardware.

Putting to Use

When this definition is complete, what does it look like to render an Indicator from our monitor process? Super simple:

Our final interface to interact with our Indicators boils down to this:

# in our project
indicator = StatusIndicator.new
indicator.charging! # calls transition! storing state in redis
# change to another state, watch the indicator update
indicator.charged!
# in separate monitor (background) process
every 1.second do
  renderer = RenderIndicatorStatus.new
  renderer.perform(indicator.class.name)
end

Testing with Rspec

Testing with RSpec was also a breeze and readable with this approach. We tested the heck out of the base Indicator class as it interfaced with other existing services to manipulate hardware values (using mocks). To test the actual definition of the state and rendering, we wrote a custom matcher as well as some shared examples for a readable test such as:

Sidekiq Deployment and Process Supervision

In order to deploy and use this application code on our Raspberry Pi, we need to ensure that both our monitor process, sidekiq, and even redis are available and running when the device boots. Read our related posts "How to use Sidekiq without Rails" and "How to Manage Ruby Processes at Launch" for much more on monitoring and deploying this software setup on your Raspberry Pi.

Wrapping Up

This was a fun problem to solve for our client, allowing for very readable indicator definitions for peer review, and for any changes that may arise if we decide to configure the LED indicators to perform differently in the future.

Our Products

It takes one to know one - we've walked the walk by building our own products that customers love.

Ready to have a chat?

Contact us to chat with our founder
so we can learn about you and your project.