Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Hello VEML7700

notebooks/basic_usage.livemd

Hello VEML7700

bus_name = "i2c-1"
bus_address = 0x10

Mix.install(
  [
    {:circuits_i2c, "~> 2.0"},
    {:circuits_sim, github: "elixir-circuits/circuits_sim"},
    {:kino, "~> 0.12.2"},
    {:veml7700, "0.1.2"}
  ],
  config: [
    circuits_i2c: [default_backend: CircuitsSim.I2C.Backend],
    circuits_sim: [
      config: [
        {CircuitsSim.Device.VEML7700, bus_name: bus_name, address: bus_address}
      ]
    ]
  ]
)

Introduction

This notebook demonstrates how to ambient light in Lux out from a VEML7700 ambient light sensor board. Our Nerves target device will communicate with a sensor board using the I2C protocol.

We need a few libraries for using a VEML7700 sensor in this notebook:

  • The circuits_i2c package allows us to communicate with hardware devices using the I2C protocol
  • The experimental circuits_sim package provides simulated I2C devices
  • The veml7700 package abstract the logic to use a VEML7700 sensor board

Running this notebook on the Nerves Livebook firmware, you can access directly to the real sensor board.

If you don’t have a real sensor board, don’t worry. It’s possible to work with a simulated device that is configured in the setup section above.

The VEML7700 sensors have the same interface as the VEML6030 sensors so the code here will work for both models.

i2c_backend_select_form =
  Kino.Control.form(
    [
      i2c_backend:
        Kino.Input.select(
          "I2C backend",
          [
            {CircuitsSim.I2C.Backend, "Simulated I2C"},
            {Circuits.I2C.I2CDev, "Real I2C"}
          ]
        )
    ],
    submit: "Select I2C backend"
  )

Kino.render(i2c_backend_select_form)

Kino.listen(i2c_backend_select_form, fn event ->
  selected_backend = event.data.i2c_backend
  Application.put_env(:circuits_i2c, :default_backend, selected_backend)
  IO.puts("==> Selected I2C backend: #{selected_backend}")
  Circuits.I2C.detect_devices()
  IO.puts(nil)
end)

Basic usage

The basic usage only takes two steps:

  • start a VEML7700 server
  • read output
{:ok, veml} = VEML7700.start_link(bus_name: bus_name, bus_address: bus_address)
VEML7700.measure(veml)

You can configure the sensor settings as needed. For details, refer to the API reference.

VEML7700.get_als_config(veml)
VEML7700.set_als_config(veml, [:als_gain_1_8])

Read output every second

defmodule AmbientLight do
  use GenServer

  def start_link(options) do
    GenServer.start_link(__MODULE__, options, name: __MODULE__)
  end

  def stop() do
    GenServer.stop(__MODULE__)
  end

  ## GenServer callbacks

  @impl GenServer
  def init(args) do
    bus_name = Keyword.fetch!(args, :bus_name)
    bus_address = Keyword.fetch!(args, :bus_address)
    frame = Keyword.fetch!(args, :frame)
    run_interval_ms = args[:run_interval_ms] || 5000

    case VEML7700.start_link(bus_name: bus_name, bus_address: bus_address) do
      {:ok, veml} ->
        state = %{
          bus_name: bus_name,
          bus_address: bus_address,
          frame: frame,
          veml: veml,
          run_interval_ms: run_interval_ms,
          start_time_ms: System.monotonic_time(:millisecond)
        }

        send(self(), :perform_measurement)

        {:ok, state}

      {:error, error} ->
        {:stop, error}
    end
  end

  @impl GenServer
  def handle_continue(:schedule_next_run, state) do
    Process.send_after(self(), :perform_measurement, state.run_interval_ms)
    {:noreply, state}
  end

  @impl GenServer
  def handle_info(:perform_measurement, state) do
    measure_and_render_to_frame(state)
    {:noreply, state, {:continue, :schedule_next_run}}
  end

  defp measure_and_render_to_frame(state) do
    case VEML7700.measure(state.veml) do
      {:ok, measurement} ->
        maybe_set_fake_data(state)
        light_lux = round(measurement.light_lux)
        seconds = round((measurement.timestamp_ms - state.start_time_ms) / 1000)
        Kino.Frame.render(state.frame, "#{light_lux} lux at #{seconds}")

      {:error, _} ->
        nil
    end
  end

  defp maybe_set_fake_data(state) do
    current_bus_type = :sys.get_state(state.veml).transport.bus.__struct__

    case current_bus_type do
      CircuitsSim.I2C.Bus ->
        CircuitsSim.Device.VEML7700.set_state(
          state.bus_name,
          state.bus_address,
          als_output: round(2200 * (1 + :rand.uniform()))
        )

      _ ->
        :skip
    end
  end
end

light_lux_measurement_frame = Kino.Frame.new()
start_button = Kino.Control.button("start")
stop_button = Kino.Control.button("stop")

Kino.listen(start_button, fn _event ->
  if Process.whereis(AmbientLight) do
    AmbientLight.stop()
  end

  AmbientLight.start_link(
    bus_name: bus_name,
    bus_address: bus_address,
    frame: light_lux_measurement_frame,
    run_interval_ms: 1000
  )
end)

Kino.listen(stop_button, fn _event ->
  if Process.whereis(AmbientLight) do
    AmbientLight.stop()
  end
end)

Kino.Layout.grid(
  [
    light_lux_measurement_frame,
    Kino.Layout.grid([start_button, stop_button], columns: 2)
  ],
  columns: 2
)

Hardware

For the curious, here is some information about the VEML7700 sensor.

For a hands-on Nerves tutorials, checkout this book.