So last week's post was similar to today. However, today I play with a bit more interesting RasPi libraries.

The big difference is that we're using software PWM via the wonderful pi-blaster library. It basically latches onto the DMA for the single PWM pin the RasPi offers, and gives you eight pins. It's derived from Richard Hirst's ServoBlaster but it allows a 100% duty cycle. We'll ultimately use this same library / concept to drive a remote controlled vehicle via drb with the Pi, but first things first!

Let's show some code.

The pi-blaster library expects that you send something like this to a device it creates for you:

echo "1=1" > /dev/pi-blaster

That would turn GPIO pin 1 on 100% of the time. The second argument is just a floating point number from 0 to 1.

I'm all about TDD, so I whipped up this spec for how my RGB LED object should work in Ruby:

describe RGBLed do
  it "can be instantiated with three GPIO pin numbers" do
    led = RGBLed.new(red_pin: 0, green_pin: 1, blue_pin: 2)
  end

  describe "setting color intensity" do
    before do
      @led = RGBLed.new(red_pin: 0, green_pin: 1, blue_pin: 2)
    end

    it "allows you to set red anywhere from 0 to 1" do
      @led.red = 1
      @led.red.must_equal 1
    end

    it "allows you to set green anywhere from 0 to 1" do
      @led.green = 1
      @led.green.must_equal 1
    end

    it "allows you to set blue anywhere from 0 to 1" do
      @led.blue = 1
      @led.blue.must_equal 1
    end

    it "does not allow you to set intensity above 1 or below 0" do
      lambda{ @led.red = 2  }.must_raise OutOfRangeException
      lambda{ @led.red = -1 }.must_raise OutOfRangeException
    end
  end

  describe "outputting pi-blaster commands" do
    before do
      @led = RGBLed.new(red_pin: 0, green_pin: 1, blue_pin: 2)
      @led.red   = 0.5
      @led.green = 0.75
      @led.blue  = 1
    end

    it "handles red" do
      @led.pi_blast_command(:red).must_equal "echo 0=0.5 > /dev/pi-blaster"
    end

    it "handles green" do
      @led.pi_blast_command(:green).must_equal "echo 1=0.75 > /dev/pi-blaster"
    end

    it "handles blue" do
      @led.pi_blast_command(:blue).must_equal "echo 2=1 > /dev/pi-blaster"
    end
  end
end

Then it was just a matter of making the spec work, so this class satisfies that spec:

class OutOfRangeException < StandardError; end

class RGBLed
  def initialize(options={})
    @options   = options
    @pins = {
      red:   fetch_option(:red_pin),
      green: fetch_option(:green_pin),
      blue:  fetch_option(:blue_pin)
    }
    @intensity = {
      red:   0,
      green: 0,
      blue:  0
    }
  end

  def red=(value)
    set_intensity(:red, value)
  end

  def red
    get_intensity(:red)
  end

  def green=(value)
    set_intensity(:green, value)
  end

  def green
    get_intensity(:green)
  end

  def blue=(value)
    set_intensity(:blue, value)
  end

  def blue
    get_intensity(:blue)
  end

  def pi_blast_command(channel)
    "echo #{get_pin(channel)}=#{get_intensity(channel)} > /dev/pi-blaster"
  end

  def pi_blast
    %w(red green blue).each do |c|
      `#{pi_blast_command(c)}`
    end
  end

  private
  def fetch_option(channel)
    @options.fetch(channel)  { raise "Must specify `#{channel.to_s}` option." }
  end

  def set_intensity(channel, value)
    validate_channel(channel)
    validate_intensity(value)
    @intensity[channel.to_sym] = value
  end

  def get_intensity(channel)
    validate_channel(channel)
    @intensity[channel.to_sym]
  end

  def get_pin(channel)
    validate_channel(channel)
    @pins[channel.to_sym]
  end

  def validate_channel(channel)
    return true if valid_channels.include?(channel.to_s)
    raise "Invalid attribute channel: #{channel}"
  end

  def validate_intensity(intensity)
    return true if valid_intensities.cover?(intensity)
    raise OutOfRangeException, "Invalid intensity: #{intensity}"
  end

  def valid_channels
    %w(red green blue)
  end

  def valid_intensities
    (0..1)
  end
end

As I mentioned in the video, the RGB LED we had on hand actually took ground to the channels, and positive voltage to the common pin, which went against my assumptions. Basically, setting red to 1 would mean off, and 0 would mean on. OOP is super-sweet, so this class fixes that up for me:

class InverseRGBLed < RGBLed
  def get_intensity(channel)
    1-super
  end
end

Finally (on the ruby side), we needed to expose him to the network. This script does that for me, as well as initializes him to blue.

require_relative '../lib/inverse_rgb_led.rb'

led = InverseRGBLed.new(red_pin: 0, green_pin: 1, blue_pin: 4)
led.pi_blast

led.red=0
led.green=0
led.blue=1
led.pi_blast

require 'drb/drb'
require 'pi_piper'

# The URI for the server to connect to
URI="druby://192.168.1.76:8787"

# The object that handles requests on the server
FRONT_OBJECT=led

$SAFE = 1   # disable eval() and friends

DRb.start_service(URI, FRONT_OBJECT)
# Wait for the drb server thread to finish before exiting.
DRb.thread.join

Then on the android side, this main activity will handle pretty much everything.

package com.isotope11.rgbledapp;

import java.util.ArrayList;
import java.util.List;

import org.jruby.embed.ScriptingContainer;

import android.app.Activity;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;

import com.commonsware.cwac.colormixer.ColorMixer;
import com.commonsware.cwac.colormixer.ColorMixer.OnColorChangedAndStoppedListener;

public class MainActivity extends Activity {

  protected final String TAG = MainActivity.class.toString();
  protected ScriptingContainer mRubyContainer;
  protected String mDrbResult;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    System.setProperty("jruby.bytecode.version", "1.5");
    mRubyContainer = new ScriptingContainer();
    List<String> loadPaths = new ArrayList<String>();
    loadPaths.add("jruby.home/lib/ruby/shared");
    loadPaths.add("jruby.home/lib/ruby/1.8");
    mRubyContainer.setLoadPaths(loadPaths);

    final Activity lol = this;

    ColorMixer colors = (ColorMixer) findViewById(R.id.mixer);
    colors.setOnColorChangedAndStoppedListener(new OnColorChangedAndStoppedListener() {
      @Override
      public void onColorChange(int argb) {
        int r = Color.red(argb);
        int g = Color.green(argb);
        int b = Color.blue(argb);
        RGBLedSetter task = new RGBLedSetter();
        task.setRed(r);
        task.setGreen(g);
        task.setBlue(b);
        task.execute();
      }
    });
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
  }

  private class RGBLedSetter extends AsyncTask<Object, Void, String> {

    int mRed = 0;
    int mGreen = 0;
    int mBlue = 0;

    @Override
    protected String doInBackground(Object... arg0) {
      String rubyDrbClient = "require 'drb/drb';SERVER_URI='druby://192.168.1.76:8787';DRb.start_service;led = DRbObject.new_with_uri(SERVER_URI);led.red=" + getMapped(mRed) + ";led.green=" + getMapped(mGreen) + ";led.blue=" + getMapped(mBlue) + ";led.pi_blast;";
      mRubyContainer.runScriptlet(rubyDrbClient);
      return "nope";
    }

    @Override
    protected void onPostExecute(String result){
      mDrbResult = result;
    }

    public void setBlue(int val){
      mBlue = val;
    }
    public void setGreen(int val){
      mGreen = val;
    }
    public void setRed(int val){
      mRed = val;
    }
    public String getMapped(int val){
      float newVal = Float.valueOf(val) / Float.valueOf(255);
      Log.d(TAG, Float.toString(newVal));
      return Float.toString(newVal);
    }
  }
}

If you are interested, here are some references:

Josh Adams is a developer and architect with over eleven years of professional experience building production-quality software and managing projects. Josh is isotope|eleven's lead architect, and is responsible for overseeing architectural decisions and translating customer requirements into working software. Josh graduated from the University of Alabama at Birmingham (UAB) with Bachelor of Science degrees in both Mathematics and Philosophy. He also occasionally provides Technical Review for Apress Publishing, specifically regarding Arduino microprocessors. When he's not working, Josh enjoys spending time with his family. <a href="http://www.erlang-factory.com/conference/show/conference-6/home/"><img src="http://www.erlang-factory.com/static/upload/media/1389191028314604speaker120x125gif" alt="speaker badge" /></a>