Skip to content

Building a weather station with Elixir Nerves PT 2.

Posted on:November 5, 2023 at 07:05 PM

The part 1 of this series is here.

The last thing I do, it was to configure WiFi, now we are going to capturing the sensor data.


The interface we worked was Inter Integrated Circuit (I2C). I2C is a serial bus protocol that allows devices to communicate with each other. The protocol uses a system clock and voltages to send values between devices. Each I2C device has an address. We send a request and we receive a response, so we need to read the sensor spec sheet to find out exactly which values we need to interact with the light sensor.

Spec sheet

There are three important links to the sensor spec sheet:

High Accuracy Ambient Light Sensor With I2C Interface Designing the VEML6030 Into an Application Qwiic Ambient Light Sensor (VEML6030) Hookup Guide

As a noob on the hardware/electronic stuff the first two links were the more confusing for me. The third one is the best one I found, but also following the book instructions. Now we are going to express values in hexadecimal because it is easier to read. In Elixir, a hex value is written with the prefix 0xff . The sensor has an address of 0x48.

Following with the instructions and the guide provided by Sparkfun the sensor support varios sensivities, called gains, the possible values are, 1, 2, 1/8 and 1/4, we work with 1/4 because it was the one provided with Sparkfun. Now also we nee the integration time setting, this is the time the sensor takes to get the value. The possible values are 800, 400, 100, 50 and 25. We set it to 100.

Fetching sensor data via Circuits.I2C

The library we are going to use is circuits_i2c

def deps do
  [{:circuits_i2c, "~> 2.0"}]

Once we update the code or dependencies we need to run mix deps.get to download the dependencies. (Remember that in my previous post I said that I had a target different that the book one, so I need to change the target in the mix.exs file, the target is rpi3), the next command is mix firmware to build the firmware and mix upload to upload it to the Raspberry Pi. Finally we can run ssh nerves.local to connect to the Raspberry Pi.

Aggregating data

TODO: Explain how I use circuits_i2c and how we configure the VEML6030 sensor.

Now we have to get the data captured by the VEML6030 sensor, so you have to create another folder inside our main project mix new veml6030 there we need to install the dependencies:

def deps do
  {:circuits_i2c, "~> 2.0"},

The veml6030 subproject will be a basic dependency project. Supervision of the VEML6030 GenServer will be happen later in the main project.

Here is the code to get the data from the sensor, as you can see some of the code is dedicated to traduce the values to human readable values. All based on the sensor specs that previouly mentioned. The next file called comm.ex is the one that will be responsible to communicate with the sensor, it will be responsible to read the sensor data and send it to the GenServer.


defmodule VEML6030.Config do
  defstruct [
    gain: :gain_1_4th,
    int_time: :it_100_ms,
    shutdown: false,
    interrupt: false

  def new, do: __struct__()
  def new(opts), do: __struct__(opts)

  def to_integer(config) do
    reserved = 0
    persistence_protect = 0

    <<integer::16>> =


  defp gain(:gain_1x), do: 0b0
  defp gain(:gain_2x), do: 0b01
  defp gain(:gain_1_8th), do: 0b10
  defp gain(:gain_1_4th), do: 0b11
  defp gain(:gain_default), do: 0b11

  defp int_time(:it_25_ms), do: 0b1100
  defp int_time(:it_50_ms), do: 0b1000
  defp int_time(:it_100_ms), do: 0b0000
  defp int_time(:it_200_ms), do: 0b0001
  defp int_time(:it_400_ms), do: 0b0010
  defp int_time(:it_800_ms), do: 0b0011
  defp int_time(:it_default), do: 0b0000

  defp shutdown(true), do: 1
  defp shutdown(_), do: 0

  defp interrupt(true), do: 1
  defp interrupt(_), do: 0

@to_lumens_factor %{
    {:it_800_ms, :gain_2x} => 0.0036,
    {:it_800_ms, :gain_1x} => 0.0072,
    {:it_800_ms, :gain_1_4th} => 0.0288,
    {:it_800_ms, :gain_1_8th} => 0.0576,

    {:it_400_ms, :gain_2x} => 0.0072,
    {:it_400_ms, :gain_1x} => 0.0144,
    {:it_400_ms, :gain_1_4th} => 0.0576,
    {:it_400_ms, :gain_1_8th} => 0.1152,

    {:it_200_ms, :gain_2x} => 0.0144,
    {:it_200_ms, :gain_1x} => 0.0288,
    {:it_200_ms, :gain_1_4th} => 0.1152,
    {:it_200_ms, :gain_1_8th} => 0.2304,

    {:it_100_ms, :gain_2x} => 0.0288,
    {:it_100_ms, :gain_1x} => 0.0576,
    {:it_100_ms, :gain_1_4th} => 0.2304,
    {:it_100_ms, :gain_1_8th} => 0.4608,

    {:it_50_ms,:gain_2x} => 0.0576,
    {:it_50_ms, :gain_1x} =>0.1152,
    {:it_50_ms, :gain_1_4th} =>0.4608,
    {:it_50_ms, :gain_1_8th} =>0.9216,

    {:it_25_ms,:gain_2x} => 0.1152,
    {:it_25_ms, :gain_1x} =>0.2304,
    {:it_25_ms, :gain_1_4th} =>0.9216,
    {:it_25_ms, :gain_1_8th} =>1.8432,

  def to_lumens(config, measurement) do
    @to_lumens_factor[{config.int_time, config.gain}] * measurement


defmodule VEML6030.Comm do
  alias Circuits.I2C
  alias VEML6030.Config

  @light_register <<4>>

  def discover(possible_addresses \\ [0x10, 0x48]) do

  def open(bus_name) do
    {:ok, i2c} =

  def write_config(configuration, i2c, sensor) do
    command = Config.to_integer(configuration)
    Circuits.I2C.write(i2c, sensor, <<0, command::little-16>>)

  def read(i2c, sensor, configuration) do
    <<value::little-16>> =
      Circuits.I2C.write_read!(i2c, sensor, @light_register, 2)

    Config.to_lumens(configuration, value)

This post was quite long, but I hope it was useful to you. Let me know if you want to know more about the project or have any questions.