Skip to content

Commit

Permalink
Make pure library and support >1 sensor
Browse files Browse the repository at this point in the history
This moves the C algorithm code management out of a singleton GenServer
since it maintains per-sensor state. This also makes this package a pure
library and simplifies state handling in the port.
  • Loading branch information
fhunleth authored Jan 13, 2025
1 parent 179b8dc commit fbf8dcf
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 58 deletions.
49 changes: 42 additions & 7 deletions lib/sgp40.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ defmodule SGP40 do

defmodule State do
@moduledoc false
defstruct [:humidity_rh, :last_measurement, :serial_id, :temperature_c, :transport]
defstruct [
:humidity_rh,
:last_measurement,
:serial_id,
:temperature_c,
:transport,
:voc_index
]
end

@default_bus_name "i2c-1"
Expand Down Expand Up @@ -58,6 +65,24 @@ defmodule SGP40 do
GenServer.call(server, :measure)
end

@spec get_states(GenServer.server()) ::
{:ok, SGP40.VocIndex.AlgorithmStates.t()} | {:error, any}
def get_states(server) do
GenServer.call(server, :get_states)
end

@spec set_states(GenServer.server(), SGP40.VocIndex.AlgorithmStates.t()) ::
{:ok, binary} | {:error, any}
def set_states(server, args) do
GenServer.call(server, {:set_states, args})
end

@spec set_tuning_params(GenServer.server(), SGP40.VocIndex.AlgorithmTuningParams.t()) ::
{:ok, binary} | {:error, any}
def set_tuning_params(server, args) do
GenServer.call(server, {:set_tuning_params, args})
end

@doc """
Update relative ambient humidity (RH %) and ambient temperature (degree C)
for the humidity compensation.
Expand All @@ -82,13 +107,15 @@ defmodule SGP40 do
case @transport_mod.open(bus_name: bus_name, bus_address: bus_address) do
{:ok, transport} ->
{:ok, serial_id} = SGP40.Comm.serial_id(transport)
{:ok, voc_index} = SGP40.VocIndex.start_link()

state = %State{
humidity_rh: humidity_rh,
last_measurement: nil,
serial_id: serial_id,
temperature_c: temperature_c,
transport: transport
transport: transport,
voc_index: voc_index
}

{:ok, state, {:continue, :init_sensor}}
Expand Down Expand Up @@ -123,7 +150,7 @@ defmodule SGP40 do
state.humidity_rh,
state.temperature_c
),
{:ok, voc_index} <- SGP40.VocIndex.process(sraw) do
{:ok, voc_index} <- SGP40.VocIndex.process(state.voc_index, sraw) do
timestamp_ms = System.monotonic_time(:millisecond)
measurement = %SGP40.Measurement{timestamp_ms: timestamp_ms, voc_index: voc_index}

Expand All @@ -140,14 +167,22 @@ defmodule SGP40 do
{:reply, {:ok, state.last_measurement}, state}
end

def handle_call(:get_states, _from, state) do
{:reply, SGP40.VocIndex.get_states(state.voc_index), state}
end

def handle_call({:set_states, args}, _from, state) do
{:reply, SGP40.VocIndex.set_states(state.voc_index, args), state}
end

def handle_call({:set_tuning_params, args}, _from, state) do
{:reply, SGP40.VocIndex.set_tuning_params(state.voc_index, args), state}
end

@impl GenServer
def handle_cast({:update_rht, humidity_rh, temperature_c}, state) do
state = %{state | humidity_rh: humidity_rh, temperature_c: temperature_c}

{:noreply, state}
end

defdelegate get_states, to: SGP40.VocIndex
defdelegate set_states(args), to: SGP40.VocIndex
defdelegate set_tuning_params(args), to: SGP40.VocIndex
end
21 changes: 0 additions & 21 deletions lib/sgp40/application.ex

This file was deleted.

42 changes: 18 additions & 24 deletions lib/sgp40/voc_index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,27 @@ defmodule SGP40.VocIndex do
@moduledoc """
Process the raw output of the SGP40 sensor into the VOC Index.
"""
use GenServer

alias SGP40.VocIndex.AlgorithmStates
alias SGP40.VocIndex.AlgorithmTuningParams

use GenServer, restart: :permanent

require Logger

@doc """
Initialize the VOC algorithm parameters. Call this once at the beginning or
whenever the sensor stopped measurements.
Initialize the VOC algorithm
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(_args \\ []) do
case GenServer.start_link(__MODULE__, nil, name: __MODULE__) do
{:ok, pid} -> {:ok, pid}
# Stop this process and let the supervisor restart so that we can
# re-initialize the VOC algorithm.
{:error, {:already_started, pid}} -> GenServer.stop(pid, :normal)
end
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args)
end

@doc """
Calculate the VOC index value from the raw sensor value.
"""
@spec process(0..0xFFFF) :: {:ok, 1..500} | {:error, any}
def process(sraw) do
GenServer.call(__MODULE__, {:process, sraw})
@spec process(GenServer.server(), 0..0xFFFF) :: {:ok, 1..500} | {:error, any}
def process(server, sraw) do
GenServer.call(server, {:process, sraw})
end

@doc """
Expand All @@ -38,30 +31,31 @@ defmodule SGP40.VocIndex do
skipping initial learning phase. This feature can only be used after at least
3 hours of continuous operation.
"""
@spec get_states :: {:ok, AlgorithmStates.t()} | {:error, any}
def get_states() do
GenServer.call(__MODULE__, :get_states)
@spec get_states(GenServer.server()) :: {:ok, AlgorithmStates.t()} | {:error, any}
def get_states(server) do
GenServer.call(server, :get_states)
end

@doc """
Set previously retrieved algorithm states to resume operation after a short
interruption, skipping initial learning phase. This feature should not be
used after inerruptions of more than 10 minutes. Call this once after
used after interruptions of more than 10 minutes. Call this once after
`start_link/1` and the optional `set_tuning_params/1`, if
desired. Otherwise, the algorithm will start with initial learning phase.
"""
@spec set_states(AlgorithmStates.t()) :: {:ok, binary} | {:error, any}
def set_states(args) do
GenServer.call(__MODULE__, {:set_states, args})
@spec set_states(GenServer.server(), AlgorithmStates.t()) :: {:ok, binary} | {:error, any}
def set_states(server, args) do
GenServer.call(server, {:set_states, args})
end

@doc """
Set parameters to customize the VOC algorithm. Call this once after
`start_link/1`, if desired. Otherwise, the default values will be used.
"""
@spec set_tuning_params(AlgorithmTuningParams.t()) :: {:ok, binary} | {:error, any}
def set_tuning_params(args) do
GenServer.call(__MODULE__, {:set_tuning_params, args})
@spec set_tuning_params(GenServer.server(), AlgorithmTuningParams.t()) ::
{:ok, binary} | {:error, any}
def set_tuning_params(server, args) do
GenServer.call(server, {:set_tuning_params, args})
end

@impl GenServer
Expand Down
3 changes: 1 addition & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ defmodule SGP40.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger],
mod: {SGP40.Application, []}
extra_applications: [:logger]
]
end

Expand Down
16 changes: 12 additions & 4 deletions test/sgp40/voc_index_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,42 @@ defmodule SGP40.VocIndexTestTest do
alias SGP40.VocIndex

test "process" do
{:ok, voc_index} = VocIndex.process(123)
pid = start_supervised!(SGP40.VocIndex)

{:ok, voc_index} = VocIndex.process(pid, 123)

assert is_integer(voc_index)
end

test "get_states" do
{:ok, states} = VocIndex.get_states()
pid = start_supervised!(SGP40.VocIndex)

{:ok, states} = VocIndex.get_states(pid)

assert %{mean: mean, std: std} = states
assert is_integer(mean)
assert is_integer(std)
end

test "set_states" do
assert {:ok, echo} = VocIndex.set_states(%{mean: 1, std: 2})
pid = start_supervised!(SGP40.VocIndex)

assert {:ok, echo} = VocIndex.set_states(pid, %{mean: 1, std: 2})

assert echo =~ ~r/mean:\d*,std:\d*/
end

test "set_tuning_params" do
pid = start_supervised!(SGP40.VocIndex)

params = %{
voc_index_offset: 1,
learning_time_hours: 2,
gating_max_duration_minutes: 3,
std_initial: 4
}

assert {:ok, echo} = VocIndex.set_tuning_params(params)
assert {:ok, echo} = VocIndex.set_tuning_params(pid, params)

assert echo ==
"voc_index_offset:1,learning_time_hours:2,gating_max_duration_minutes:3,std_initial:4"
Expand Down

0 comments on commit fbf8dcf

Please sign in to comment.