The internet is of many things now, including manufacturing, retail, and more! I have been interested in building a personal project that makes use of embedded hardware connected to the internet, but struggled to find the project. The past couple of weeks included being fortunate enough to attend ElixirConf in Colorado. I saw a lot of great talks at ElixirConf, including Jacqueline Manzi’s Breaking Into Nerves: How to Use Your Elixir Knowledge to Create Your First Embedded Nerves Project.

The Problem

Upon returning to Milwaukee, the colder weather meant bringing my outdoor herb plants inside. I’ve grown close to them, so I wanted to ensure a healthy environment for their first winter. What better way to do that than to order some hardware sensors to constantly monitor the conditions they live in? It’s worth noting that I am not much of a gardener at the moment.

In the ideal world, I’d have a small computer near my plants periodically reporting environmental statistics to a database. This database would be on the internet and I then I could have an interface reading from the database to show me current and historical data even when away from home. Some of these ideas are inspired by FarmBot, a very impressive and open-source set of software and hardware for farming.

In short, I need:

  • A small internet-connected computer to collect and report environmental information
  • Somewhere for that data to live
  • A website to view that data

Building a Plant Computer

Nerves is an open source platform for building embedded applications with Elixir. It includes a bunch of great tooling, libraries, and resources. It supports a bunch of different types of computers, including the Raspberry Pi 3, which I’ll be using for this project.

There are a handful of sensors for measuring ambient environmental factors, and I chose a BME280 as it provides measurements of air pressure, relative humidity and temperature. The board supports both I2C and SPI communication protocols, but the distinction isn’t super important at this point. I2C is easier for me, so that’s what we’ll be rolling with.

The Nerves team built the fantastic Circuits.I2C to make it easier to do I2C communication. The I2C port on an RPi3 is 1, and the I2C address assigned to the BME 280 is 0x77. This lets us know where to be reading from and writing to. As a small example, to soft reset the BME280, the datasheet says to write 0xB6 to the soft reset register at address 0xE0:

# Open a connection on I2C Bus 1 to send commands down
iex> {:ok, ref} = I2C.open("i2c-1")
# Using our connection, write the value 0xB6 to the soft reset register (0xE0) at the 0x77 I2C address (the BME280)
# If things go well, it will return :ok
iex> I2C.write(ref, 0x77, <<0xE0, 0xB6>>)
:ok

Unfortunately, this is where things get messier. Before you read any readings, you must issue commands to check the status, configure the board and read the compensation coefficients. Once that is done though, it is cool to read out the pressure, humidity and temperature data. Those values must then be added, multiplied, and shifted using the formula in the datasheet using the compensastion coefficients.

Fortunately, the wonderful people at Adafruit built a CircuitPython library that translates the above process into a more readable format. The datasheet suggests that for my use as a “weather station”, I don’t need to worry about the more complex options like reading at quick intervals, oversampling, or infinite impulse response filtering. This simplification reduces the amount I need to reimplement! There’s still a significant to do to ensure everything is correct since the board stores data as both 8-bit and 16-bit signed or unsigned integers. Elixir’s excellent binary pattern matching goes a long way to make this destructuring easier. It’s quite terse, but some of the data registers are a good example:

<<t1::little-unsigned-integer-size(16), t2::little-signed-integer-size(16),
  t3::little-signed-integer-size(16), p1::little-unsigned-integer-size(16),
  p2::little-signed-integer-size(16), p3::little-signed-integer-size(16),
  p4::little-signed-integer-size(16), p5::little-signed-integer-size(16),
  p6::little-signed-integer-size(16), p7::little-signed-integer-size(16),
  p8::little-signed-integer-size(16),
  p9::little-signed-integer-size(16)>> =
  I2C.write_read!(ref, @address, <<@dig_t1_register>>, 24)

The above shows reading 24 bytes starting at the @dig_t1_register address (dig is short for digital). The temperature and pressure data registers are consecutive, so we can read all of them at once. Registers are organized as 1 byte, but all of the temperature and pressure values are 2 bytes. There are three values to collect for temperature and nine for pressure for a total of 12. Since each of them is 2 bytes (or 16 bits), we have to tell I2C to read 24 bytes. The binary pattern matching takes care of the destructuring the binary at the correct points and decoding into the specified types. The little specifies that the bits are returned in little endian bit order. Ultimately, t1 will havev a 16-bit integer, t2 will as well, and so on. This pattern is used frequently in the code to handle the various register reads that need to be done.

Once I’ve got the reimplementation working, I’d like to read all of the environmental factors every second and report it up to my eventual database.

To encapsulate this, I’ll build this behavior into an Elixir GenServer. This construct will allow the I2C connection to live asynchronously and take readings every second without interrupting other stuff I may want the Raspberry Pi to be doing. A significant amount of trial error I went through in building this module was made easier with the ability to push code changes over SSH and avoid doing the SD card shuffle.

It was a bit more effort than I anticipated, and it looks like other sensors are a bit easier to interface with, but the resulting module is here. In the future, I’d like to build out the ability to use all of the BME280’s features and release a library.

Ultimately, I am satisfied for now with being able to read from the sensor, resulting in code that looks something like this:

<<status::little-unsigned-integer-size(8)>> =
  I2C.write_read!(ref, @address, <<@status_register>>, 1)

Logger.info("Status is #{inspect(status)}")

# check status to make sure it's okay to read sensor data
# &&& is bitwise AND
if (status &&& 0x08) > 0 do
  Logger.error("Status is not 0, it is: #{inspect(status)}")
  # try again in 6 milliseconds
  Process.send_after(self(), :read_temperature, 6)
else
  # read sensors
  {humidity, pressure_hpa, temperature_f} = read(ref, {state.temp_calib, state.pressure_calib, state.humidity_calib})
  # send data to database
  # To Be Determined
  # schedule another reading in 60 seconds
  Process.send_after(self(), :read_temperature, 60_000)
end

Database

I considered a few options, including my go-to database Postgres. It’s a fine choice and I’d probably choose Postgres if I had dreams of supporting this project for the long term. But I had been wanting to try out a time-series database, so I chose InfluxDB. It wasn’t a short process, but I set up my InfluxDB on a virtual server in the cloud.

InfluxDB has different semantics, but adding data once things are running is relatively straightforward. In InfluxDB, creating data requires a measurement, which is similar to a SQL table. Rows in a measurement are made up of fields, tags and timestamp. The fields represent the data to store, and tags are metadata to filter on. As this is a time-series database, the timestamp is whenever we measured the associated values.

In trying to keep things simple, I’ll have one measurement named weather, fields for the temperature, humidity, and pressure. I’ll have tags for location of the readings, and the device they’re being read from. If no timestamp is specified, InfluxDB will use the current server time, which is fine.

InfluxDB defines a communication protocol that can be used over HTTP, which I’ll be doing using HTTPoison. Filling in the code from above, I send data from the RPi to InfluxDB like this:

# read sensors
{humidity, pressure_hpa, temperature_f} = read(ref, {state.temp_calib, state.pressure_calib, state.humidity_calib})
# send data to database
data =
  "weather,source=pi,location=downstairs temperature=#{temperature_f},humidity=#{humidity},pressure=#{
    pressure_hpa
  }"

auth = "Basic #{Base.encode64("influx_username:influx_password")}"

r =
  HTTPoison.post("https://influx.mydomain.com/write?db=db_name", data, [
    {"Authorization", auth}
  ])

# schedule another reading in 60 seconds
Process.send_after(self(), :read_temperature, 60_000)

I’ve got data going into InfluxDB, so the only remaining part for now is a way to view it.

Visualization

I’m not super capable of building pleasant visual interfaces at the moment, so I chose to look elsewhere for products that can accomplish what I need. Fortunately, there are some very good data visualization platforms available these days. One that integrates well with InfluxDB is Grafana. I installed and run Grafana on the same server I run InfluxDB on.

Grafana has a vast array of visualizations, and the process to set them up using InfluxDB is well-covered in Grafana’s documentation.

After setting up the data source and a few panels, I can see the environment my plants are living in!

grafana screenshot 1

What’s Next

It’s great to be able to see the environmental factors, but it would be cool if I could also SEE the plants using the Raspberry Pi Camera Module.