Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove singleton VocIndex GenServer to make pure library #10

Merged
merged 1 commit into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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