First pass.
This commit is contained in:
parent
9d28a79b45
commit
fb247dbead
24 changed files with 1036 additions and 40 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -20,5 +20,5 @@ erl_crash.dump
|
|||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
chip_hop-*.tar
|
||||
wafer-*.tar
|
||||
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
# ChipHop
|
||||
# Wafer
|
||||
|
||||
**TODO: Add description**
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `chip_hop` to your list of dependencies in `mix.exs`:
|
||||
by adding `wafer` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:chip_hop, "~> 0.1.0"}
|
||||
{:wafer, "~> 0.1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at [https://hexdocs.pm/chip_hop](https://hexdocs.pm/chip_hop).
|
||||
be found at [https://hexdocs.pm/wafer](https://hexdocs.pm/wafer).
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
defmodule ChipHop do
|
||||
@moduledoc """
|
||||
Documentation for ChipHop.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Hello world.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> ChipHop.hello()
|
||||
:world
|
||||
|
||||
"""
|
||||
def hello do
|
||||
:world
|
||||
end
|
||||
end
|
14
lib/wafer.ex
Normal file
14
lib/wafer.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
defmodule Wafer do
|
||||
@moduledoc """
|
||||
Welcome to Wafer. The funkiest way to write hardware drivers.
|
||||
|
||||
This library doesn't do much on it's own, it is used to help with some of the
|
||||
repetitive tasks of writing drivers for hardware peripherals such as I2C and
|
||||
SPI connected sensors.
|
||||
|
||||
Wafer works with both ElixirALE and Circuits. As such it's up to you to
|
||||
define which dependency you're using.
|
||||
"""
|
||||
|
||||
@type i2c_address :: 0..127
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
defmodule ChipHop.Application do
|
||||
defmodule Wafer.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
@ -7,13 +7,14 @@ defmodule ChipHop.Application do
|
|||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# Starts a worker by calling: ChipHop.Worker.start_link(arg)
|
||||
# {ChipHop.Worker, arg}
|
||||
{Registry, [keys: :duplicate, name: Wafer.InterruptRegistry]},
|
||||
Wafer.Driver.ElixirAleGPIODispatcher,
|
||||
Wafer.Driver.CircuitsGPIODispatcher
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: ChipHop.Supervisor]
|
||||
opts = [strategy: :one_for_one, name: Wafer.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
75
lib/wafer/chip.ex
Normal file
75
lib/wafer/chip.ex
Normal file
|
@ -0,0 +1,75 @@
|
|||
defprotocol Wafer.Chip do
|
||||
alias Wafer.Conn
|
||||
|
||||
@moduledoc """
|
||||
A `Chip` is a physical peripheral with registers which can be read from and
|
||||
written to.
|
||||
"""
|
||||
|
||||
@type i2c_address :: 0..0x7F
|
||||
@type register_address :: non_neg_integer
|
||||
@type bytes :: non_neg_integer
|
||||
|
||||
@doc """
|
||||
Read the register at the specified address.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `conn` a type which implements the `Wafer.Conn` behaviour.
|
||||
- `register_address` the address of the register to read from.
|
||||
- `bytes` the number of bytes to read from the register.
|
||||
|
||||
## Example
|
||||
|
||||
iex> {:ok, conn} = ElixirAleI2C.acquire(bus: "i2c-1", address: 0x68)
|
||||
...> Chip.read_register(conn, 0, 1)
|
||||
{:ok, <<0>>}
|
||||
"""
|
||||
@spec read_register(Conn.t(), register_address, bytes) ::
|
||||
{:ok, data :: binary} | {:error, reason :: any}
|
||||
def read_register(conn, register_address, bytes)
|
||||
|
||||
@doc """
|
||||
Write to the register at the specified address.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `conn` a type which implements the `Wafer.Conn` behaviour.
|
||||
- `register_address` the address of the register to write to.
|
||||
- `data` a bitstring or binary of data to write to the register.
|
||||
|
||||
## Example
|
||||
|
||||
iex> {:ok, conn} = ElixirAleI2C.acquire(bus: "i2c", address: 0x68)
|
||||
...> Chip.write_register(conn, 0, <<0>>)
|
||||
:ok
|
||||
"""
|
||||
@spec write_register(Conn.t(), register_address, data :: binary) ::
|
||||
:ok | {:error, reason :: any}
|
||||
def write_register(conn, register_address, data)
|
||||
|
||||
@doc """
|
||||
Perform a swap with the register at the specified address. With some drivers
|
||||
this is atomic, and with others it is implemented as a register read followed
|
||||
by a write.
|
||||
|
||||
## Arguments
|
||||
|
||||
- `conn` a type which implements the `Wafer.Conn` behaviour.
|
||||
- `register_address` the address of the register to swap.
|
||||
- `new_data` the data to write to the regsiter.
|
||||
|
||||
## Returns
|
||||
|
||||
The data that was previously in the register.
|
||||
|
||||
## Example
|
||||
|
||||
iex> {:ok, conn} = ElixirAleI2C.acquire(bus: "i2c", address: 0x68)
|
||||
...> Chip.swap_register(conn, 0, <<1>>)
|
||||
{:ok, <<0>>}
|
||||
"""
|
||||
@spec swap_register(Conn.t(), register_address, new_data :: binary) ::
|
||||
{:ok, data :: binary} | {:error, reason :: any}
|
||||
def swap_register(conn, register_address, new_data)
|
||||
end
|
17
lib/wafer/conn.ex
Normal file
17
lib/wafer/conn.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule Wafer.Conn do
|
||||
@moduledoc """
|
||||
Defines a protocol and behaviour for connecting to a peripheral.
|
||||
"""
|
||||
|
||||
@type option :: {atom, any}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the provided driver.
|
||||
"""
|
||||
@callback acquire(opts :: [option]) :: t :: {:error, reason :: any}
|
||||
|
||||
@doc """
|
||||
Release all resources associated with this connection.
|
||||
"""
|
||||
@callback release(module) :: :ok | {:error, reason :: any}
|
||||
end
|
82
lib/wafer/drivers/circuits_gpio.ex
Normal file
82
lib/wafer/drivers/circuits_gpio.ex
Normal file
|
@ -0,0 +1,82 @@
|
|||
defmodule Wafer.Driver.CircuitsGPIO do
|
||||
defstruct ~w[direction pin ref]a
|
||||
@behaviour Wafer.Conn
|
||||
alias Circuits.GPIO, as: Driver
|
||||
alias Wafer.GPIO
|
||||
|
||||
@moduledoc """
|
||||
A connection to a native GPIO pin via Circuit's GPIO driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{ref: reference, pin: non_neg_integer, direction: GPIO.pin_direction()}
|
||||
|
||||
@type options :: [option]
|
||||
@type option :: {:pin, non_neg_integer} | {:direction, GPIO.pin_direction()}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a native GPIO pin via Circuit's GPIO driver.
|
||||
|
||||
## Options
|
||||
|
||||
- `:pin` (required) the integer number of the pin to connect to. Hardware dependent.
|
||||
- `:direction` (optional) either `:in` or `:out`. Defaults to `:out`.
|
||||
"""
|
||||
@spec acquire(options) :: {:ok, t} | {:error, reason :: any}
|
||||
def acquire(opts) when is_list(opts) do
|
||||
with {:ok, pin} <- Keyword.get(opts, :pin),
|
||||
{:ok, direction} <- Keyword.get(opts, :direction, :out),
|
||||
{:ok, ref} <- Driver.open(pin, direction, Keyword.drop(opts, ~w[pin direction]a)) do
|
||||
%__MODULE__{ref: ref, pin: pin, direction: direction}
|
||||
else
|
||||
:error -> {:error, "Circuits.GPIO requires a `pin` option."}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Release all resources related to this GPIO pin connection.
|
||||
|
||||
Note that other connections may still be using the pin.
|
||||
"""
|
||||
@spec release(t) :: :ok | {:error, reason :: any}
|
||||
def release(%{ref: ref}), do: Driver.close(ref)
|
||||
end
|
||||
|
||||
defimpl Wafer.GPIOProto, for: Wafer.Driver.CircuitsGPIO do
|
||||
alias Wafer.Driver.CircuitsGPIODispatcher
|
||||
alias Circuits.GPIO, as: Driver
|
||||
|
||||
def read(%{ref: ref}) do
|
||||
case(Driver.read(ref)) do
|
||||
value when value in [0, 1] -> {:ok, value}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def write(%{ref: ref} = conn, value) when value in [0, 1] do
|
||||
case(Driver.write(ref, value)) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def direction(%{direction: :in} = conn, :in), do: {:ok, conn}
|
||||
def direction(%{direction: :out} = conn, :out), do: {:ok, conn}
|
||||
|
||||
def direction(%{ref: ref} = conn, direction) when direction in [:in, :out] do
|
||||
case(Driver.set_direction(ref, direction)) do
|
||||
:ok -> %{conn | direction: direction}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def enable_interrupt(conn, pin_trigger), do: CircuitsGPIODispatcher.enable(conn, pin_trigger)
|
||||
def disable_interrupt(conn, pin_trigger), do: CircuitsGPIODispatcher.disable(conn, pin_trigger)
|
||||
|
||||
def pull_mode(%{ref: ref} = conn, mode) when mode in [:not_set, :none, :pull_up, :pull_down] do
|
||||
case Driver.set_pull_mode(ref, mode) do
|
||||
:ok -> {:error, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
112
lib/wafer/drivers/circuits_gpio_dispatcher.ex
Normal file
112
lib/wafer/drivers/circuits_gpio_dispatcher.ex
Normal file
|
@ -0,0 +1,112 @@
|
|||
defmodule Wafer.Driver.CircuitsGPIODispatcher do
|
||||
use GenServer
|
||||
alias __MODULE__
|
||||
alias Wafer.{Conn, GPIO, InterruptRegistry}
|
||||
alias Circuit.GPIO, as: Driver
|
||||
|
||||
@allowed_triggers ~w[rising falling both]a
|
||||
|
||||
@moduledoc """
|
||||
This module implements a simple dispatcher for GPIO interrupts when using
|
||||
`Circuits.GPIO`.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, [opts], name: CircuitsGPIODispatcher)
|
||||
|
||||
@doc """
|
||||
Enable interrupts for this connection using the specified trigger.
|
||||
"""
|
||||
@spec enable(Conn.t(), GPIO.pin_trigger()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable(conn, pin_trigger) when pin_trigger in @allowed_triggers,
|
||||
do: GenServer.call(CircuitsGPIODispatcher, {:enable, conn, pin_trigger})
|
||||
|
||||
@doc """
|
||||
Disable interrupts for this connection using the specified trigger.
|
||||
"""
|
||||
@spec disable(Conn.t(), GPIO.pin_trigger()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def disable(conn, pin_trigger) when pin_trigger in @allowed_triggers,
|
||||
do: GenServer.call(CircuitsGPIODispatcher, {:disable, conn, pin_trigger})
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
{:ok, %{subscriptions: %{}, values: %{}}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(
|
||||
{:enable, %{pin: pin, ref: ref} = conn, pin_trigger},
|
||||
_from,
|
||||
%{subscriptions: subscriptions} = state
|
||||
)
|
||||
when pin_trigger in @allowed_triggers do
|
||||
subscription = {conn, pin_trigger}
|
||||
|
||||
subscriptions =
|
||||
subscriptions
|
||||
|> Map.update(pin, MapSet.new([subscription]), &MapSet.put(&1, subscription))
|
||||
|
||||
case Driver.set_interrupts(ref, pin_trigger) do
|
||||
:ok ->
|
||||
{:reply, {:ok, conn}, %{state | subscriptions: subscriptions}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call(
|
||||
{:disable, %{pin: pin} = conn, pin_trigger},
|
||||
_from,
|
||||
%{subscriptions: subscriptions, values: values} = state
|
||||
)
|
||||
when pin_trigger in @allowed_triggers do
|
||||
subscription = {conn, pin_trigger}
|
||||
|
||||
pin_subscriptions =
|
||||
subscriptions
|
||||
|> Map.get(pin, MapSet.new())
|
||||
|> MapSet.delete(subscription)
|
||||
|
||||
subscriptions =
|
||||
subscriptions
|
||||
|> Map.put(pin, pin_subscriptions)
|
||||
|
||||
values = if Enum.empty?(pin_subscriptions), do: Map.delete(values, pin), else: values
|
||||
|
||||
{:reply, {:ok, conn}, %{state | values: values, subscriptions: subscriptions}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
{:circuits_gpio, pin, _timestamp, value},
|
||||
%{subscriptions: subscriptions, values: values} = state
|
||||
) do
|
||||
last_value = Map.get(values, pin, nil)
|
||||
|
||||
on_condition_change(last_value, value, fn condition ->
|
||||
subscriptions
|
||||
|> Map.get(pin, [])
|
||||
|> Stream.filter(fn
|
||||
{_conn, ^condition} -> true
|
||||
{_conn, :both} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.each(fn {conn, _} = registry_key ->
|
||||
Registry.dispatch(InterruptRegistry, registry_key, fn pids ->
|
||||
for {pid, _} <- pids do
|
||||
send(pid, {:interrupt, conn, condition})
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
{:noreply, %{state | values: Map.put(values, pin, value)}}
|
||||
end
|
||||
|
||||
defp on_condition_change(0, 1, callback), do: callback.(:rising)
|
||||
defp on_condition_change(1, 0, callback), do: callback.(:falling)
|
||||
defp on_condition_change(nil, 1, callback), do: callback.(:rising)
|
||||
defp on_condition_change(nil, 0, callback), do: callback.(:falling)
|
||||
defp on_condition_change(_, _, _), do: :no_change
|
||||
end
|
55
lib/wafer/drivers/circuits_i2c.ex
Normal file
55
lib/wafer/drivers/circuits_i2c.ex
Normal file
|
@ -0,0 +1,55 @@
|
|||
defmodule Wafer.Driver.CircuitsI2C do
|
||||
defstruct ~w[address bus ref]a
|
||||
@behaviour Wafer.Conn
|
||||
alias Circuits.I2C, as: Driver
|
||||
alias Wafer.Chip
|
||||
|
||||
@moduledoc """
|
||||
A connection to a chip via Circuits' I2C driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{address: Chip.i2c_address(), bus: binary, ref: reference}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the Circuits' I2C driver on the specified bus and address.
|
||||
"""
|
||||
@spec acquire(bus_name: binary, address: Chip.i2c_address()) ::
|
||||
{:ok, t} | {:error, reason :: any}
|
||||
def acquire(opts) when is_list(opts) do
|
||||
with {:ok, bus} <- Keyword.get(opts, :bus_name),
|
||||
{:ok, address} <- Keyword.get(opts, :address),
|
||||
{:ok, ref} <- Driver.open(bus) do
|
||||
{:ok, %__MODULE__{bus: bus, address: address, ref: ref}}
|
||||
else
|
||||
:error -> {:error, "Circuits.I2C requires both the `bus_name` and `address` options."}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@spec release(t) :: :ok | {:error, reason :: any}
|
||||
def release(%{ref: ref}), do: Circuits.I2C.close(ref)
|
||||
end
|
||||
|
||||
defimpl Wafer.Chip, for: Wafer.Driver.CircuitsI2C do
|
||||
alias Circuits.I2C, as: Driver
|
||||
|
||||
def read_register(%{ref: ref, address: address}, register_address, bytes),
|
||||
do: Driver.write_read(ref, address, <<register_address>>, bytes)
|
||||
|
||||
def read_register(_conn, _register_address, _bytes), do: {:error, "Invalid argument"}
|
||||
|
||||
def write_register(%{ref: ref, address: address}, register_address, data),
|
||||
do: Driver.write(ref, address, <<register_address, data>>)
|
||||
|
||||
def write_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
|
||||
|
||||
def swap_register(conn, register_address, data)
|
||||
when is_integer(register_address) and register_address >= 0 and is_binary(data) do
|
||||
with {:ok, old_data} <- read_register(conn, register_address, byte_size(data)),
|
||||
:ok <- write_register(conn, register_address, data) do
|
||||
{:ok, old_data}
|
||||
end
|
||||
end
|
||||
|
||||
def swap_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
|
||||
end
|
75
lib/wafer/drivers/elixir_ale_gpio.ex
Normal file
75
lib/wafer/drivers/elixir_ale_gpio.ex
Normal file
|
@ -0,0 +1,75 @@
|
|||
defmodule Wafer.Driver.ElixirAleGPIO do
|
||||
defstruct ~w[direction pid pin]a
|
||||
@behaviour Wafer.Conn
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
alias Wafer.GPIO
|
||||
|
||||
@moduledoc """
|
||||
A connection to a native GPIO pin via ElixirALE's GPIO driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{pid: pid}
|
||||
|
||||
@type options :: [option]
|
||||
@type option :: {:pin, non_neg_integer} | {:direction, GPIO.pin_direction()}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to the specified GPIO pin using the ElixirALE GPIO driver.
|
||||
|
||||
## Options
|
||||
|
||||
- `:pin` (required) - the integer pin number. Hardware dependent.
|
||||
- `:direction` - either `:in` or `:out`. Defaults to `:out`.
|
||||
"""
|
||||
@spec acquire(options) :: {:ok, t} | {:error, reason :: any}
|
||||
def acquire(opts) when is_list(opts) do
|
||||
with {:ok, pin} <- Keyword.get(opts, :pin),
|
||||
{:ok, direction} <- Keyword.get(opts, :direction, :out),
|
||||
{:ok, pid} <- Driver.start_link(pin, direction, Keyword.drop(opts, ~w[pin direction]a)) do
|
||||
%__MODULE__{pid: pid}
|
||||
else
|
||||
:error -> {:error, "ElixirALE.GPIO requires a `pin` option."}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Release all resources related to this GPIO pin connection.
|
||||
|
||||
Note that other connections may still be using the pin.
|
||||
"""
|
||||
@spec release(t) :: :ok | {:error, reason :: any}
|
||||
def release(%{pid: pid}), do: Driver.release(pid)
|
||||
end
|
||||
|
||||
defimpl Wafer.GPIOProto, for: Wafer.Driver.ElixirAleGPIO do
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
alias Wafer.Driver.ElixirAleGPIODispatcher
|
||||
|
||||
def read(%{pid: pid} = _conn) do
|
||||
case Driver.read(pid) do
|
||||
value when value in [0, 1] -> {:ok, value}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def write(%{pid: pid} = conn, value) when value in [0, 1] do
|
||||
case Driver.write(pid, value) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def direction(_conn, _direction),
|
||||
do:
|
||||
{:error,
|
||||
"ElixirALE doesn't support direction changing. Restart the connection process with the new direction instead."}
|
||||
|
||||
def enable_interrupt(conn, pin_trigger),
|
||||
do: ElixirAleGPIODispatcher.enable(conn, pin_trigger)
|
||||
|
||||
def disable_interrupt(conn, pin_trigger),
|
||||
do: ElixirAleGPIODispatcher.disable(conn, pin_trigger)
|
||||
|
||||
def pull_mode(_conn, _pull_mode), do: {:error, :not_supported}
|
||||
end
|
91
lib/wafer/drivers/elixir_ale_gpio_dispatcher.ex
Normal file
91
lib/wafer/drivers/elixir_ale_gpio_dispatcher.ex
Normal file
|
@ -0,0 +1,91 @@
|
|||
defmodule Wafer.Driver.ElixirAleGPIODispatcher do
|
||||
use GenServer
|
||||
alias __MODULE__
|
||||
alias Wafer.{Conn, GPIO, InterruptRegistry}
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
|
||||
@allowed_triggers ~w[rising falling both]a
|
||||
|
||||
@moduledoc """
|
||||
This module implements a simple dispatcher for GPIO interrupts when using
|
||||
`ElixirALE`.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
def start_link(opts),
|
||||
do: GenServer.start_link(__MODULE__, [opts], name: ElixirAleGPIODispatcher)
|
||||
|
||||
@doc """
|
||||
Enable intterrupts for this connection using the specified trigger.
|
||||
"""
|
||||
@spec enable(Conn.t(), GPIO.pin_trigger()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable(conn, pin_trigger) when pin_trigger in @allowed_triggers,
|
||||
do: GenServer.call(ElixirAleGPIODispatcher, {:enable, conn, pin_trigger})
|
||||
|
||||
@doc """
|
||||
Disable interrupts for this connection on the specified trigger.
|
||||
"""
|
||||
@spec disable(Conn.t(), GPIO.pin_trigger()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def disable(conn, pin_trigger) when pin_trigger in @allowed_triggers,
|
||||
do: GenServer.call(ElixirAleGPIODispatcher, {:disable, conn, pin_trigger})
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:enable, %{pin: pin, pid: pid} = conn, pin_trigger}, _from, state)
|
||||
when pin_trigger in @allowed_triggers do
|
||||
case Driver.set_int(pid, pin_trigger) do
|
||||
:ok ->
|
||||
subscription = {conn, pin_trigger}
|
||||
|
||||
state =
|
||||
state
|
||||
|> Map.update(pin, MapSet.new([subscription]), &MapSet.put(&1, subscription))
|
||||
|
||||
{:reply, {:ok, conn}, state}
|
||||
|
||||
{:error, reason} ->
|
||||
{:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:disable, %{pin: pin} = conn, pin_trigger}, _from, state)
|
||||
when pin_trigger in @allowed_triggers do
|
||||
subscription = {conn, pin_trigger}
|
||||
|
||||
pin_subscriptions =
|
||||
state
|
||||
|> Map.get(pin, MapSet.new())
|
||||
|> MapSet.delete(subscription)
|
||||
|
||||
state =
|
||||
state
|
||||
|> Map.put(pin, pin_subscriptions)
|
||||
|
||||
{:reply, {:ok, conn}, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:gpio_interrupt, pin, pin_trigger}, state)
|
||||
when pin_trigger in @allowed_triggers do
|
||||
state
|
||||
|> Map.get(pin, MapSet.new())
|
||||
|> Stream.filter(fn
|
||||
{_conn, ^pin_trigger} -> true
|
||||
{_conn, :both} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> Enum.each(fn {conn, _} = registry_key ->
|
||||
Registry.dispatch(InterruptRegistry, registry_key, fn pids ->
|
||||
for {pid, _} <- pids do
|
||||
send(pid, {:interrupt, conn, pin_trigger})
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
65
lib/wafer/drivers/elixir_ale_i2c.ex
Normal file
65
lib/wafer/drivers/elixir_ale_i2c.ex
Normal file
|
@ -0,0 +1,65 @@
|
|||
defmodule Wafer.Driver.ElixirAleI2C do
|
||||
defstruct ~w[address bus pid]a
|
||||
@behaviour Wafer.Conn
|
||||
alias ElixirALE.I2C, as: Driver
|
||||
alias Wafer.Chip
|
||||
|
||||
@moduledoc """
|
||||
A connection to a chip via ElixirALE's I2C driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{address: Chip.i2c_address(), bus: binary, pid: pid}
|
||||
|
||||
@type options :: [option]
|
||||
@type option :: {:bus_name, binary} | {:address, Chip.i2c_address()}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the ElixirALE I2C driver on the
|
||||
specified bus and address.
|
||||
"""
|
||||
@spec acquire(options) :: {:ok, t} | {:error, reason :: any}
|
||||
def acquire(opts) when is_list(opts) do
|
||||
with {:ok, bus} <- Keyword.get(opts, :bus_name),
|
||||
{:ok, address} <- Keyword.get(opts, :address),
|
||||
{:ok, pid} <- Driver.start_link(bus, address) do
|
||||
{:ok, %__MODULE__{bus: bus, address: address, pid: pid}}
|
||||
else
|
||||
:error -> {:error, "ElixirALE.I2C requires both `bus_name` and `address` options."}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@spec release(t) :: :ok | {:error, reason :: any}
|
||||
def release(%{pid: pid}), do: ElixirALE.I2C.release(pid)
|
||||
end
|
||||
|
||||
defimpl Wafer.Chip, for: Wafer.Driver.ElixirAleI2C do
|
||||
alias ElixirALE.I2C, as: Driver
|
||||
|
||||
def read_register(%{pid: pid}, register_address, bytes)
|
||||
when is_integer(register_address) and register_address >= 0 and is_integer(bytes) and
|
||||
bytes >= 0 do
|
||||
case Driver.write_read(pid, <<register_address>>, bytes) do
|
||||
data when is_binary(data) -> {:ok, data}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def read_register(_conn, _register_address, _bytes), do: {:error, "Invalid argument"}
|
||||
|
||||
def write_register(%{pid: pid}, register_address, data)
|
||||
when is_integer(register_address) and register_address >= 0 and is_binary(data),
|
||||
do: Driver.write(pid, <<register_address, data>>)
|
||||
|
||||
def write_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
|
||||
|
||||
def swap_register(conn, register_address, data)
|
||||
when is_integer(register_address) and register_address >= 0 and is_binary(data) do
|
||||
with {:ok, old_data} <- read_register(conn, register_address, byte_size(data)),
|
||||
:ok <- write_register(conn, register_address, data) do
|
||||
{:ok, old_data}
|
||||
end
|
||||
end
|
||||
|
||||
def swap_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
|
||||
end
|
62
lib/wafer/gpio.ex
Normal file
62
lib/wafer/gpio.ex
Normal file
|
@ -0,0 +1,62 @@
|
|||
defmodule Wafer.GPIO do
|
||||
alias Wafer.{Conn, GPIOProto, InterruptRegistry}
|
||||
|
||||
@moduledoc """
|
||||
A `GPIO` is a physical pin which can be read from and written to.
|
||||
"""
|
||||
|
||||
@type pin_direction :: :in | :out
|
||||
@type pin_trigger :: :none | :rising | :falling | :both
|
||||
@type pin_value :: 0 | 1
|
||||
@type pull_mode :: :not_set | :none | :pull_up | :pull_down
|
||||
|
||||
@type interrupt_options :: [interrupt_option]
|
||||
@type interrupt_option :: {:suppress_glitches, boolean} | {:receiver, pid}
|
||||
|
||||
@doc """
|
||||
Read the current pin value.
|
||||
"""
|
||||
@spec read(Conn.t()) :: {:ok, pin_value, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate read(conn), to: GPIOProto
|
||||
|
||||
@doc """
|
||||
Set the pin value.
|
||||
"""
|
||||
@spec write(Conn.t(), pin_value) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate write(conn, pin_value), to: GPIOProto
|
||||
|
||||
@doc """
|
||||
Set the pin direction.
|
||||
"""
|
||||
@spec direction(Conn.t(), pin_direction) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate direction(conn, pin_direction), to: GPIOProto
|
||||
|
||||
@doc """
|
||||
Enable an interrupt for this pin.
|
||||
|
||||
Interrupts will be sent to the calling process as messages in the form of
|
||||
`{:interrupt, Conn.t(), pin_value}`.
|
||||
|
||||
## Implementors note
|
||||
|
||||
`Wafer` starts it's own `Registry` named `Wafer.InterruptRegistry` which
|
||||
you should publish your interrupts to using the above format. The registry
|
||||
key is set as follows: `{Conn.t(), pin_trigger}`.
|
||||
"""
|
||||
@spec enable_interrupt(Conn.t(), pin_trigger) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable_interrupt(conn, pin_trigger) do
|
||||
with {:ok, _pid} <- Registry.register(InterruptRegistry, {conn, pin_trigger}, nil),
|
||||
{:ok, conn} <- GPIOProto.enable_interrupt(conn, pin_trigger),
|
||||
do: {:ok, conn}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the pull mode for this pin.
|
||||
|
||||
If the hardware contains software-switchable pull-up and/or pull-down
|
||||
resistors you can configure them this way. If they are not supported then
|
||||
this function will return `{:error, :not_supported}`.
|
||||
"""
|
||||
@spec pull_mode(Conn.t(), pull_mode) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate pull_mode(conn, pull_mode), to: GPIOProto
|
||||
end
|
54
lib/wafer/gpio_proto.ex
Normal file
54
lib/wafer/gpio_proto.ex
Normal file
|
@ -0,0 +1,54 @@
|
|||
defprotocol Wafer.GPIOProto do
|
||||
alias Wafer.{Conn, GPIO}
|
||||
|
||||
@moduledoc """
|
||||
A `GPIO` is a physical pin which can be read from and written to. This is the
|
||||
protocol used to interract with the pin. Used via `Wafer.GPIO`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Read the current pin value.
|
||||
"""
|
||||
@spec read(Conn.t()) :: {:ok, GPIO.pin_value()} | {:error, reason :: any}
|
||||
def read(conn)
|
||||
|
||||
@doc """
|
||||
Set the pin value.
|
||||
"""
|
||||
@spec write(Conn.t(), GPIO.pin_value()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def write(conn, pin_value)
|
||||
|
||||
@doc """
|
||||
Set the pin direction.
|
||||
"""
|
||||
@spec direction(Conn.t(), GPIO.pin_direction()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def direction(conn, pin_direction)
|
||||
|
||||
@doc """
|
||||
Enable an interrupt for this pin.
|
||||
|
||||
Interrupts will be sent to the calling process as messages in the form of
|
||||
`{:interrupt, Conn.t(), GPIO.pin_value()}`.
|
||||
|
||||
## Implementors note
|
||||
|
||||
`Wafer` starts it's own `Registry` named `Wafer.InterruptRegistry` which
|
||||
you should publish your interrupts to using the above format. The registry
|
||||
key is set as follows: `{Conn.t(), pin_trigger}`.
|
||||
"""
|
||||
@spec enable_interrupt(Conn.t(), GPIO.pin_trigger()) ::
|
||||
{:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable_interrupt(conn, pin_trigger)
|
||||
|
||||
@doc """
|
||||
Set the pull-mode for this pin.
|
||||
|
||||
## Implementors note
|
||||
|
||||
If your GPIO device does not contain any internal resistors for pull up or
|
||||
pull down operation then simply return `{:error, :not_supported}` from this
|
||||
call.
|
||||
"""
|
||||
@spec pull_mode(Conn.t(), GPIO.pull_mode()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def pull_mode(conn, pull_mode)
|
||||
end
|
124
lib/wafer/registers.ex
Normal file
124
lib/wafer/registers.ex
Normal file
|
@ -0,0 +1,124 @@
|
|||
defmodule Wafer.Registers do
|
||||
@moduledoc """
|
||||
This module provides helpful macros for specifying the registers used to
|
||||
communicate with your device.
|
||||
"""
|
||||
alias Wafer.Chip
|
||||
alias Wafer.Conn
|
||||
|
||||
@type register_name :: atom
|
||||
@type access_mode :: :ro | :rw | :wo
|
||||
@type bytes :: non_neg_integer
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import Wafer.Registers
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Define a registers.
|
||||
|
||||
## Parameters
|
||||
- `name` - name of the register.
|
||||
- `register_address` - the address of the register.
|
||||
- `mode` the access mode of the register.
|
||||
- `bytes` the number of bytes in the register.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> defregister(:status, 0x03, :ro, 1)
|
||||
|
||||
iex> defregister(:config, 0x01, :rw, 2)
|
||||
|
||||
iex> defregister(:int_en, 0x02, :wo, 1)
|
||||
|
||||
"""
|
||||
defmacro defregister(name, register_address, :ro, bytes)
|
||||
when is_atom(name) and is_integer(register_address) and register_address >= 0 and
|
||||
is_integer(bytes) and bytes >= 0 do
|
||||
quote do
|
||||
@spec unquote(:"read_#{name}")(Conn.t()) :: {:ok, binary} | {:error, reason :: any}
|
||||
def unquote(:"read_#{name}")(conn),
|
||||
do: Chip.read_register(conn, unquote(register_address), unquote(bytes))
|
||||
end
|
||||
end
|
||||
|
||||
defmacro defregister(name, register_address, :wo, bytes)
|
||||
when is_atom(name) and is_integer(register_address) and register_address >= 0 and
|
||||
is_integer(bytes) and bytes >= 0 do
|
||||
quote do
|
||||
@spec unquote(:"write_#{name}")(Conn.t(), data :: binary) :: :ok | {:error, reason :: any}
|
||||
def unquote(:"write_#{name}")(conn, data)
|
||||
when is_binary(data) and byte_size(data) == unquote(bytes),
|
||||
do: Chip.write_register(conn, unquote(register_address), data)
|
||||
|
||||
def unquote(:"write_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defmacro defregister(name, register_address, :rw, bytes)
|
||||
when is_atom(name) and is_integer(register_address) and register_address >= 0 and
|
||||
is_integer(bytes) and bytes >= 0 do
|
||||
quote do
|
||||
@spec unquote(:"read_#{name}")(Conn.t()) :: {:ok, binary} | {:error, reason :: any}
|
||||
def unquote(:"read_#{name}")(conn),
|
||||
do: Chip.read_register(conn, unquote(register_address), unquote(bytes))
|
||||
|
||||
@spec unquote(:"write_#{name}")(Conn.t(), data :: binary) :: :ok | {:error, reason :: any}
|
||||
def unquote(:"write_#{name}")(conn, data)
|
||||
when is_binary(data) and byte_size(data) == unquote(bytes),
|
||||
do: Chip.write_register(conn, unquote(register_address), data)
|
||||
|
||||
def unquote(:"write_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}
|
||||
|
||||
@spec unquote(:"swap_#{name}")(Conn.t(), data :: binary) ::
|
||||
:ok | {:error, reason :: any}
|
||||
def unquote(:"swap_#{name}")(conn, data)
|
||||
when is_binary(data) and byte_size(data) == unquote(bytes),
|
||||
do: Chip.swap_register(conn, unquote(register_address), data)
|
||||
|
||||
@spec unquote(:"update_#{name}")(
|
||||
Conn.t(),
|
||||
(<<_::_*unquote(bytes * 8)>> -> <<_::_*unquote(bytes * 8)>>)
|
||||
) :: :ok | {:error, reason :: any}
|
||||
def unquote(:"update_#{name}")(conn, callback) when is_function(callback, 1) do
|
||||
with {:ok, old_data} <-
|
||||
Chip.read_register(conn, unquote(register_address), unquote(bytes)),
|
||||
new_data when is_binary(new_data) and byte_size(new_data) == unquote(bytes) <-
|
||||
callback.(old_data),
|
||||
:ok <- Chip.write_register(conn, unquote(register_address), new_data),
|
||||
do: :ok
|
||||
end
|
||||
|
||||
def unquote(:"update_#{name}")(_conn, callback),
|
||||
do: {:error, "Argument error: callback should be an arity 1 function"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Define a register with common defaults:
|
||||
|
||||
## Examples
|
||||
|
||||
When specified with an access mode, assumes a 1 byte register:
|
||||
|
||||
iex> defregister(:status, 0x03, :ro)
|
||||
|
||||
When specified with a byte size, assumes a `:rw` register:
|
||||
|
||||
iex> defregister(:config, 0x02, 2)
|
||||
|
||||
"""
|
||||
defmacro defregister(name, register_address, mode) when mode in ~w[ro rw wo]a do
|
||||
quote do
|
||||
defregister(unquote(name), unquote(register_address), unquote(mode), 1)
|
||||
end
|
||||
end
|
||||
|
||||
defmacro defregister(name, register_address, bytes) when is_integer(bytes) and bytes >= 0 do
|
||||
quote do
|
||||
defregister(unquote(name), unquote(register_address), :rw, unquote(bytes))
|
||||
end
|
||||
end
|
||||
end
|
17
mix.exs
17
mix.exs
|
@ -1,11 +1,12 @@
|
|||
defmodule ChipHop.MixProject do
|
||||
defmodule Wafer.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :chip_hop,
|
||||
app: :wafer,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.9",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
|
@ -15,15 +16,21 @@ defmodule ChipHop.MixProject do
|
|||
def application do
|
||||
[
|
||||
extra_applications: [:logger],
|
||||
mod: {ChipHop.Application, []}
|
||||
mod: {Wafer.Application, []}
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
{:mimic, "~> 1.1", only: :test},
|
||||
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
|
||||
{:elixir_ale, "~> 1.2", only: :dev},
|
||||
{:circuits_i2c, "~> 0.3", only: :dev},
|
||||
{:circuits_gpio, "~> 0.4", only: :dev}
|
||||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test), do: ["test/support" | elixirc_paths(nil)]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
end
|
||||
|
|
10
mix.lock
Normal file
10
mix.lock
Normal file
|
@ -0,0 +1,10 @@
|
|||
%{
|
||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
|
||||
"circuits_gpio": {:hex, :circuits_gpio, "0.4.3", "1a53dff1eaeefb9f67f4ebc2c1852b603683eedaa6053bed51c038dd64b978bb", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"circuits_i2c": {:hex, :circuits_i2c, "0.3.5", "43e043d7efc3aead364061f8a7ed627f81ff7cef52bfa47cb629d8a68ca56a9f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"elixir_ale": {:hex, :elixir_ale, "1.2.1", "07ac2f17a0191b8bd3b0df6b526c7f699a3a4d690c9def573fcb5824eef24d98", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"},
|
||||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"},
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
defmodule ChipHopTest do
|
||||
use ExUnit.Case
|
||||
doctest ChipHop
|
||||
|
||||
test "greets the world" do
|
||||
assert ChipHop.hello() == :world
|
||||
end
|
||||
end
|
153
test/registers_test.exs
Normal file
153
test/registers_test.exs
Normal file
|
@ -0,0 +1,153 @@
|
|||
defmodule WaferRegistersTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias Wafer.Chip
|
||||
|
||||
describe "read-only register" do
|
||||
test "the register read function is defined" do
|
||||
assert function_exported?(test_mod(), :read_ro_test, 1)
|
||||
end
|
||||
|
||||
test "the register write function is not defined" do
|
||||
refute function_exported?(test_mod(), :write_ro_test, 2)
|
||||
end
|
||||
|
||||
test "the register swap function is not defined" do
|
||||
refute function_exported?(test_mod(), :swap_ro_test, 2)
|
||||
end
|
||||
|
||||
test "the update register function is not defined" do
|
||||
refute function_exported?(test_mod(), :update_ro_test, 2)
|
||||
end
|
||||
|
||||
test "the register can be read from" do
|
||||
mod = test_mod()
|
||||
|
||||
Chip
|
||||
|> expect(:read_register, 1, fn conn, address, bytes ->
|
||||
{:read_register, conn, address, bytes}
|
||||
end)
|
||||
|
||||
assert mod.read_ro_test(:conn) == {:read_register, :conn, 1, 3}
|
||||
end
|
||||
end
|
||||
|
||||
describe "write-only register" do
|
||||
test "the register read function is not defined" do
|
||||
refute function_exported?(test_mod(), :read_wo_test, 1)
|
||||
end
|
||||
|
||||
test "the register write function is defined" do
|
||||
assert function_exported?(test_mod(), :write_wo_test, 2)
|
||||
end
|
||||
|
||||
test "the register swap function is not defined" do
|
||||
refute function_exported?(test_mod(), :swap_wo_test, 2)
|
||||
end
|
||||
|
||||
test "the update register function is not defined" do
|
||||
refute function_exported?(test_mod(), :update_wo_test, 2)
|
||||
end
|
||||
|
||||
test "the register can be written to" do
|
||||
mod = test_mod()
|
||||
|
||||
Chip
|
||||
|> expect(:write_register, 1, fn conn, address, data ->
|
||||
{:write_register, conn, address, data}
|
||||
end)
|
||||
|
||||
assert mod.write_wo_test(:conn, <<1, 1, 3, 4, 7>>) ==
|
||||
{:write_register, :conn, 2, <<1, 1, 3, 4, 7>>}
|
||||
end
|
||||
end
|
||||
|
||||
describe "read-write register" do
|
||||
test "the register read function is not defined" do
|
||||
assert function_exported?(test_mod(), :read_rw_test, 1)
|
||||
end
|
||||
|
||||
test "the register write function is defined" do
|
||||
assert function_exported?(test_mod(), :write_rw_test, 2)
|
||||
end
|
||||
|
||||
test "the register swap function is defined" do
|
||||
assert function_exported?(test_mod(), :swap_rw_test, 2)
|
||||
end
|
||||
|
||||
test "the update register function is defined" do
|
||||
assert function_exported?(test_mod(), :update_rw_test, 2)
|
||||
end
|
||||
|
||||
test "the register can be read from" do
|
||||
mod = test_mod()
|
||||
|
||||
Chip
|
||||
|> expect(:read_register, 1, fn conn, address, bytes ->
|
||||
{:read_register, conn, address, bytes}
|
||||
end)
|
||||
|
||||
assert mod.read_rw_test(:conn) == {:read_register, :conn, 3, 7}
|
||||
end
|
||||
|
||||
test "the register can be written to" do
|
||||
mod = test_mod()
|
||||
|
||||
Chip
|
||||
|> expect(:write_register, 1, fn conn, address, data ->
|
||||
{:write_register, conn, address, data}
|
||||
end)
|
||||
|
||||
assert mod.write_rw_test(:conn, <<1, 1, 3, 4, 7, 11, 18>>) ==
|
||||
{:write_register, :conn, 3, <<1, 1, 3, 4, 7, 11, 18>>}
|
||||
end
|
||||
|
||||
test "the register contents can be swapped" do
|
||||
mod = test_mod()
|
||||
|
||||
Chip
|
||||
|> expect(:swap_register, 1, fn conn, address, data ->
|
||||
{:swap_register, conn, address, data}
|
||||
end)
|
||||
|
||||
assert mod.swap_rw_test(:conn, <<1, 1, 3, 4, 7, 11, 18>>) ==
|
||||
{:swap_register, :conn, 3, <<1, 1, 3, 4, 7, 11, 18>>}
|
||||
end
|
||||
|
||||
test "the register contents can be updated" do
|
||||
mod = test_mod()
|
||||
|
||||
Chip
|
||||
|> expect(:read_register, 1, fn conn, address, bytes ->
|
||||
assert conn == :conn
|
||||
assert address == 3
|
||||
assert bytes == 7
|
||||
{:ok, <<1, 1, 3, 4, 7, 11, 18>>}
|
||||
end)
|
||||
|> expect(:write_register, 1, fn conn, address, data ->
|
||||
assert conn == :conn
|
||||
assert address == 3
|
||||
assert data == <<18, 11, 7, 4, 3, 1, 1>>
|
||||
:ok
|
||||
end)
|
||||
|
||||
swapper = fn <<a, b, c, d, e, f, g>> -> <<g, f, e, d, c, b, a>> end
|
||||
|
||||
assert mod.update_rw_test(:conn, swapper) == :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp test_mod() do
|
||||
mod = TestUtils.random_module_name()
|
||||
|
||||
defmodule mod do
|
||||
use Wafer.Registers
|
||||
|
||||
defregister(:ro_test, 1, :ro, 3)
|
||||
defregister(:wo_test, 2, :wo, 5)
|
||||
defregister(:rw_test, 3, :rw, 7)
|
||||
end
|
||||
|
||||
mod
|
||||
end
|
||||
end
|
9
test/support/elixir_ale_i2c.ex
Normal file
9
test/support/elixir_ale_i2c.ex
Normal file
|
@ -0,0 +1,9 @@
|
|||
defmodule ElixirALE.I2C do
|
||||
def write_read(pid, data, bytes)
|
||||
when is_pid(pid) and is_bitstring(data) and is_integer(bytes) and bytes >= 1 do
|
||||
bits = bytes * 8
|
||||
<<0::unsigned-integer-size(bits)>>
|
||||
end
|
||||
|
||||
def write(pid, data) when is_pid(pid) and is_bitstring(data), do: :ok
|
||||
end
|
10
test/support/utils.ex
Normal file
10
test/support/utils.ex
Normal file
|
@ -0,0 +1,10 @@
|
|||
defmodule TestUtils do
|
||||
def random_module_name do
|
||||
name =
|
||||
16
|
||||
|> :crypto.strong_rand_bytes()
|
||||
|> Base.encode64(padding: false)
|
||||
|
||||
Module.concat(__MODULE__, name)
|
||||
end
|
||||
end
|
|
@ -1 +1,3 @@
|
|||
Mimic.copy(ElixirALE.I2C)
|
||||
Mimic.copy(Wafer.Chip)
|
||||
ExUnit.start()
|
||||
|
|
4
test/wafer_test.exs
Normal file
4
test/wafer_test.exs
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule WaferTest do
|
||||
use ExUnit.Case
|
||||
doctest Wafer
|
||||
end
|
Loading…
Reference in a new issue