Current section

Files

Jump to
elixir_bme680 lib elixir_bme680.ex
Raw

lib/elixir_bme680.ex

defmodule Bme680 do
@moduledoc """
`elixir_bme680` provides a high level abstraction to interface with the
BME680 environmental sensor on Linux platforms.
"""
use Bitwise
use GenServer
defmodule State do
@moduledoc false
defstruct port: nil, subscribers: [], async_subscribers: [], measuring: false
end
defmodule Measurement do
@moduledoc false
defstruct temperature: nil, pressure: nil, humidity: nil, gas_resistance: nil
end
@doc """
Starts and links the `Bme680` GenServer.
"""
@spec start_link([ i2c_device_number: integer, i2c_address: 0x76 | 0x77 ], [ term ]) :: GenServer.on_start()
def start_link(bme_opts \\ [], opts \\ []) do
i2c_device_number = Keyword.get(bme_opts, :i2c_device_number, 1)
i2c_address = Keyword.get(bme_opts, :i2c_address, 0x76)
if Enum.member?([0x76, 0x77], i2c_address) do
arg = [i2c_device_number, i2c_address]
GenServer.start_link(__MODULE__, arg, opts)
else
{ :error, "invalid i2c address #{i2c_address}. Valid values are 0x76 and 0x77" }
end
end
@doc """
Perform a measurement on the BME680 sensor and synchronously return it
"""
@spec measure(GenServer.server()) :: %Measurement{ temperature: float, pressure: float, humidity: float, gas_resistance: integer | nil }
def measure(pid) do
GenServer.call(pid, :measure)
end
@doc """
Perform a measurement on the BME680 sensor and asynchronously get the result as a message
"""
@spec measure_async(GenServer.server(), pid) :: :ok
def measure_async(pid, send_to) do
GenServer.cast(pid, {:measure_async, send_to})
end
@doc """
Gracefully stops the `Bme680` GenServer.
"""
@spec stop(GenServer.server()) :: :ok
def stop(pid) do
GenServer.cast(pid, :stop)
end
# GenServer callbacks
def init([i2c_device_number, i2c_address]) do
executable_dir = Application.get_env(:elixir_bme680, :executable_dir, :code.priv_dir(:elixir_bme680))
port = Port.open({:spawn_executable, executable_dir ++ '/bme680'}, [
{:args, ["#{i2c_device_number}", "#{i2c_address}"]},
{:line, 64},
:use_stdio,
:binary,
:exit_status
])
{:ok, %State{ port: port, measuring: false, subscribers: [] }}
end
def handle_call(:measure, from, state = %State{ port: port, subscribers: subscribers, measuring: measuring }) do
unless measuring, do: Port.command(port, "measure\n")
{ :noreply, %State{ state | measuring: true, subscribers: [from | subscribers] } }
end
def handle_cast({:measure_async, pid}, state = %State{ port: port, async_subscribers: subs, measuring: measuring }) do
unless measuring, do: Port.command(port, "measure\n")
{ :noreply, %State{ state | measuring: true, async_subscribers: [pid | subs] } }
end
def handle_cast(:stop, state) do
{ :stop, :normal, state }
end
def handle_info({p, {:data, {:eol, line}}}, %State{ port: p, subscribers: subs, async_subscribers: async_subs }) do
measurement = decode_measurement(line)
for pid <- subs, do: GenServer.reply(pid, measurement)
for pid <- async_subs, do: send(pid, measurement)
{ :noreply, %State{ port: p, measuring: false, subscribers: [], async_subscribers: [] } }
end
def handle_info({port, {:exit_status, exit_status}}, state = %State{ port: port }) do
{ :stop, exit_status, state }
end
# Private helper functions
defp decode_measurement(line) do
case String.split(line, ",", [trim: true]) do
["T:" <> t, "P:" <> p, "H:" <> h, "G:" <> g] ->
%Measurement{
temperature: String.to_float(t),
pressure: String.to_float(p),
humidity: String.to_float(h),
gas_resistance: String.to_integer(g)
}
["T:" <> t, "P:" <> p, "H:" <> h] ->
%Measurement{
temperature: String.to_float(t),
pressure: String.to_float(p),
humidity: String.to_float(h)
}
_ -> { :error, "Measurement failed" }
end
end
end