An Elixir Nerves Thermometer
Introduction
In a previous article, I wired up a raspberry pi to a DHT-11 temperature sensor. The initial goal was to get something working with Nerves, my favorite IoT framework of choice. However, I was unable to get a library that worked successfully so I opted for Python and C using the Adafruit library. Yet, I always intended to circle back to Nerves and Elixir.
Finally, with some help from the community, I have something that resembles success. So for this article, we’ll be focusing on mostly pure elixir to set up a thermometer using the DHT-11. You might also have better luck with the DHT-22. The code and wiring should be identical.
Hardware and Prerequisites
To do this project, you’ll need to grab all the hardware from my previous article here:
You’ll need to hook up everything the way its described above and you might just want to test it out using Python. Once you have done that, circle back here and we’ll run through Nerves.
Also, I’m using Elixir 1.9.1 and Erlang 22.0.7 at the time of this writing with Nerves 1.5 so keep that in mind. I do have a nice “Getting Started with Nerves” two-part article if you need to brush up on the basics:
Once you have the hardware working and a basic understanding of elixir/nerves, you can proceed below.
A Word of Warning
I have spent a lot of time getting this working. The libraries and hardware for the DHT-11 seem a bit…tempermental. I suggest buying both a DHT-11 and a DHT-22 (they’re cheap!) and try it either way. I might give that a shot at a future date and come back and update this article. In any case, I did get it working but just keep in mind it did require some experimentation.
Getting Started
Like any old Nerves project, we get started at the terminal using mix
:
mix nerves.new nerves_thermo
cd nerves_thermo
export MIX_TARGET=rpi2
This is all basic stuff here, making a new project and setting our MIX_TARGET to raspberry pi 2. If you have a different device, just change MIX_TARGET=rpi2
to rpi3
, rpi0
etc.
Next we need a couple of libraries. First, I want some networking. So i’m going to add nerves_network
. Also, I’m going to need a library to do the heavy lifting. I received quite a bit of help from jcarstens
using his library jjcarstens/nerves_dht
. I tried a bunch of different ones and Jon’s is a port of someone elses, but his was the only one I could get working with the DHT-11. In any case, I added these two lines to my mix.exs
file:
# Does the dirty work of streaming in the bits for the temp/thermo
{:nerves_dht, github: "jjcarstens/nerves_dht"},
# For network access
{:nerves_network, "~> 0.5"}
I now need to configuring my networking. I’m just plugging in with ethernet, but you can also use wifi or a USB cable to connect to your Pi. In fact, you don’t need the networking library if you’re using USB (see my getting started articles above for details), but it’s handy to know so I’m including it here. In any case we just need to modify config/config.exs
. Add the following lines at the bottom:
config :nerves_network, :default,
eth0: [
ipv4_address_method: :dhcp
]
If you are using Wifi or something other than DHCP like myself, your setting will differ and you’ll need to include things like SSID and such.
github.com/nerves_project/nerves_network
The basics are all there, lets make sure we have a good build:
mix deps.get
mix compile
If you didn’t get any errors from that, keep going.
Elixir Code
Next we need to edit or create the file in lib/nerves_thermo.ex
(I cannot remember if mix creates this for your or not.). Your file should look like this:
defmodule NervesThermo do
use NervesDht
def init do
dht = NervesThermo.start_link({26,11})
end
def listen({:ok, p, s, h, t}) do
IO.puts("Listen event on MyGenServer")
IO.puts("Pin: #{p}, Sensor: #{s}")
IO.puts("Temperature: #{t} Humidity: #{h}\n")
Process.sleep 2000
end
def listen({:error, error}) do
IO.puts("Listen event on MyGenServer")
IO.puts("Error: #{error}\n")
Process.sleep 2000
end
end
The elixir code is fairly simple. Basically we just use NervesDht
which will import the module in our own. The NervesDht module turns the NervesThermo
module into a GenServer which we can use in two ways. We can use the listen
functions which act as callbacks from the Dht module, telling us when there’s an error or delivering our temp and humidity readings. Or we can manually request a reading using the info
function.
The init
function is just a time-saver. It starts our GenServer using pin 26, the same pin I was using in my previous article on the DHT-11. If you’re wiring it up to a different pin, you’ll need to swap it out. The “11” is for the DHT-11. If you have a DHT-22, use “22” instead. So if we were using pin 13 on a DHT-22 for example, our init
would instead look like this:
dht = NervesThermo.start_link({13,22})
The listen
function is essentially a callback from the Dht library. It allows us to accept readings from the sensor. We define two versions of it using elixir’s lovely pattern matching which lets us trap errors separate from data, without if
statements or conditionals.
Initially, I commented out the listen
functions and just called NervesThermo.info
which is predefined by the library for us. This works for quick testing, but I was getting a lot of nil
values instead of actual readings. You might try deploying the code both ways to see the difference (just put the #
sign in front of each line of the listen functions.) The problem with deploying this with our listen
functions intact is that once we call init
from the command line on the Pi, it will just keep running and outputting data to us whether we like it or not. We have to stop the GenServer process in order to quit getting data, which is annoying. I prefer to query data ad hoc rather than being spammed by the console. On the other hand, I have found the listen
function to be more reliable. Since we have two versions of the function listen({:ok, p,s,ht,})
and listen({:error, error})
we will get notified if there’s an error code. The info
function doesn’t do that.
Finally, at the end of each function, I added Process.sleep
in order to pause for two seconds between intervals. I’m honestly not 100% sure if this is the right way to do this, but I’m an Elvis programmer and it seemed to work. We’re only supposed to access the DHT-11 every 2 seconds anyway, so we want this pause in there to avoid hammering it with requests.
This is really all the code you need. The NervesDht library is doing most of the work here by observing changes in voltage over time and translating that into a temperature and humidity reading.
When we’re ready to deploy, we can burn to our SD card like so:
mix firmware.burn
and then plug the SD card into the Pi and power on.
Or, if you’re using the USB cable with a Raspberry Pi that supports Composite Gadget mode (my old Pi2 does not), you can just use:
mix firmware.gen.script
which will generate our upload script, then we can run that script to deploy via USB cable:
./upload.sh
Once this has finished, you should SSH to your Pi or plug it into a keyboard, mouse and monitor.
Getting Temperature Readings
Now the moment of truth. Assuming we haven’t accidentally messed up any cables or components from the last article we should be able to fire up our GenServer and see some readings. On our Pi at the iex>
command prompt, we type the following to start our GenServer:
{:ok, dht} = NervesThermo.start
We should get:
{:ok, #PID<0.1147.0>}
This starts the GenServer and pattern matches on the :ok
atom which should be part of the return value. Then our dht
variable will hold the process associated with the NervesThermo GenServer. Every couple of seconds we should see this:
Listen event on MyGenServer
Error: -1
Listen event on MyGenServer
Pin: 26, Sensor: 11
Temperature: 22.0 Humidity: 47.0
Listen event on MyGenServer
Error: -1
Listen event on MyGenServer
Error: -1
I get a mix of sensor readings and errors like the above message. This should mean everything is working. Sometimes it looks like the DHT gets into an invalid state and produces errors, but for the most part it should work. In a real world application, I would probably only get a reading a few times every minute, throughout the day. Those readings would be averaged out by hour, sent to an API, a database or your mom. There’s no need to get the temperature every second unless you’re doing something very sensitive, in which case you’ll want a more precise sensor than the DHT-11 anyway!
When you want to stop the GenServer just run the following command:
Process.exit dht, :kill
This tells elixir to kill our GenServer process and we should stop seeing messages. If we want to see messages again, we’ll have to use the init
function to start a new process with our GenServer.
Gotchas
I ran into a couple of different problems when working with Nerves and the DHT-11. The library in question had an issue where it would not recompile the C code automatically. This was especially problematic when working with VSCode and the elixir-ls add-on. I submitted a github issue here: https://github.com/esdrasedu/nerves_dht/issues/3
/Users/sevus/.nerves/artifacts/nerves_toolchain_arm_unknown_linux_gnueabihf-darwin_x86_64-1.2.0/bin/arm-unknown-linux-gnueabihf-gcc src/common_dht_read.o src/Raspberry_Pi_2/pi_2_dht_read.o src/Raspberry_Pi_2/pi_2_mmio.o src/_Raspberry_Pi_2_Driver.o -o priv/nerves_dht -L/Users/craigbowes/.nerves/artifacts/nerves_system_rpi2-portable-1.8.2/staging/usr/lib/erlang/erts-10.4.4/lib -L/Users/craigbowes/.nerves/artifacts/nerves_system_rpi2-portable-1.8.2/staging/usr/lib/erlang/lib/erl_interface-3.12/lib -lerts -lerl_interface -lei --sysroot=/Users/craigbowes/.nerves/artifacts/nerves_system_rpi2-portable-1.8.2/staging -lpthread
src/common_dht_read.o: file not recognized: file format not recognized
collect2: error: ld returned 1 exit status
make: *** [priv/nerves_dht] Error 1
could not compile dependency :nerves_dht, "mix compile" failed. You can recompile this dependency with "mix deps.compile nerves_dht", update it with "mix deps.update nerves_dht" or clean it with "mix deps.clean nerves_dht"
22
==> nerves_thermo
** (Mix) Could not compile with "make" (exit status: 2).
You need to have gcc and make installed. Try running the
commands "gcc --version" and / or "make --version". If these programs
are not installed, you will be prompted to install them.
If you seem to be getting similar compile errors, you might try the steps in that issue. The recommendation is basically just removing _build/
and deps/
directories and doing a full refetch and recompile:
rm -Rf _build/ deps/
mix deps.get
mix deps.compile
mix firmware.burn
I also repeatedly got nil
when using the info
function above. I’d disconnect and reconnect the circuit from the power and it would start working again. The listen
function seems more reliable.
Wrap Up
So there you have it. Everything is operational with Elixir and Nerves. I have the full source code here: https://github.com/IPushButtonsFTW/nerves_thermo
Again, thanks to Jon Carstens for getting his fork of the NervesDht
library working and helping me trouableshoot.
Sevus
IoT Raspberry Pi Elixir Nerves programming