First pass.

This commit is contained in:
James Harton 2019-12-29 17:12:36 +13:00
parent 9d28a79b45
commit fb247dbead
24 changed files with 1036 additions and 40 deletions

2
.gitignore vendored
View file

@ -20,5 +20,5 @@ erl_crash.dump
*.ez
# Ignore package tarball (built via "mix hex.build").
chip_hop-*.tar
wafer-*.tar

View file

@ -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).

View file

@ -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
View 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

View file

@ -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
View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View file

@ -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
View 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"},
}

View file

@ -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
View 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

View 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
View 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

View file

@ -1 +1,3 @@
Mimic.copy(ElixirALE.I2C)
Mimic.copy(Wafer.Chip)
ExUnit.start()

4
test/wafer_test.exs Normal file
View file

@ -0,0 +1,4 @@
defmodule WaferTest do
use ExUnit.Case
doctest Wafer
end