From a0a7bc651811bb6d7977d807e7b255c199c88c05 Mon Sep 17 00:00:00 2001 From: Frank Hunleth Date: Sun, 12 Jan 2025 15:56:08 -0500 Subject: [PATCH] Make pure library and support >1 sensor 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. --- lib/sgp40.ex | 49 ++++++++++++++++++++++++++++++----- lib/sgp40/application.ex | 21 --------------- lib/sgp40/voc_index.ex | 42 +++++++++++++----------------- mix.exs | 3 +-- test/sgp40/voc_index_test.exs | 16 +++++++++--- 5 files changed, 73 insertions(+), 58 deletions(-) delete mode 100644 lib/sgp40/application.ex diff --git a/lib/sgp40.ex b/lib/sgp40.ex index 4b80b0b..db9b8a8 100644 --- a/lib/sgp40.ex +++ b/lib/sgp40.ex @@ -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" @@ -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. @@ -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}} @@ -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} @@ -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 diff --git a/lib/sgp40/application.ex b/lib/sgp40/application.ex deleted file mode 100644 index 65c1be5..0000000 --- a/lib/sgp40/application.ex +++ /dev/null @@ -1,21 +0,0 @@ -defmodule SGP40.Application do - @moduledoc false - - use Application - - @impl true - def start(_type, _args) do - children = [ - SGP40.VocIndex - ] - - opts = [ - name: SGP40.Supervisor, - strategy: :one_for_one, - max_restarts: 3, - max_seconds: 5 - ] - - Supervisor.start_link(children, opts) - end -end diff --git a/lib/sgp40/voc_index.ex b/lib/sgp40/voc_index.ex index 430c0b9..12f0ca4 100644 --- a/lib/sgp40/voc_index.ex +++ b/lib/sgp40/voc_index.ex @@ -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 """ @@ -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 diff --git a/mix.exs b/mix.exs index 0e3b710..711ee27 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/test/sgp40/voc_index_test.exs b/test/sgp40/voc_index_test.exs index 9ffae7b..8a77392 100644 --- a/test/sgp40/voc_index_test.exs +++ b/test/sgp40/voc_index_test.exs @@ -3,13 +3,17 @@ 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) @@ -17,12 +21,16 @@ defmodule SGP40.VocIndexTestTest do 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, @@ -30,7 +38,7 @@ defmodule SGP40.VocIndexTestTest do 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"