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.