Blog

RPi & Rails Occupancy Detector, V1.0

May 21, 2014    Rails Raspberry Pi

A challenge was issued to see who could come up with the best Raspberry Pi hack for our coworking space. When there, I find myself walking all the way down the hall to use the restroom, only to see that it’s occupied!

Here I’m going to build a very simple restroom occupancy detector. It’s a working prototype, but it’s probably not quite advanced enough for prime time. Here’s a look at the finished product:

Unfortunately, it would be overwhelming to try and give a comprehensive step by step walkthrough, so I’ll only be covering the major points. All of the tools & software that I’ll be using are well documented, and don’t be afraid to ping me with any questions!

The complete source code is available on Github.

Parts list:

  • Raspberry Pi (NOOBS OS installed)
  • Magnetic door switches
  • Miscellaneous electrical stuff - breadboard, jumpers, resistors, multi-meter, etc. Check out SparkFun!
  • Wireless ethernet adapter (only necessary if you don't have an ethernet connection available).

Software we’ll be using:

Playing with the magnetic door switches

First up, let's check out the magnetic door switches. These particular ones are extremely simple. Essentially, when the two pieces are apart, the switch is OPEN. When the pieces come together, however, the internal magnetics are such that the switch is CLOSED.

To be sure we're correct, we can set our multi-meter to check for continuity (usually a "sound wave" icon, since it will beep when the circuit is complete).

When the pieces are brought together, the circuit is complete and the multi-meter beeps.

Occupancy Status with Pied Piper and Redis

On the Raspberry Pi, you'll need to install a recent version of Ruby and Rails. I used Ruby 2.1.1 and Rails 4.0.5. Also, you'll need to install Redis and add the pi_piper, puma, and redis gems.

Gemfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
source 'https://rubygems.org'
ruby '2.1.1'

gem 'rails', '4.0.5'
gem 'sqlite3'

gem 'sass-rails', '~> 4.0.3'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.0.0'

gem 'jquery-rails'

gem 'redis'
gem 'puma'
gem 'pi_piper'

Within the rails directory, I created a file named listener.rb. We'll start this file up in order to listen for Raspberry Pi input/output pins. Here's what it looks like.

listener.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require "pi_piper"
require 'redis'

redis = Redis.new

PiPiper.watch(pin: 7, direction: :in) do
  # 1 - door closed
  # 0 - door open

  if value == 1
    redis.set('status', "occupied")
  elsif value == 0
    redis.set('status', "available")
  end

  puts "Pin changed from #{last_value} to #{value}"
end

PiPiper.wait

This is setting up RPi pin 7 as an INPUT pin. When the pin's value changes, we update a Redis entry to reflect the updated status. Pretty simple.

You'll need privledged access for this, so I'm running it like this: bundle exec rvmsudo ruby listener.rb. Now for the circuit.

I'm getting the 3.3V and GND from the available RPi pins.

The 330Ω resistor is in there to protect the RPi input incase I set it to output by accident (note, I don't know how well the resistor actually protects the Pi).

The 10KΩ resistor is a pull-down resistor. When the switch is OPEN, the Pi input will be fed by ground through the 10KΩ resistor. When the switch is CLOSED, the 3.3V will overpower the ground due to the resistance values, and the Pi input will be HIGH.

If you've done everything right, you should see the value changing as you move the sensors together and apart:

Displaying the status

Because we’ve now got the status saved in Redis, we can display it very through a Rails action!

routes.rb
1
2
3
4
DoorStatusSimple::Application.routes.draw do
  root to: "pages#status"
  get :updates, to: "pages#updates"  # we'll use this later
end

For now, our pages controller is very simple. We'll just fire up Redis and grab the occupancy status.

pages_controller.rb
1
2
3
4
5
6
7
class PagesController < ApplicationController
  def status
    redis = Redis.new
    @status = redis.get("status") || "unknown"
  end
end

Next, we'll add the template and some styling to bring it together:

status.html.erb
1
2
3
<div class="status" data-status="<%= @status %>">
  <span class="smaller">Restroom is currently</span>
</div>
status.scss
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
[data-status] {
  border-radius: 5px;
  width: 600px;
  margin: 50px auto 0 auto;
  color: #FFF;
  text-align: center;
  font-size: 90px;

  .smaller {
    padding: 4px 0;
    color: #333;
    border-top-left-radius: 5px;
    border-top-right-radius: 5px;
    background-color: #FFF;
    display: block;
    font-size: 20px;
  }
}

[data-status=occupied] {
  background-color: red;
  border: 2px solid red;
  &:after {
    content: "OCCUPIED"
  }
}

[data-status=available] {
  background-color: green;
  border: 2px solid green;
  &:after {
    content: "AVAILABLE"
  }
}

When you start the Rails server on the Pi and visit “#{PI_IP_ADDRESS}:3000”, you should see something like this:

Unfortunately, the state is fixed and you need to to refresh the page each time you want to see an updated restroom status. That’s OK, but let’s see if we can do better…

Live (as in ActionController::) status updates

For fun, I'm going to use ActionController::Live to stream occupancy updates to the browser.

First, I'll need to update the listener to send Redis publications about the status. I'm still going to keep the sets so that when we first visit the page we'll get the last known status.

listener.rb
1
2
3
4
5
6
7
8
9
10
11
12
PiPiper.watch(pin: 7, direction: :in) do
  # 1 - door close
  # 0 - door open

  if value == 1
    redis.set('status', "occupied")
    redis.publish('status_update', "occupied")
  elsif value == 0
    redis.set('status', "available")
    redis.publish('status_update', "available")
  end
end

Then I'm going to add the updates controller action.

pages_controller.rb
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
class PagesController < ApplicationController
  include ActionController::Live

  def status
    redis = Redis.new
    @status = redis.get("status") || "unknown"
  end

  def updates
    response.headers["Content-Type"] = "text/event-stream"

    redis = Redis.new
    redis.subscribe('status_update') do |on|
      on.message do |event, data|
        if %w(available occupied).include?(data)
          data = { event: "status_update", status: data }
          response.stream.write("data: #{data.to_json}\n\n")
        end
      end
    end
  rescue IOError
    logger.info "Stream closed"
  ensure
    response.stream.close
  end
end

We’ll subscribe to the Redis “status_update” that we publish in listener.rb, and when a message is received we’ll check if it’s either “available”, or “occupied” - the two statuses that we expect. If so, we’ll write out a bit of JSON to the stream.

Check out the Railscast on ActionController::Live for more information.

Finally, some JavaScript to bring it together.

updates.js.coffee
1
2
3
4
5
6
7
8
9
source = new EventSource('/updates')
source.addEventListener 'message', (e) ->
  data = JSON.parse(e.data)

  if data.event == "status_update"
    $("[data-status]").attr("data-status", data.status)

  console.log e
  console.log data

We’ll add the /updates path as a source, and then listen for messages. When a status_update message comes along, we’ll just change the status data attribute. Updating this data attribute will change the occupancy styling/text.

Check it out:

Shortcomings & Looking to the next prototype

While cool, this prototype is unfortunately not very practical. For venues with multiple restrooms, we don’t really want to have a separate Raspberry Pi for each. Also, the RPi will require an electrical and ethernet outlet (or wireless adapter).

My plan is to improve on this design by introducing small microprocessors and wireless transceivers into the mix. Each door, instead of having a RPi, would have a small, cheap microprocessor (think ATTiny) and talk to the RPi through a transceiver.

I’ll surely post an article about the next prototype when it’s completed. Be sure to subscribe with the form below so you don’t miss it!