Packages

Driver for the TMF882X family of multi-zone direct time-of-flight (dToF) distance sensor

Current section

Files

Jump to
tmf882x lib tmf882x.ex
Raw

lib/tmf882x.ex

defmodule TMF882X do
@moduledoc """
Interface with tmf8820/tmf8821 direct time-of-flight (dToF) sensor ([https://ams.com/en/tmf8820]).
### Starting
Start a `TMF882X` it directly from another process (See Options section below for more info):
```elixir
{:ok, pid} = TMF882X.start_link(bus: "i2c-1")
```
### Configuration
A configuration keyword list can be passed to the `start_link` function containing the following parameters:
* **`bus`** - Name of the I2C bus that the sensor is attached to.
* **`interrupt_gpio`** - If the interrupt pin of the sensor is connected, the GPIO number can be specified here. If not interrupt
pin is specified, then the library will use I2C to poll when a new measurement packet is ready.
* **`enable_gpio`** - If the enable pin is connected, the GPIO number can be specified here. If connected, calling `reset/1` will
use this pin to reset the device. Otherwise, I2C commands will be issued to reset the device. (default: `nil`)
* **`auto_start`** - If set to `true`, the device will immediately start taking measurements once the `app_ready` status is `true`.
If set to `false`, the device will wait for a call to `start_measuring/1` before taking any measurements. (default: `true`)
* **`measure_interval`** - Target interval (in milliseconds) between measurements. If set to `0`, as soon as a measurement is received, another
will start.
* **`device_config`** - The configuration to be sent to the device on startup. See Configuration section below.
### Results
The calling process will receive messages of the format `{:tmf882x, %TMF882X.Result{}}`:
```elixir
def handle_info({:tmf882x, %TMF882X.Result{} = result}) do
...
end
```
The `TMF882X.Result` struct contains a list of measurements from each channel. Each measurement is a tuple containing a `distance` (in millimeters) and `confidence` (out of 255) value:
```elixir
%TMF882X.Result{
tid: 200,
size: 128,
number: 200,
temperature: 41,
valid_results: 11,
ambient: 283,
photon_count: 16971,
reference_count: 60573,
sys_tick: 1215866837,
measurements: [
{844, 61},
{841, 106},
{1010, 56},
...
]
}
```
Other fields in the `Result` struct are directly from the decode of the Result register.
"""
use GenServer
alias TMF882X.{Device, Bus, Boot, Measure, SPAD}
require Logger
@enable_down_time 1
@type server() :: atom() | pid() | {atom(), any()} | {:via, atom(), any()}
@doc """
Starts the measurement process for the given device. Once started, the process will send
`{:tmf882x, %TMF882X.Result{}}` messages each time a measurement is completed.
"""
@spec start_measuring(server()) :: :ok
def start_measuring(pid), do: GenServer.cast(pid, :start_measuring)
@doc """
Stops the measurement process for the given device.
"""
@spec stop_measuring(server()) :: :ok
def stop_measuring(pid), do: GenServer.cast(pid, :stop_measuring)
@doc """
Applies the given configuration values to the device. This will temporarily stop the
measurements and resume them after the configuration if written (if they were running before).
"""
@spec put_config(server(), map()) :: :ok
def put_config(pid, config), do: GenServer.cast(pid, {:put_config, config})
@doc """
Queries the device for its current configuration and returns it.
"""
@spec get_config(server()) :: map()
def get_config(pid), do: GenServer.call(pid, :get_config)
@doc """
Queries the device for the current custom spad_map and returns it.
"""
@spec get_custom_spad(server()) :: map()
def get_custom_spad(pid), do: GenServer.call(pid, :get_custom_spad)
@doc """
Sets the custom spad to the one provided.
NOTE: the device configuration must be set with `spad_map_id` set to 14
before the custom spad map can be written. Otherwise, the custom spad
command will silently fail.
"""
@spec set_custom_spad(server(), SPAD.t()) :: :ok
def set_custom_spad(pid, spad), do: GenServer.cast(pid, {:set_custom_spad, spad})
@doc """
Resets the device including resetting the enable pin (if configured).
"""
@spec reset(server()) :: :ok
def reset(pid), do: GenServer.cast(pid, :reset)
@doc """
Returns true if the application on the device has been initialized. Measurements cannot be started until the
`app_ready` flag is `true`.
"""
@spec app_ready?(server()) :: boolean()
def app_ready?(pid), do: GenServer.call(pid, :app_ready?)
@doc """
Returns true if the process is measuring actively.
"""
@spec running?(server()) :: boolean()
def running?(pid), do: GenServer.call(pid, :running?)
@doc """
Returns the status of the device gathered from the `0xE0` register.
"""
@spec get_status(server()) :: %{active: boolean(), enabled: boolean()}
def get_status(pid), do: GenServer.call(pid, :get_status)
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(
TMF882X,
%{
bus: Keyword.fetch!(opts, :bus),
int_gpio: Keyword.get(opts, :interrupt_gpio),
en_gpio: Keyword.get(opts, :enable_gpio),
measure_interval: Keyword.get(opts, :measure_interval, 0),
device_config: Keyword.get(opts, :device_config, %{}),
auto_start: Keyword.get(opts, :auto_start, true),
parent: self()
},
opts
)
end
@impl true
def init(config) do
{:ok, int_gpio} = maybe_open_gpio(config.int_gpio, :input)
{:ok, en_gpio} = maybe_open_gpio(config.en_gpio, :output)
if int_gpio, do: :ok = Circuits.GPIO.set_interrupts(int_gpio, :falling)
{:ok, i2c} = Bus.wait_for_i2c_and_connect(config.bus, 100)
send(self(), :connect)
{:ok,
%{
device_config: config.device_config,
int_gpio: int_gpio,
en_gpio: en_gpio,
pid: i2c,
running: false,
app_ready: false,
measurement_pending: false,
parent: config.parent,
measure_interval: config.measure_interval,
measure_start: nil,
auto_start: config.auto_start
}}
end
@impl true
def handle_call(:app_ready?, _, state), do: {:reply, state.app_ready, state}
def handle_call(:running?, _, state), do: {:reply, state.running, state}
def handle_call(:get_config, _, state) do
if state.measurement_pending do
:ok = Measure.stop(state.pid)
end
config = Device.read_config(state.pid)
:ok = Measure.enable_interrupt(state.pid)
if state.running do
send(self(), :measure)
end
{:reply, config, state}
end
def handle_call(:get_custom_spad, _, state) do
if state.measurement_pending do
:ok = Measure.stop(state.pid)
end
spad = Device.get_custom_spad(state.pid)
:ok = Measure.enable_interrupt(state.pid)
if state.running do
send(self(), :measure)
end
{:reply, spad, state}
end
def handle_call(:get_status, _, state) do
{:reply, Device.status(state.pid), state}
end
def handle_call({:write_read, data, size}, _, state) do
{:ok, resp} = Bus.write_read(state.pid, data, size)
{:reply, resp, state}
end
@impl true
def handle_cast(:start_measuring, %{app_ready: true} = state) do
:ok = Measure.enable_interrupt(state.pid)
send(self(), :measure)
{:noreply, %{state | running: true}}
end
def handle_cast(:start_measuring, state) do
{:noreply, state}
end
def handle_cast(:stop_measuring, state) do
:ok = Measure.stop(state.pid)
{:noreply, %{state | running: false}}
end
def handle_cast({:put_config, config}, state) do
if state.measurement_pending do
:ok = Measure.stop(state.pid)
end
:ok = Device.apply_config(state.pid, config)
:ok = Measure.enable_interrupt(state.pid)
if state.running do
send(self(), :measure)
end
{:noreply, state}
end
def handle_cast(:reset, state) do
send(self(), :connect)
{:noreply, %{state | running: false, app_ready: false}}
end
def handle_cast({:write, data}, state) do
:ok = Bus.write(state.pid, data)
{:noreply, state}
end
def handle_cast({:set_custom_spad, spad}, state) do
:ok = Device.set_custom_spad(state.pid, spad)
{:noreply, state}
end
@impl true
def handle_info(:connect, state) do
with :ok <- reset_enable_pin(state.en_gpio),
:ok <- Bus.wait_for_device(state.pid, 100),
:ok <- Device.enable(state.pid),
:ok <- Device.wait_for_enabled(state.pid, 100),
:ok <- Boot.load_app_if_in_bootloader(state.pid),
:ok <- Device.apply_config(state.pid, state.device_config),
:ok <- Measure.enable_interrupt(state.pid) do
if state.auto_start do
send(self(), :measure)
end
{:noreply, %{state | running: true, app_ready: true}}
end
end
def handle_info(:measure, %{running: true} = state) do
:ok = Measure.start(state.pid)
start = System.monotonic_time()
if state.int_gpio do
{:noreply, %{state | measurement_pending: true, measure_start: start}}
else
:ok = Measure.wait_for_interrupt(state.pid, 100)
:ok = complete_measurement(%{state | measure_start: start})
{:noreply, state}
end
end
def handle_info(:measure, state), do: {:noreply, state}
def handle_info({:circuits_gpio, _, _, 0}, %{measurement_pending: true} = state) do
:ok = complete_measurement(state)
{:noreply, %{state | measurement_pending: false}}
end
def handle_info({:circuits_gpio, _, _, _}, state), do: {:noreply, state}
defp reset_enable_pin(nil), do: :ok
defp reset_enable_pin(en_gpio) do
:ok = Circuits.GPIO.write(en_gpio, 0)
:timer.sleep(@enable_down_time)
Circuits.GPIO.write(en_gpio, 1)
end
def calc_measure_delay(state) do
time =
(System.monotonic_time() - state.measure_start)
|> System.convert_time_unit(:native, :millisecond)
(state.measure_interval - time) |> max(0)
end
defp maybe_open_gpio(nil, _), do: {:ok, nil}
defp maybe_open_gpio(pin, mode), do: Circuits.GPIO.open(pin, mode)
defp complete_measurement(state) do
result = Measure.read_data(state.pid)
send(state.parent, {:tmf882x, result})
:timer.send_after(calc_measure_delay(state), :measure)
Measure.clear_interrupts(state.pid)
end
end