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.
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:
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:
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:
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:
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.
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:
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