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

Introduction

temperature.livemd

Temperature sensor demo

Mix.install([
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.11"}
])

alias VegaLite, as: Vl

Introduction

We will see how to read the environment temperature via a TMP36 sensor connected to a Raspberry Pi 4 using Nerves.

A time series chart will show how the temperature changes during the demo.

Let’s get started

Our equipment

Raspberry Pi 4

A single board computer with up to 8 GB RAM, a 1.5 GHz Quad-core ARM
Cortex-A72 processor, several ports and 40 General Purpose Input/Output (GPIO) pins. It can read only digital values, not analogic ones. It operates at 3.3V.

Raspberry Pi 4

TMP36

A low voltage, precision centigrade temperature sensor. It provides a voltage output that is linearly proportional to the Celsius temperature.

TMP36

Adafruit ADS1115

A 16-Bit Analog to Digital Converter (ADC) - 4 Channel

ADS1115

Hardware schematic

Schema

Start reading temperature

Let’s start reading the temperature with Elixir.

{:ok, ref} = Circuits.I2C.open("i2c-1")
addr = 0x48 # default address for this device

# :ain0 is the analog input channel no. 1 on ADC
{:ok, reading} = ADS1115.read(ref, addr, {:ain0, :gnd})

IO.puts("Temperature is #{reading}")

Well, this value seems a bit odd!

From a quick check on the TMP36 documentation, it turns out that it can read temperatures from −40°C to +125°C

TMP 36 datasheet

Each temperature yields a different voltage output, following the simple linear equation

$$ V = \dfrac{1}{100} T + 0.5 $$

where T is the temperature and V is the read voltage.

This means that the given a measured voltage, the corresponding temperature is:

$$ T = (V - 0.5) \cdot 100 $$

Remember though, we cannot read the voltage analog value from the Raspberry PI. So we need to ask the ADS115 ADC to convert the voltage into a numeric value.

ADS1115 is a 16 bit ADC, it provides values from -32768 to 32767.

Since we’re considering only positive numbers, we’re working in the range 0 .. 215 - 1 = 32767

The formula to get the voltage is

$$ V = \dfrac {r \cdot 2,048} {2^{15} - 1} $$

where r is the value read from the ADC and 2.048 is a coefficient specific for the ADS115 ADC, related to the operating voltage of the board, in this case 3.3V. Other voltages (e.g. 5V) would require a different coefficient.

This means the formula to read the temperature is:

$$

T = \bigg(\Big(\dfrac {r \cdot 2,048} {2^{15} - 1}\Big) - 0.5 \bigg) \cdot 100

$$

Now, let’s convert all this maths into Elixir code:


{:ok, ref} = Circuits.I2C.open("i2c-1")
addr = 0x48 # default address for ADS1115
{:ok, reading} = ADS1115.read(ref, addr, {:ain0, :gnd})

IO.puts("Value provided by the ADC: #{reading}")

voltage = ((reading * 2.048) / (:math.pow(2, 15) - 1)) 
IO.puts("Corresponding to voltage: #{voltage |> Float.ceil(2)} V")

temp = (voltage - 0.5) * 100
IO.puts("Temperature is: #{temp |> Float.ceil(2) } °C")
 
# Same formula with all pre computed calculations
temp = (reading - 8000) / 160
IO.puts("Using a pre-computed formula: #{temp |> Float.ceil(2)} °C")

We now need to plot the temperature on a chart every 2 seconds.

We create a GenServer which will call function temperature/0 periodically.

  defmodule Periodically do
    use GenServer
  
    def start_link(_opts) do
      GenServer.start_link(__MODULE__, %{})
    end
  
    def init(_state) do
      {:ok, ref} = Circuits.I2C.open("i2c-1")
  
      chart =
        Vl.new(width: 600, height: 300)
        |> Vl.mark(:line)
        |> Vl.encode_field(:x, "date", type: :temporal)
        |> Vl.encode_field(:y, "temperature", type: :quantitative, scale: [zero: false])
        |> Kino.VegaLite.render()
      
      initial_state = %{
        ref: ref,
        chart: chart
      } 
      
      schedule_work() # Schedule work to be performed at some point
      {:ok, initial_state}
    end
  
    def handle_info(:work, %{ref: ref, chart: chart}) do
      
      temp = temperature(ref)
      new_point = 
        %{
          "date" => DateTime.now!("Europe/Rome") |> DateTime.to_iso8601(),
          "temperature" => temp
        }
      Kino.VegaLite.push(chart, new_point)

      schedule_work() # Reschedule once more
      {:noreply, %{ref: ref, chart: chart}}
    end
  
    defp schedule_work() do
      Process.send_after(self(), :work, 2 * 1000)
    end
  
    defp temperature(ref) do
      addr = 0x48 # default address for ADS1115
      {:ok, reading} = ADS1115.read(ref, addr, {:ain0, :gnd})
      
      voltage = ((reading * 2.048) / (:math.pow(2, 15) - 1)) 
      
      temp = (voltage - 0.5) * 100 |> Float.ceil(2)
      # IO.puts("Temperature is: #{temp |> Float.ceil(2) } °C")
      temp 
    end
  
  end
  
  children = [ Periodically ]
  
  Supervisor.start_link(children, strategy: :one_for_one)

References