Erlang

Rebuilding the Mission Impossible security system in Elixir on RaspberryPi

by Ju Liu

Learn more about how Erlang Solutions can support you with Elixir Development or sign up to our mailing list to be the first to know about our future blog posts.


Yes, you’ve read that right. In this tutorial we are going to rebuild the amazing security system featured in the 1996 all time classic Mission Impossible. We will use a Raspberry Pi, lots of sensors and we’ll write the code in Elixir.

Hipster Standing Desk

Just a quick refresher for those who haven’t seen the movie. Ethan Hunt is a super spy trying to infiltrate the CIA headquarters in order to steal a valuable list of double-agents. Unfortunately, the list is safely stored in a highly secure bunker with the following security mechanisms:

  • - Laser beams
  • - Temperature sensors
  • - Noise sensors
  • - Ground vibration sensors

Preparation

Before we start, let me give you the best advice I received when I started developing on a Raspberry Pi: get yourself a USB to TTL serial cable! You can find them on adafruit (link, tutorial) and these little devices will save you the trouble of having to connect an external monitor, a keyboard and a mouse in order to use your Raspberry. Just connect the cable, fire up screen and boom you’re in.

Now we need some sensors to build our security system, and I’ve found a set made by Sunfounder that has everything that we need and even more (link). I’m in no way affiliated with the company, but they posted all the C and Python code to control them on github so I think they’re pretty cool.

The last thing we need is Elixir! We can install it on our Raspberry Pi following this tutorial.

Let’s get started

We can now create the project using our beloved mix:

$ mix new intrusion_countermeasures

and add elixir_ale as a dependency in our mix.exs file:

defmodule IntrusionCountermeasures.Mixfile do
  use Mix.Project

  def project do
    [app: :intrusion_countermeasures,
     version: "0.1.0",
     elixir: "~> 1.4",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps()]
  end

  def application do
    [extra_applications: [:logger],
     mod: {IntrusionCountermeasures, []}]
  end

  defp deps do
    [{:elixir_ale, "~> 0.5.6"}]
  end
end

This library will provide us the abstractions for controlling the Raspberry GPIO pins and I2C bus. You can find out more about the library here. So let’s install and compile:

$ mix deps.get && mix compile

First things first: a laser!

Grab your laser emitter module and connect it to the breadboard in this way:

Laser

Now we can start writing the code for our security system:

defmodule IntrusionCountermeasures do
  use Application

  def start(_, _) do
    {:ok, laser} = Gpio.start_link(17, :output)
    pid = spawn(fn -> loop(laser) end)
    {:ok, pid}
  end

  def loop(laser) do
    :timer.sleep(200)
    turn_on(laser)

    :timer.sleep(200)
    turn_off(laser)

    loop(laser)
  end

  defp turn_on(pid) do
    Gpio.write(pid, 0)
  end

  defp turn_off(pid) do
    Gpio.write(pid, 1)
  end
end

We connect the GPIO pin 17 using Gpio.start_link, specifying we’re using it as an output. Then we spawn a recursive loop function which repeatedly turns the laser on and off. We can run our app with iex -S mix and the laser will start blinking. How cool is that?

Also note that the default behaviour is to write 0 for turning something on and 1 for turning it off. To make the code easier to understand I just added the turn_on and turn_off helpers.

Here’s a picture of the setup on my desk:

Laser desk

Back to analog

Now that we have the laser blinking, we can add a sensor which measures how much light shines through it, also known as a photoresistor. The gotcha is that this is an analog sensor, so we need an analog-to-digital converter to be able to read off the digital value in our program. Here’s the diagram:

Laser and sensor

In order to connect to the AD converter in our program, we have to use the I2C bus. Unfortunately the I2C bus isn’t enabled by default, so we’ll have to do it ourselves:

$ sudo raspi-config
# Choose Interfacing Options
# Choose I2C
# Choose Enable
$ sudo reboot

As soon as the Raspberry reboots, you can connect the circuitry and check that the bus is visible:

$ ls /dev/i2c*
/dev/i2c-1
$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- 48 -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

Amazing, we can see that the system recognises the AD converter and gives it a certain bus address (48). We can now connect our photoresistor output with one of the inputs of the AD converter (AIN0 in this example).

Here’s a picture of the circuitry setup:

Laser and Photoresistor board

It’s time to update the code!

defmodule IntrusionCountermeasures do
  use Application

  def start(_, _) do
    {:ok, laser} = Gpio.start_link(17, :output)
    {:ok, sensors} = I2c.start_link("i2c-1", 0x48)

    turn_on(laser)

    pid = spawn(fn ->
      loop(%{laser: laser, sensors: sensors})
    end)

    {:ok, pid}
  end

  def loop(%{laser: laser, sensors: sensors} = state) do
    :timer.sleep(200)
    IO.puts read_channel(sensors, 0)
    loop(state)
  end

  defp turn_on(pid) do
    Gpio.write(pid, 0)
  end

  defp turn_off(pid) do
    Gpio.write(pid, 1)
  end

  defp read_channel(pid, channel) do
    {channel_value, _} = Integer.parse("#{channel + 40}", 16)
    I2c.write(pid, <<channel_value>>)
    <<value>> = I2c.read(pid, 1)
    value
  end
end

As you can see, we now connect to the I2C bus using the address we found and pass the process PID to the loop. We’ve added a magic read_channel function that is able to read off the value of the photoresistor. You don’t really need to understand how it works, I’ve mostly copied it from the Python implementation while glancing at the datasheet of the ADC converter.

If we run the app with iex -S mix, align the beam (pun intended) we should see this sort of output.

iex(1)> 3
4
3
5
3

But if we interrupt the beam with our finger we should see the value rise.

iex(1)> 3
4
3
74
76
77

It works! Now we can just replace the loop function with this:

def loop(%{laser: laser, sensors: sensors} = state) do
  :timer.sleep(200)

  value = read_channel(sensors, 0)
  if value > 40 do
    IO.puts "[ALARM] Laser triggered! Value was #{value}"
  end

  loop(state)
end

And we should see something like this:

iex(1)> 3
4
3
[ALARM] Laser triggered! Value was 69
[ALARM] Laser triggered! Value was 73
[ALARM] Laser triggered! Value was 72
3
3
3

Nice! We can pat ourselves on the back and go for a walk and a cup of coffe.

Alarms alarms alarms

What security system would be complete without a blazing, ear-deafening, blasting alarm? In our case we are going to use an active buzzer.

Active buzzer

By this time, it should be pretty easy to connect it to the board. We are going to use GPIO pin 18 and change the start function to look like this:

def start(_, _) do
  {:ok, laser} = Gpio.start_link(17, :output)
  {:ok, alarm} = Gpio.start_link(18, :output)
  {:ok, sensors} = I2c.start_link("i2c-1", 0x48)

  turn_on(laser)

  pid = spawn(fn ->
    loop(%{laser: laser, alarm: alarm, sensors: sensors})
  end)

  {:ok, pid}
end

and the loop function to this:

def loop(%{alarm: alarm, sensors: sensors} = state) do
  :timer.sleep(200)

  value = read_channel(sensors, 0)
  if value > 40 do
    alarm("[ALARM] Laser triggered! Value was #{value}", state)
  end

  turn_off(alarm)
  loop(state)
end

defp alarm(message, %{alarm: alarm} = state) do
  turn_on(alarm)
  IO.puts message
  loop(state)
end

If we interrupt the laser beam with our finger, we should hear the buzzer beeping. Hooray!

All the sensors!

Now that we have the basic structure for our security system, we just need to add the other sensors and wire them up to finish it. We are going to add:

  • - A temperature sensor (thermistor)
  • - A sound sensor
  • - A vibration sensor

We are going to connect the first two analog sensors to the AIN1 and AIN2 inputs of our AD converter, while we are going to connect the last one to the GPIO pin 27. Here is how the setup looks now:

Final board

And here is the full code:

defmodule IntrusionCountermeasures do
  use Application

  def start(_, _) do
    {:ok, laser} = Gpio.start_link(17, :output)
    {:ok, alarm} = Gpio.start_link(18, :output)
    {:ok, sensors} = I2c.start_link("i2c-1", 0x48)
    {:ok, vibration} = Gpio.start_link(27, :input)

    turn_on(laser)

    pid = spawn(fn ->
      loop(%{laser: laser, alarm: alarm, sensors: sensors, vibration: vibration})
    end)

    {:ok, pid}
  end

  def loop(%{alarm: alarm, sensors: sensors, vibration: vibration} = state) do
    :timer.sleep(200)

    # Laser
    value = read_channel(sensors, 0)
    if value > 40 do
      alarm("[ALARM] Laser triggered! Value was #{value}", state)
    end

    # Temperature
    value = read_channel(sensors, 1)
    if value < 120 do
      alarm("[ALARM] Temperature triggered! Value was #{value}", state)
    end

    # Noise
    value = read_channel(sensors, 2)
    if value < 50 do
      alarm("[ALARM] Noise triggered! Value was #{value}", state)
    end

    # Vibration
    value = Gpio.read(vibration)
    if value == 0 do
      alarm("[ALARM] Vibration triggered! Value was #{value}", state)
    end

    turn_off(alarm)
    loop(state)
  end

  defp alarm(message, %{alarm: alarm} = state) do
    turn_on(alarm)
    IO.puts message
    loop(state)
  end

  defp turn_on(pid) do
    Gpio.write(pid, 0)
  end

  defp turn_off(pid) do
    Gpio.write(pid, 1)
  end

  defp read_channel(pid, channel) do
    {channel_value, _} = Integer.parse("#{channel + 40}", 16)
    I2c.write(pid, <<channel_value>>)
    <<value>> = I2c.read(pid, 1)
    value
  end
end

Let’s take it for a spin!

$ iex -S mix
Erlang/OTP 18 [erts-7.3] [source] [smp:4:4] [async-threads:10] [kernel-poll:false]

Interactive Elixir (1.4.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> [ALARM] Laser triggered! Value was 74
[ALARM] Laser triggered! Value was 75
[ALARM] Laser triggered! Value was 74
[ALARM] Temperature triggered! Value was 119
[ALARM] Temperature triggered! Value was 118
[ALARM] Temperature triggered! Value was 117
[ALARM] Temperature triggered! Value was 119
[ALARM] Noise triggered! Value was 0
[ALARM] Noise triggered! Value was 10
[ALARM] Noise triggered! Value was 0
[ALARM] Vibration triggered! Value was 0
[ALARM] Vibration triggered! Value was 0
[ALARM] Vibration triggered! Value was 1

So there we have it! In 70 lines of code we implemented a full fledged Mission Impossible security system, with laser, temperature, noise and vibration detection.

One more thing..

So now that we have our app working in Raspbian, wouldn’t it be cool to flash it to a SD card so that as soon as we connect our Raspberry, our security system comes online automatically?

Well, we can! Thanks to the amazing nerves project, it’s super easy to do. Nerves will take care of all the hard parts of working on an embedded device, such as cross compiling the application, optimizing the runtime environment and even flashing the image to the SD card.

First of all, we need to install it on our machine following this simple tutorial.

Now on we can create our project specifying the desired target (rpi2 for the Raspberry Pi 2 and rpi3 for Raspberry Pi 3).

$ mix new intrusion_countermeasures --target rpi3

And add elixir_ale as a dependency in our mix.exs configuration file:

def application do
  [mod: {IntrusionCountermeasures, []},
   applications: [:logger, :elixir_ale]]
end

def deps do
  [{:nerves, "~> 0.4.0"},
   {:elixir_ale, "~> 0.5.6"}]
end

If we replace lib/intrusion_countermeasures.ex with our program, we can get all dependencies and compile them as an app:

$ mix deps.get && mix compile
[...]
Generated intrusion_countermeasures app

Amazing! We can now build the firmware image with a simple:

$ mix nerves.release.init && mix firmware

If that worked, we should be able to see an image in the _images/rpi3 folder:

$ ls -lh _images/rpi3/
total 39912
-rw-r--r--  1 juliu  staff    19M Jan 10 12:11 intrusion_countermeasures.fw

We also can see that the image is only 19 megabytes. Let’s insert an SD card and burn the image (this will completely erase the SD card as well):

$ mix firmware.burn
Use 3.69 GiB memory card found at /dev/rdisk2? [Yn]
100%
Elapsed time: 8.307s

Now we can pop out our SD card and insert it into our Raspberry Pi and it should start the app as soon as the kernel finishes booting!

For the curious

Here’s a video of me doing the same app live on stage at last year’s Elixir London conference:


Hope you had fun following this tutorial, feel free to comment if you bump into any issues :)


Learn more about how Erlang Solutions can support you with Elixir Development or sign up to our mailing list to be the first to know about our future blog posts.

Go back to the blog

×

Thank you for your message

We sent you a confirmation email to let you know we received it. One of our colleagues will get in touch shortly.
Have a nice day!