I believe that everything I wanted now works. Yowza!
This commit is contained in:
parent
fb247dbead
commit
33e332ea22
38 changed files with 1744 additions and 260 deletions
32
.gitlab-ci.yml
Normal file
32
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,32 @@
|
|||
image: elixir:latest
|
||||
|
||||
cache:
|
||||
key: "$CI_JOB_NAME"
|
||||
paths:
|
||||
- deps
|
||||
- _build
|
||||
- /root/.mix
|
||||
|
||||
variables:
|
||||
MIX_ENV: "test"
|
||||
|
||||
before_script:
|
||||
- mix local.hex --force
|
||||
- mix local.rebar --force
|
||||
- mix deps.get --only test
|
||||
|
||||
test:
|
||||
script:
|
||||
- mix test
|
||||
|
||||
credo:
|
||||
script:
|
||||
- mix credo
|
||||
|
||||
audit:
|
||||
script:
|
||||
- mix hex.audit
|
||||
|
||||
format:
|
||||
script:
|
||||
- mix format --check-formatted
|
|
@ -1,6 +1,10 @@
|
|||
# Wafer
|
||||
|
||||
**TODO: Add description**
|
||||
Wafer is an OTP application that assists with writing drivers for peripherals using I2C, SPI and GPIO pins.
|
||||
|
||||
Wafer provides Elixir protocols for interacting with device registers and dealing with GPIO, so that you can use directly connected hardware GPIO pins or GPIO expanders such as the [MCP23008](https://www.microchip.com/wwwproducts/en/MCP23008) or the [CD74HC595](http://www.ti.com/product/CD74HC595) SPI shift register.
|
||||
|
||||
Wafer implements the [GPIO](https://hexdocs.pm/wafer/Wafer.GPIOProto.html) and [Chip](https://hexdocs.pm/wafer/Wafer.Chip.html) protocols for [ElixirALE](https://hex.pm/packages/elixir_ale)'s GPIO and I2C drivers, [Circuits.GPIO](https://hex.pm/packages/circuits_gpio) and [Circuits.I2C](https://hex.pm/packages/circuits_i2c). Implementing it for SPI should also be trivial, I just don't have any SPI devices to test with at the moment.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -10,7 +14,7 @@ by adding `wafer` to your list of dependencies in `mix.exs`:
|
|||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:wafer, "~> 0.1.0"}
|
||||
{:wafer, "~> 0.1"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Wafer.Application do
|
|||
def start(_type, _args) do
|
||||
children = [
|
||||
{Registry, [keys: :duplicate, name: Wafer.InterruptRegistry]},
|
||||
Wafer.Driver.ElixirAleGPIODispatcher,
|
||||
Wafer.Driver.ElixirALEGPIODispatcher,
|
||||
Wafer.Driver.CircuitsGPIODispatcher
|
||||
]
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ defprotocol Wafer.Chip do
|
|||
written to.
|
||||
"""
|
||||
|
||||
@type i2c_address :: 0..0x7F
|
||||
@type register_address :: non_neg_integer
|
||||
@type bytes :: non_neg_integer
|
||||
|
||||
|
@ -21,7 +20,7 @@ defprotocol Wafer.Chip do
|
|||
|
||||
## Example
|
||||
|
||||
iex> {:ok, conn} = ElixirAleI2C.acquire(bus: "i2c-1", address: 0x68)
|
||||
iex> {:ok, conn} = ElixirALEI2C.acquire(bus: "i2c-1", address: 0x68)
|
||||
...> Chip.read_register(conn, 0, 1)
|
||||
{:ok, <<0>>}
|
||||
"""
|
||||
|
@ -40,12 +39,12 @@ defprotocol Wafer.Chip do
|
|||
|
||||
## Example
|
||||
|
||||
iex> {:ok, conn} = ElixirAleI2C.acquire(bus: "i2c", address: 0x68)
|
||||
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}
|
||||
{:ok, t} | {:error, reason :: any}
|
||||
def write_register(conn, register_address, data)
|
||||
|
||||
@doc """
|
||||
|
@ -65,11 +64,11 @@ defprotocol Wafer.Chip do
|
|||
|
||||
## Example
|
||||
|
||||
iex> {:ok, conn} = ElixirAleI2C.acquire(bus: "i2c", address: 0x68)
|
||||
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}
|
||||
{:ok, data :: binary, t} | {:error, reason :: any}
|
||||
def swap_register(conn, register_address, new_data)
|
||||
end
|
||||
|
|
|
@ -3,12 +3,13 @@ defmodule Wafer.Conn do
|
|||
Defines a protocol and behaviour for connecting to a peripheral.
|
||||
"""
|
||||
|
||||
@type options :: [option]
|
||||
@type option :: {atom, any}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the provided driver.
|
||||
"""
|
||||
@callback acquire(opts :: [option]) :: t :: {:error, reason :: any}
|
||||
@callback acquire(options) :: t :: {:error, reason :: any}
|
||||
|
||||
@doc """
|
||||
Release all resources associated with this connection.
|
||||
|
|
|
@ -3,9 +3,10 @@ defmodule Wafer.Driver.CircuitsGPIO do
|
|||
@behaviour Wafer.Conn
|
||||
alias Circuits.GPIO, as: Driver
|
||||
alias Wafer.GPIO
|
||||
import Wafer.Guards
|
||||
|
||||
@moduledoc """
|
||||
A connection to a native GPIO pin via Circuit's GPIO driver.
|
||||
A connection to a native GPIO pin via Circuits' GPIO driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{ref: reference, pin: non_neg_integer, direction: GPIO.pin_direction()}
|
||||
|
@ -23,10 +24,10 @@ defmodule Wafer.Driver.CircuitsGPIO do
|
|||
"""
|
||||
@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),
|
||||
with pin when is_pin_number(pin) <- Keyword.get(opts, :pin),
|
||||
direction when is_pin_direction(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}
|
||||
{:ok, %__MODULE__{ref: ref, pin: pin, direction: direction}}
|
||||
else
|
||||
:error -> {:error, "Circuits.GPIO requires a `pin` option."}
|
||||
{:error, reason} -> {:error, reason}
|
||||
|
@ -39,21 +40,22 @@ defmodule Wafer.Driver.CircuitsGPIO do
|
|||
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)
|
||||
def release(%__MODULE__{ref: ref}) when is_reference(ref), do: Driver.close(ref)
|
||||
end
|
||||
|
||||
defimpl Wafer.GPIOProto, for: Wafer.Driver.CircuitsGPIO do
|
||||
defimpl Wafer.GPIO, for: Wafer.Driver.CircuitsGPIO do
|
||||
alias Wafer.Driver.CircuitsGPIODispatcher
|
||||
alias Circuits.GPIO, as: Driver
|
||||
import Wafer.Guards
|
||||
|
||||
def read(%{ref: ref}) do
|
||||
def read(%{ref: ref}) when is_reference(ref) do
|
||||
case(Driver.read(ref)) do
|
||||
value when value in [0, 1] -> {:ok, value}
|
||||
value when is_pin_value(value) -> {:ok, value}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def write(%{ref: ref} = conn, value) when value in [0, 1] do
|
||||
def write(%{ref: ref} = conn, value) when is_reference(ref) and is_pin_value(value) do
|
||||
case(Driver.write(ref, value)) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
|
@ -63,19 +65,23 @@ defimpl Wafer.GPIOProto, for: Wafer.Driver.CircuitsGPIO do
|
|||
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
|
||||
def direction(%{ref: ref} = conn, direction)
|
||||
when is_reference(ref) and is_pin_direction(direction) do
|
||||
case(Driver.set_direction(ref, direction)) do
|
||||
:ok -> %{conn | direction: direction}
|
||||
:ok -> {: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 enable_interrupt(conn, pin_condition) when is_pin_condition(pin_condition),
|
||||
do: CircuitsGPIODispatcher.enable(conn, pin_condition)
|
||||
|
||||
def pull_mode(%{ref: ref} = conn, mode) when mode in [:not_set, :none, :pull_up, :pull_down] do
|
||||
def disable_interrupt(conn, pin_condition) when is_pin_condition(pin_condition),
|
||||
do: CircuitsGPIODispatcher.disable(conn, pin_condition)
|
||||
|
||||
def pull_mode(%{ref: ref} = conn, mode) when is_reference(ref) and is_pin_pull_mode(mode) do
|
||||
case Driver.set_pull_mode(ref, mode) do
|
||||
:ok -> {:error, conn}
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
defmodule Wafer.Driver.CircuitsGPIODispatcher do
|
||||
use GenServer
|
||||
alias __MODULE__
|
||||
alias Circuits.GPIO, as: Driver
|
||||
alias Wafer.{Conn, GPIO, InterruptRegistry}
|
||||
alias Circuit.GPIO, as: Driver
|
||||
|
||||
@allowed_triggers ~w[rising falling both]a
|
||||
import Wafer.Guards
|
||||
|
||||
@moduledoc """
|
||||
This module implements a simple dispatcher for GPIO interrupts when using
|
||||
`Circuits.GPIO`.
|
||||
|
||||
Because the Circuit's interrupt doesn't provide an indication of whether the
|
||||
pin is rising or falling we store the last known pin state and use it to
|
||||
compare.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
|
@ -17,87 +20,58 @@ defmodule Wafer.Driver.CircuitsGPIODispatcher do
|
|||
@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})
|
||||
@spec enable(Conn.t(), GPIO.pin_condition()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable(conn, pin_condition) when is_pin_condition(pin_condition),
|
||||
do: GenServer.call(CircuitsGPIODispatcher, {:enable, conn, pin_condition, self()})
|
||||
|
||||
@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})
|
||||
@spec disable(Conn.t(), GPIO.pin_condition()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def disable(conn, pin_condition) when is_pin_condition(pin_condition),
|
||||
do: GenServer.call(CircuitsGPIODispatcher, {:disable, conn, pin_condition})
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
{:ok, %{subscriptions: %{}, values: %{}}}
|
||||
{:ok, %{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
|
||||
def handle_call({:enable, %{pin: pin, ref: ref} = conn, pin_condition, receiver}, _from, state)
|
||||
when is_pin_condition(pin_condition) and is_pid(receiver) and is_reference(ref) and
|
||||
is_pin_number(pin) do
|
||||
case Driver.set_interrupts(ref, pin_condition) do
|
||||
:ok ->
|
||||
{:reply, {:ok, conn}, %{state | subscriptions: subscriptions}}
|
||||
subscribe(pin, pin_condition, conn, receiver)
|
||||
{:reply, {:ok, conn}, state}
|
||||
|
||||
{: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}
|
||||
def handle_call({:disable, %{pin: pin} = conn, pin_condition}, _from, %{values: values} = state)
|
||||
when is_pin_number(pin) and is_pin_condition(pin_condition) do
|
||||
unsubscribe(pin, pin_condition, conn)
|
||||
|
||||
pin_subscriptions =
|
||||
subscriptions
|
||||
|> Map.get(pin, MapSet.new())
|
||||
|> MapSet.delete(subscription)
|
||||
values = if any_pin_subs?(pin), do: values, else: Map.delete(values, pin)
|
||||
|
||||
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}}
|
||||
{:reply, {:ok, conn}, %{state | values: values}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
{:circuits_gpio, pin, _timestamp, value},
|
||||
%{subscriptions: subscriptions, values: values} = state
|
||||
) do
|
||||
%{values: values} = state
|
||||
)
|
||||
when is_pin_number(pin) and is_pin_value(value) 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)
|
||||
Registry.dispatch(InterruptRegistry, {__MODULE__, pin, condition}, fn subs ->
|
||||
for {pid, conn} <- subs do
|
||||
send(pid, {:interrupt, conn, condition})
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
|
@ -109,4 +83,55 @@ defmodule Wafer.Driver.CircuitsGPIODispatcher do
|
|||
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
|
||||
|
||||
defp subscribe(pin, :rising, conn, receiver),
|
||||
do:
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :rising}, conn},
|
||||
receiver
|
||||
)
|
||||
|
||||
defp subscribe(pin, :falling, conn, receiver),
|
||||
do:
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :falling}, conn},
|
||||
receiver
|
||||
)
|
||||
|
||||
defp subscribe(pin, :both, conn, receiver) do
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :rising}, conn},
|
||||
receiver
|
||||
)
|
||||
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :falling}, conn},
|
||||
receiver
|
||||
)
|
||||
end
|
||||
|
||||
defp unsubscribe(pin, :rising, conn),
|
||||
do: Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :rising}, conn)
|
||||
|
||||
defp unsubscribe(pin, :falling, conn),
|
||||
do: Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :falling}, conn)
|
||||
|
||||
defp unsubscribe(pin, :both, conn) do
|
||||
Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :rising}, conn)
|
||||
Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :falling}, conn)
|
||||
end
|
||||
|
||||
defp any_pin_subs?(pin) do
|
||||
rising_subs =
|
||||
InterruptRegistry
|
||||
|> Registry.lookup({__MODULE__, pin, :rising})
|
||||
|
||||
falling_subs =
|
||||
InterruptRegistry
|
||||
|> Registry.lookup({__MODULE__, pin, :falling})
|
||||
|
||||
rising_subs
|
||||
|> Stream.concat(falling_subs)
|
||||
|> Enum.any?()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,54 +2,108 @@ defmodule Wafer.Driver.CircuitsI2C do
|
|||
defstruct ~w[address bus ref]a
|
||||
@behaviour Wafer.Conn
|
||||
alias Circuits.I2C, as: Driver
|
||||
alias Wafer.Chip
|
||||
alias Wafer.I2C
|
||||
import Wafer.Guards
|
||||
|
||||
@moduledoc """
|
||||
A connection to a chip via Circuits' I2C driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{address: Chip.i2c_address(), bus: binary, ref: reference}
|
||||
@type t :: %__MODULE__{address: I2C.address(), bus: binary, ref: reference}
|
||||
@type options :: [option]
|
||||
@type option :: {:bus_name, binary} | {:address, I2C.address()} | {:force, boolean}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the Circuits' I2C driver on the specified bus and address.
|
||||
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}
|
||||
@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, ref} <- Driver.open(bus) do
|
||||
with bus when is_binary(bus) <- Keyword.get(opts, :bus_name),
|
||||
address when is_i2c_address(address) <- Keyword.get(opts, :address),
|
||||
{:ok, ref} when is_reference(ref) <- Driver.open(bus),
|
||||
devices when is_list(devices) <- Driver.detect_devices(ref),
|
||||
true <- Keyword.get(opts, :force, false) || Enum.member?(devices, address) 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}
|
||||
false ->
|
||||
{:error, "No device detected at address. Pass `force: true` to override."}
|
||||
|
||||
: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)
|
||||
def release(%__MODULE__{ref: ref}), do: Driver.close(ref)
|
||||
end
|
||||
|
||||
defimpl Wafer.Chip, for: Wafer.Driver.CircuitsI2C do
|
||||
alias Circuits.I2C, as: Driver
|
||||
import Wafer.Guards
|
||||
|
||||
def read_register(%{ref: ref, address: address}, register_address, bytes),
|
||||
do: Driver.write_read(ref, address, <<register_address>>, bytes)
|
||||
def read_register(%{ref: ref, address: address}, register_address, bytes)
|
||||
when is_reference(ref) and is_i2c_address(address) and is_register_address(register_address) and
|
||||
is_byte_size(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(%{ref: ref, address: address} = conn, register_address, data)
|
||||
when is_reference(ref) and is_i2c_address(address) and is_register_address(register_address) and
|
||||
is_binary(data) do
|
||||
case Driver.write(ref, address, <<register_address, data::binary>>) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
when is_register_address(register_address) 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}
|
||||
{:ok, conn} <- write_register(conn, register_address, data) do
|
||||
{:ok, old_data, conn}
|
||||
end
|
||||
end
|
||||
|
||||
def swap_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
|
||||
end
|
||||
|
||||
defimpl Wafer.I2C, for: Wafer.Driver.CircuitsI2C do
|
||||
import Wafer.Guards
|
||||
alias Circuits.I2C, as: Driver
|
||||
|
||||
def read(%{ref: ref, address: address}, bytes, options \\ [])
|
||||
when is_reference(ref) and is_i2c_address(address) and is_byte_size(bytes) and
|
||||
is_list(options),
|
||||
do: Driver.read(ref, address, bytes, options)
|
||||
|
||||
def write(%{ref: ref, address: address} = conn, data, options \\ [])
|
||||
when is_reference(ref) and is_i2c_address(address) and is_binary(data) and is_list(options) do
|
||||
case Driver.write(ref, address, data, options) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def write_read(%{ref: ref, address: address} = conn, data, bytes, options \\ [])
|
||||
when is_reference(ref) and is_i2c_address(address) and is_binary(data) and
|
||||
is_byte_size(bytes) and is_list(options) do
|
||||
case Driver.write_read(ref, address, data, bytes, options) do
|
||||
{:ok, data} -> {:ok, data, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def detect_devices(%{ref: ref}) when is_reference(ref) do
|
||||
case Driver.detect_devices(ref) do
|
||||
devices when is_list(devices) -> {:ok, devices}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
52
lib/wafer/drivers/circuits_spi.ex
Normal file
52
lib/wafer/drivers/circuits_spi.ex
Normal file
|
@ -0,0 +1,52 @@
|
|||
defmodule Wafer.Driver.CircuitsSPI do
|
||||
defstruct ~w[bus ref]a
|
||||
@behaviour Wafer.Conn
|
||||
alias Circuits.SPI, as: Driver
|
||||
|
||||
@moduledoc """
|
||||
A connection to a chip via Circuits's SPI driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{bus: binary, ref: reference}
|
||||
|
||||
@type options :: [option | driver_option]
|
||||
@type option :: {:bus_name, binary}
|
||||
# These options are passed unchanged to the underlying driver.
|
||||
@type driver_option ::
|
||||
{:mode, 0..3}
|
||||
| {:bits_per_word, 0..16}
|
||||
| {:speed_hz, pos_integer}
|
||||
| {:delay_us, non_neg_integer}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the Circuits' SPI driver on the
|
||||
specified bus and address.
|
||||
"""
|
||||
@spec acquire(options) :: {:ok, t} | {:error, reason :: any}
|
||||
def acquire(opts) when is_list(opts) do
|
||||
with bus when is_binary(bus) <- Keyword.get(opts, :bus_name),
|
||||
{:ok, ref} when is_reference(ref) <- Driver.open(bus, Keyword.delete(opts, :bus_name)) do
|
||||
{:ok, %__MODULE__{bus: bus, ref: ref}}
|
||||
else
|
||||
:error -> {:error, "Circuits.SPI requires a `bus_name` option"}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Close the SPI bus connection.
|
||||
"""
|
||||
@spec release(t) :: :ok | {:error, reason :: any}
|
||||
def release(%__MODULE__{ref: ref}) when is_reference(ref), do: Driver.close(ref)
|
||||
end
|
||||
|
||||
defimpl Wafer.SPI, for: Wafer.Driver.CircuitsSPI do
|
||||
alias Circuits.SPI, as: Driver
|
||||
|
||||
def transfer(%{ref: ref} = conn, data) when is_reference(ref) and is_binary(data) do
|
||||
case Driver.transfer(ref, data) do
|
||||
{:ok, data} -> {:ok, data, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Wafer.Driver.ElixirAleGPIO do
|
||||
defmodule Wafer.Driver.ElixirALEGPIO do
|
||||
defstruct ~w[direction pid pin]a
|
||||
@behaviour Wafer.Conn
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
|
@ -11,7 +11,7 @@ defmodule Wafer.Driver.ElixirAleGPIO do
|
|||
@type t :: %__MODULE__{pid: pid}
|
||||
|
||||
@type options :: [option]
|
||||
@type option :: {:pin, non_neg_integer} | {:direction, GPIO.pin_direction()}
|
||||
@type option :: {:pin, non_neg_integer} | {:direction, GPIO.pin_direction()} | {:force, boolean}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to the specified GPIO pin using the ElixirALE GPIO driver.
|
||||
|
@ -23,10 +23,10 @@ defmodule Wafer.Driver.ElixirAleGPIO do
|
|||
"""
|
||||
@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),
|
||||
with pin when is_integer(pin) and pin >= 0 <- Keyword.get(opts, :pin),
|
||||
direction when direction in [:in, :out] <- Keyword.get(opts, :direction, :out),
|
||||
{:ok, pid} <- Driver.start_link(pin, direction, Keyword.drop(opts, ~w[pin direction]a)) do
|
||||
%__MODULE__{pid: pid}
|
||||
{:ok, %__MODULE__{pid: pid, pin: pin, direction: direction}}
|
||||
else
|
||||
:error -> {:error, "ElixirALE.GPIO requires a `pin` option."}
|
||||
{:error, reason} -> {:error, reason}
|
||||
|
@ -39,12 +39,12 @@ defmodule Wafer.Driver.ElixirAleGPIO do
|
|||
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)
|
||||
def release(%__MODULE__{pid: pid}), do: Driver.release(pid)
|
||||
end
|
||||
|
||||
defimpl Wafer.GPIOProto, for: Wafer.Driver.ElixirAleGPIO do
|
||||
defimpl Wafer.GPIO, for: Wafer.Driver.ElixirALEGPIO do
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
alias Wafer.Driver.ElixirAleGPIODispatcher
|
||||
alias Wafer.Driver.ElixirALEGPIODispatcher
|
||||
|
||||
def read(%{pid: pid} = _conn) do
|
||||
case Driver.read(pid) do
|
||||
|
@ -60,16 +60,13 @@ defimpl Wafer.GPIOProto, for: Wafer.Driver.ElixirAleGPIO do
|
|||
end
|
||||
end
|
||||
|
||||
def direction(_conn, _direction),
|
||||
do:
|
||||
{:error,
|
||||
"ElixirALE doesn't support direction changing. Restart the connection process with the new direction instead."}
|
||||
def direction(_conn, _direction), do: {:error, :not_supported}
|
||||
|
||||
def enable_interrupt(conn, pin_trigger),
|
||||
do: ElixirAleGPIODispatcher.enable(conn, pin_trigger)
|
||||
def enable_interrupt(conn, pin_condition),
|
||||
do: ElixirALEGPIODispatcher.enable(conn, pin_condition)
|
||||
|
||||
def disable_interrupt(conn, pin_trigger),
|
||||
do: ElixirAleGPIODispatcher.disable(conn, pin_trigger)
|
||||
def disable_interrupt(conn, pin_condition),
|
||||
do: ElixirALEGPIODispatcher.disable(conn, pin_condition)
|
||||
|
||||
def pull_mode(_conn, _pull_mode), do: {:error, :not_supported}
|
||||
end
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
defmodule Wafer.Driver.ElixirAleGPIODispatcher do
|
||||
defmodule Wafer.Driver.ElixirALEGPIODispatcher do
|
||||
use GenServer
|
||||
alias __MODULE__
|
||||
alias Wafer.{Conn, GPIO, InterruptRegistry}
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
alias Wafer.{Conn, GPIO, InterruptRegistry}
|
||||
|
||||
@allowed_triggers ~w[rising falling both]a
|
||||
|
||||
|
@ -13,21 +13,21 @@ defmodule Wafer.Driver.ElixirAleGPIODispatcher do
|
|||
|
||||
@doc false
|
||||
def start_link(opts),
|
||||
do: GenServer.start_link(__MODULE__, [opts], name: ElixirAleGPIODispatcher)
|
||||
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})
|
||||
@spec enable(Conn.t(), GPIO.pin_condition()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable(conn, pin_condition) when pin_condition in @allowed_triggers,
|
||||
do: GenServer.call(ElixirALEGPIODispatcher, {:enable, conn, pin_condition, self()})
|
||||
|
||||
@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})
|
||||
@spec disable(Conn.t(), GPIO.pin_condition()) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def disable(conn, pin_condition) when pin_condition in @allowed_triggers,
|
||||
do: GenServer.call(ElixirALEGPIODispatcher, {:disable, conn, pin_condition})
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
|
@ -35,16 +35,11 @@ defmodule Wafer.Driver.ElixirAleGPIODispatcher do
|
|||
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
|
||||
def handle_call({:enable, %{pin: pin, pid: pid} = conn, pin_condition, receiver}, _from, state)
|
||||
when pin_condition in @allowed_triggers do
|
||||
case Driver.set_int(pid, pin_condition) do
|
||||
:ok ->
|
||||
subscription = {conn, pin_trigger}
|
||||
|
||||
state =
|
||||
state
|
||||
|> Map.update(pin, MapSet.new([subscription]), &MapSet.put(&1, subscription))
|
||||
|
||||
subscribe(pin, pin_condition, conn, receiver)
|
||||
{:reply, {:ok, conn}, state}
|
||||
|
||||
{:error, reason} ->
|
||||
|
@ -52,40 +47,58 @@ defmodule Wafer.Driver.ElixirAleGPIODispatcher do
|
|||
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)
|
||||
|
||||
def handle_call({:disable, %{pin: pin} = conn, pin_condition}, _from, state)
|
||||
when pin_condition in @allowed_triggers do
|
||||
unsubscribe(pin, pin_condition, conn)
|
||||
{: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)
|
||||
def handle_info({:gpio_interrupt, pin, condition}, state)
|
||||
when condition in @allowed_triggers do
|
||||
Registry.dispatch(InterruptRegistry, {__MODULE__, pin, condition}, fn subs ->
|
||||
for {pid, conn} <- subs do
|
||||
send(pid, {:interrupt, conn, condition})
|
||||
end
|
||||
end)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp subscribe(pin, :rising, conn, receiver),
|
||||
do:
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :rising}, conn},
|
||||
receiver
|
||||
)
|
||||
|
||||
defp subscribe(pin, :falling, conn, receiver),
|
||||
do:
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :falling}, conn},
|
||||
receiver
|
||||
)
|
||||
|
||||
defp subscribe(pin, :both, conn, receiver) do
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :rising}, conn},
|
||||
receiver
|
||||
)
|
||||
|
||||
Registry.register_name(
|
||||
{InterruptRegistry, {__MODULE__, pin, :falling}, conn},
|
||||
receiver
|
||||
)
|
||||
end
|
||||
|
||||
defp unsubscribe(pin, :rising, conn),
|
||||
do: Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :rising}, conn)
|
||||
|
||||
defp unsubscribe(pin, :falling, conn),
|
||||
do: Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :falling}, conn)
|
||||
|
||||
defp unsubscribe(pin, :both, conn) do
|
||||
Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :rising}, conn)
|
||||
Registry.unregister_match(InterruptRegistry, {__MODULE__, pin, :falling}, conn)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
defmodule Wafer.Driver.ElixirAleI2C do
|
||||
defmodule Wafer.Driver.ElixirALEI2C do
|
||||
defstruct ~w[address bus pid]a
|
||||
@behaviour Wafer.Conn
|
||||
alias ElixirALE.I2C, as: Driver
|
||||
alias Wafer.Chip
|
||||
alias Wafer.I2C
|
||||
import Wafer.Guards
|
||||
|
||||
@moduledoc """
|
||||
A connection to a chip via ElixirALE's I2C driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{address: Chip.i2c_address(), bus: binary, pid: pid}
|
||||
@type t :: %__MODULE__{address: I2C.address(), bus: binary, pid: pid}
|
||||
|
||||
@type options :: [option]
|
||||
@type option :: {:bus_name, binary} | {:address, Chip.i2c_address()}
|
||||
@type option :: {:bus_name, binary} | {:address, I2C.address()}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the ElixirALE I2C driver on the
|
||||
|
@ -19,26 +20,34 @@ defmodule Wafer.Driver.ElixirAleI2C do
|
|||
"""
|
||||
@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
|
||||
with bus when is_binary(bus) <- Keyword.get(opts, :bus_name),
|
||||
address when is_i2c_address(address) <- Keyword.get(opts, :address),
|
||||
{:ok, pid} <- Driver.start_link(bus, address),
|
||||
devices when is_list(devices) <- Driver.detect_devices(pid),
|
||||
true <- Keyword.get(opts, :force, false) || Enum.member?(devices, 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}
|
||||
false ->
|
||||
{:error, "No device detected at address. Pass `force: true` to override."}
|
||||
|
||||
: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)
|
||||
def release(%__MODULE__{pid: pid}) when is_pid(pid), do: ElixirALE.I2C.release(pid)
|
||||
end
|
||||
|
||||
defimpl Wafer.Chip, for: Wafer.Driver.ElixirAleI2C do
|
||||
defimpl Wafer.Chip, for: Wafer.Driver.ElixirALEI2C do
|
||||
alias ElixirALE.I2C, as: Driver
|
||||
import Wafer.Guards
|
||||
|
||||
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
|
||||
when is_pid(pid) and is_register_address(register_address) and is_byte_size(bytes) do
|
||||
case Driver.write_read(pid, <<register_address>>, bytes) do
|
||||
data when is_binary(data) -> {:ok, data}
|
||||
{:error, reason} -> {:error, reason}
|
||||
|
@ -47,19 +56,59 @@ defimpl Wafer.Chip, for: Wafer.Driver.ElixirAleI2C do
|
|||
|
||||
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(%{pid: pid} = conn, register_address, data)
|
||||
when is_pid(pid) and is_register_address(register_address) and is_binary(data) do
|
||||
case Driver.write(pid, <<register_address, data::binary>>) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
when is_register_address(register_address) 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}
|
||||
{:ok, conn} <- write_register(conn, register_address, data) do
|
||||
{:ok, old_data, conn}
|
||||
end
|
||||
end
|
||||
|
||||
def swap_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
|
||||
end
|
||||
|
||||
defimpl Wafer.I2C, for: Wafer.Driver.ElixirALEI2C do
|
||||
import Wafer.Guards
|
||||
alias ElixirALE.I2C, as: Driver
|
||||
|
||||
def read(%{pid: pid}, bytes, options \\ [])
|
||||
when is_pid(pid) and is_byte_size(bytes) and is_list(options) do
|
||||
case Driver.read(pid, bytes, options) do
|
||||
data when is_binary(data) -> {:ok, data}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def write(%{pid: pid} = conn, data, options \\ [])
|
||||
when is_pid(pid) and is_binary(data) and is_list(options) do
|
||||
case Driver.write(pid, data, options) do
|
||||
:ok -> {:ok, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def write_read(%{pid: pid} = conn, data, bytes, options \\ [])
|
||||
when is_pid(pid) and is_binary(data) and is_byte_size(bytes) and is_list(options) do
|
||||
case Driver.write_read(pid, data, bytes, options) do
|
||||
data when is_binary(data) -> {:ok, data, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def detect_devices(%{pid: pid}) do
|
||||
case Driver.detect_devices(pid) do
|
||||
devices when is_list(devices) -> {:ok, devices}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
53
lib/wafer/drivers/elixir_ale_spi.ex
Normal file
53
lib/wafer/drivers/elixir_ale_spi.ex
Normal file
|
@ -0,0 +1,53 @@
|
|||
defmodule Wafer.Driver.ElixirALESPI do
|
||||
defstruct ~w[bus pid]a
|
||||
@behaviour Wafer.Conn
|
||||
alias ElixirALE.SPI, as: Driver
|
||||
|
||||
@moduledoc """
|
||||
A connection to a chip via ElixirALE's SPI driver.
|
||||
"""
|
||||
|
||||
@type t :: %__MODULE__{bus: binary, pid: pid}
|
||||
|
||||
@type options :: [option | driver_option]
|
||||
@type option :: {:bus_name, binary}
|
||||
# These options are passed unchanged to the underlying driver.
|
||||
@type driver_option ::
|
||||
{:mode, 0..3}
|
||||
| {:bits_per_word, 0..16}
|
||||
| {:speed_hz, pos_integer}
|
||||
| {:delay_us, non_neg_integer}
|
||||
|
||||
@doc """
|
||||
Acquire a connection to a peripheral using the ElixirALE' SPI driver on the
|
||||
specified bus and address.
|
||||
"""
|
||||
@spec acquire(options) :: {:ok, t} | {:error, reason :: any}
|
||||
def acquire(opts) when is_list(opts) do
|
||||
with bus when is_binary(bus) <- Keyword.get(opts, :bus_name),
|
||||
{:ok, pid} when is_pid(pid) <-
|
||||
Driver.start_link(bus, Keyword.delete(opts, :bus_name), []) do
|
||||
{:ok, %__MODULE__{bus: bus, pid: pid}}
|
||||
else
|
||||
:error -> {:error, "ElixirALE.SPI requires a `bus_name` option"}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Close the SPI bus connection.
|
||||
"""
|
||||
@spec release(t) :: :ok | {:error, reason :: any}
|
||||
def release(%__MODULE__{pid: pid}) when is_pid(pid), do: Driver.release(pid)
|
||||
end
|
||||
|
||||
defimpl Wafer.SPI, for: Wafer.Driver.ElixirALESPI do
|
||||
alias ElixirALE.SPI, as: Driver
|
||||
|
||||
def transfer(%{pid: pid} = conn, data) when is_pid(pid) and is_binary(data) do
|
||||
case Driver.transfer(pid, data) do
|
||||
data when is_binary(data) -> {:ok, data, conn}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
defmodule Wafer.GPIO do
|
||||
alias Wafer.{Conn, GPIOProto, InterruptRegistry}
|
||||
defprotocol Wafer.GPIO do
|
||||
alias Wafer.Conn
|
||||
|
||||
@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_condition :: :none | :rising | :falling | :both
|
||||
@type pin_value :: 0 | 1
|
||||
@type pull_mode :: :not_set | :none | :pull_up | :pull_down
|
||||
|
||||
|
@ -17,38 +17,42 @@ defmodule Wafer.GPIO do
|
|||
Read the current pin value.
|
||||
"""
|
||||
@spec read(Conn.t()) :: {:ok, pin_value, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate read(conn), to: GPIOProto
|
||||
def read(conn)
|
||||
|
||||
@doc """
|
||||
Set the pin value.
|
||||
"""
|
||||
@spec write(Conn.t(), pin_value) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate write(conn, pin_value), to: GPIOProto
|
||||
def write(conn, pin_value)
|
||||
|
||||
@doc """
|
||||
Set the pin direction.
|
||||
"""
|
||||
@spec direction(Conn.t(), pin_direction) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
defdelegate direction(conn, pin_direction), to: GPIOProto
|
||||
def direction(conn, pin_direction)
|
||||
|
||||
@doc """
|
||||
Enable an interrupt for this pin.
|
||||
Enable an interrupt for this connection and trigger.
|
||||
|
||||
Interrupts will be sent to the calling process as messages in the form of
|
||||
`{:interrupt, Conn.t(), pin_value}`.
|
||||
`{:interrupt, Conn.t(), pin_condition}`.
|
||||
|
||||
## 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}`.
|
||||
`Wafer` starts it's own `Registry` named `Wafer.InterruptRegistry` which you
|
||||
can use to publish your interrupts to using the above format. The registry
|
||||
key is set as follows: `{PublishingModule, pin, pin_condition}`. You can see
|
||||
examples in the `CircuitsGPIODispatcher` and `ElixirALEGPIODispatcher`
|
||||
modules.
|
||||
"""
|
||||
@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
|
||||
@spec enable_interrupt(Conn.t(), pin_condition) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def enable_interrupt(conn, pin_condition)
|
||||
|
||||
@doc """
|
||||
Disables interrupts for this connection and trigger.
|
||||
"""
|
||||
@spec disable_interrupt(Conn.t(), pin_condition) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||
def disable_interrupt(conn, pin_condition)
|
||||
|
||||
@doc """
|
||||
Set the pull mode for this pin.
|
||||
|
@ -58,5 +62,5 @@ defmodule Wafer.GPIO do
|
|||
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
|
||||
def pull_mode(conn, pull_mode)
|
||||
end
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
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
|
29
lib/wafer/guards.ex
Normal file
29
lib/wafer/guards.ex
Normal file
|
@ -0,0 +1,29 @@
|
|||
defmodule Wafer.Guards do
|
||||
@moduledoc """
|
||||
Handy guards which you can use in your code to assert correct values.
|
||||
"""
|
||||
|
||||
@doc "A positive integer"
|
||||
defguard is_pin_number(pin) when is_integer(pin) and pin >= 0
|
||||
|
||||
@doc "Either `:in` or `:out`"
|
||||
defguard is_pin_direction(direction) when direction in ~w[in out]a
|
||||
|
||||
@doc "One of `:none`, `:rising`, `:falling` or `:both`"
|
||||
defguard is_pin_condition(condition) when condition in ~w[none rising falling both]a
|
||||
|
||||
@doc "Either `0` or `1`"
|
||||
defguard is_pin_value(value) when value in [0, 1]
|
||||
|
||||
@doc "One of `:not_set`, `:none`, `:pull_up` or `:pull_down`"
|
||||
defguard is_pin_pull_mode(mode) when mode in ~w[not_set none pull_up pull_down]a
|
||||
|
||||
@doc "An integer between `0` and `0x7F`"
|
||||
defguard is_i2c_address(address) when is_integer(address) and address >= 0 and address <= 0x7F
|
||||
|
||||
@doc "A positive integer"
|
||||
defguard is_register_address(address) when is_integer(address) and address >= 0
|
||||
|
||||
@doc "A positive integer"
|
||||
defguard is_byte_size(bytes) when is_integer(bytes) and bytes >= 0
|
||||
end
|
44
lib/wafer/i2c.ex
Normal file
44
lib/wafer/i2c.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defprotocol Wafer.I2C do
|
||||
alias Wafer.Conn
|
||||
|
||||
@moduledoc """
|
||||
A protocol for interacting with I2C devices directly. Most of the time you'll
|
||||
want to use the `Chip` protocol for working with registers, but this is
|
||||
provided for consistency's sake.
|
||||
"""
|
||||
|
||||
@type address :: 0..0x7F
|
||||
|
||||
# See the documentation to the underlying driver for information about which options are supported.
|
||||
@type options :: [option]
|
||||
@type option :: any
|
||||
|
||||
@type data :: binary
|
||||
|
||||
@doc """
|
||||
Initiate a read transaction to the connection's I2C device.
|
||||
"""
|
||||
@spec read(Conn.t(), non_neg_integer, options) ::
|
||||
{:ok, data} | {:error, reason :: any}
|
||||
def read(conn, bytes_to_read, options \\ [])
|
||||
|
||||
@doc """
|
||||
Write `data` to the connection's I2C device.
|
||||
"""
|
||||
@spec write(Conn.t(), data, options) ::
|
||||
{:ok, Conn.t()} | {:error, reason :: any}
|
||||
def write(conn, data, options \\ [])
|
||||
|
||||
@doc """
|
||||
Write data to an I2C device and then immediately issue a read.
|
||||
"""
|
||||
@spec write_read(Conn.t(), data, non_neg_integer, options) ::
|
||||
{:ok, data, Conn.t()} | {:error, reason :: any}
|
||||
def write_read(conn, data, bytes_to_read, options \\ [])
|
||||
|
||||
@doc """
|
||||
Detect the devices adjacent to the connection's device on the same I2C bus.
|
||||
"""
|
||||
@spec detect_devices(Conn.t()) :: {:ok, [address]}
|
||||
def detect_devices(conn)
|
||||
end
|
|
@ -1,3 +1,4 @@
|
|||
# credo:disable-for-this-file
|
||||
defmodule Wafer.Registers do
|
||||
@moduledoc """
|
||||
This module provides helpful macros for specifying the registers used to
|
||||
|
|
19
lib/wafer/spi.ex
Normal file
19
lib/wafer/spi.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defprotocol Wafer.SPI do
|
||||
alias Wafer.Conn
|
||||
|
||||
@moduledoc """
|
||||
A (very simple) protocol for interacting with SPI connected devices.
|
||||
"""
|
||||
|
||||
@type data :: binary
|
||||
|
||||
@doc """
|
||||
Perform an SPI transfer.
|
||||
|
||||
SPI transfers are synchronous, so `data` should be a binary of bytes to send
|
||||
to the device, and you will receive back a binary of the same length
|
||||
containing the data received from the device.
|
||||
"""
|
||||
@spec transfer(Conn.t(), data) :: {:ok, data, Conn.t()} | {:error, reason :: any}
|
||||
def transfer(conn, data)
|
||||
end
|
8
mix.exs
8
mix.exs
|
@ -1,5 +1,6 @@
|
|||
defmodule Wafer.MixProject do
|
||||
use Mix.Project
|
||||
@moduledoc false
|
||||
|
||||
def project do
|
||||
[
|
||||
|
@ -25,9 +26,10 @@ defmodule Wafer.MixProject do
|
|||
[
|
||||
{: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}
|
||||
{:elixir_ale, "~> 1.2", only: :dev, optional: true},
|
||||
{:circuits_i2c, "~> 0.3", only: :dev, optional: true},
|
||||
{:circuits_gpio, "~> 0.4", only: :dev, optional: true},
|
||||
{:circuits_spi, "~> 0.1", only: :dev, optional: true}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -2,6 +2,7 @@
|
|||
"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"},
|
||||
"circuits_spi": {:hex, :circuits_spi, "0.1.4", "b64161e0a25837bdb3301fbc5754d52278a916b7a549065fba4dc107395a930e", [: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"},
|
||||
|
|
156
test/drivers/circuits_gpio_dispatcher_test.exs
Normal file
156
test/drivers/circuits_gpio_dispatcher_test.exs
Normal file
|
@ -0,0 +1,156 @@
|
|||
defmodule WaferDriverCircuitsGPIODispatcherTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Circuits.GPIO, as: Driver
|
||||
alias Wafer.Driver.CircuitsGPIODispatcher, as: Dispatcher
|
||||
alias Wafer.InterruptRegistry
|
||||
import Mimic
|
||||
@moduledoc false
|
||||
|
||||
describe "handle_call/3" do
|
||||
test "enabling rising interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_interrupts, 1, fn ref, trigger ->
|
||||
assert ref == conn.ref
|
||||
assert trigger == :rising
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:enable, conn, :rising, self()}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == [{self(), conn}]
|
||||
end
|
||||
|
||||
test "enabling falling interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_interrupts, 1, fn ref, trigger ->
|
||||
assert ref == conn.ref
|
||||
assert trigger == :falling
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:enable, conn, :falling, self()}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == [
|
||||
{self(), conn}
|
||||
]
|
||||
end
|
||||
|
||||
test "enabling both interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_interrupts, 1, fn ref, trigger ->
|
||||
assert ref == conn.ref
|
||||
assert trigger == :both
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:enable, conn, :both, self()}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == [
|
||||
{self(), conn}
|
||||
]
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == [
|
||||
{self(), conn}
|
||||
]
|
||||
end
|
||||
|
||||
test "disabling rising interrupts" do
|
||||
conn = conn()
|
||||
Dispatcher.handle_call({:enable, conn, :rising, self()}, nil, state())
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:disable, conn, :rising}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == []
|
||||
end
|
||||
|
||||
test "disabling falling interrupts" do
|
||||
conn = conn()
|
||||
Dispatcher.handle_call({:enable, conn, :falling, self()}, nil, state())
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:disable, conn, :falling}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == []
|
||||
end
|
||||
|
||||
test "disabling both interrupts" do
|
||||
conn = conn()
|
||||
Dispatcher.handle_call({:enable, conn, :both, self()}, nil, state())
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:disable, conn, :both}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == []
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info/2" do
|
||||
test "publishing interrupts when the value was previously unknown" do
|
||||
{:reply, {:ok, conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state())
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:circuits_gpio, 1, :ts, 1}, state)
|
||||
|
||||
assert_received {:interrupt, ^conn, :rising}
|
||||
end
|
||||
|
||||
test "publishing interrupts when the value rises" do
|
||||
state = state(values: %{1 => 0})
|
||||
|
||||
{:reply, {:ok, conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state)
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:circuits_gpio, 1, :ts, 1}, state)
|
||||
|
||||
assert_received {:interrupt, ^conn, :rising}
|
||||
end
|
||||
|
||||
test "publishing interrupts when the value falls" do
|
||||
state = state(values: %{1 => 1})
|
||||
|
||||
{:reply, {:ok, conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state)
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:circuits_gpio, 1, :ts, 0}, state)
|
||||
|
||||
assert_received {:interrupt, ^conn, :falling}
|
||||
end
|
||||
|
||||
test "ignoring interrupts when the value stays high" do
|
||||
state = state(values: %{1 => 1})
|
||||
|
||||
{:reply, {:ok, _conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state)
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:circuits_gpio, 1, :ts, 1}, state)
|
||||
|
||||
refute_received {:interrupt, _conn, _condition}
|
||||
end
|
||||
|
||||
test "ignoring interrupts when the value stays low" do
|
||||
state = state(values: %{1 => 0})
|
||||
|
||||
{:reply, {:ok, _conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state)
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:circuits_gpio, 1, :ts, 0}, state)
|
||||
|
||||
refute_received {:interrupt, _conn, _condition}
|
||||
end
|
||||
end
|
||||
|
||||
defp conn(opts \\ []), do: Enum.into(opts, %{pin: pin(), ref: :erlang.make_ref()})
|
||||
defp state(opts \\ []), do: Enum.into(opts, %{values: %{}})
|
||||
defp pin, do: 1
|
||||
end
|
136
test/drivers/circuits_gpio_test.exs
Normal file
136
test/drivers/circuits_gpio_test.exs
Normal file
|
@ -0,0 +1,136 @@
|
|||
defmodule WaferDriverCircuitsGPIOTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias Circuits.GPIO, as: Driver
|
||||
alias Wafer.Driver.CircuitsGPIO, as: Subject
|
||||
alias Wafer.Driver.CircuitsGPIODispatcher, as: Dispatcher
|
||||
alias Wafer.GPIO, as: GPIO
|
||||
@moduledoc false
|
||||
|
||||
describe "acquire/1" do
|
||||
test "opens the pin and creates the conn" do
|
||||
Driver
|
||||
|> expect(:open, 1, fn pin, direction, opts ->
|
||||
assert pin == 1
|
||||
assert direction == :out
|
||||
assert opts == []
|
||||
{:ok, :erlang.make_ref()}
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = Subject.acquire(pin: 1, direction: :out)
|
||||
end
|
||||
end
|
||||
|
||||
describe "release/1" do
|
||||
test "closes the pin" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:close, 1, fn ref ->
|
||||
assert ref == conn.ref
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = Subject.release(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.read/1" do
|
||||
test "can read the pin value" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:read, 1, fn ref ->
|
||||
assert ref == conn.ref
|
||||
0
|
||||
end)
|
||||
|
||||
assert {:ok, 0} = GPIO.read(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.write/2" do
|
||||
test "can set the pin value" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn ref, value ->
|
||||
assert ref == conn.ref
|
||||
assert value == 1
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = GPIO.write(conn, 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.direction/2" do
|
||||
test "when the direction isn't changing" do
|
||||
Driver
|
||||
|> reject(:set_direction, 2)
|
||||
|
||||
assert {:ok, %Subject{} = conn} = Subject.acquire(pin: 1, direction: :out)
|
||||
assert {:ok, %Subject{}} = GPIO.direction(conn, :out)
|
||||
end
|
||||
|
||||
test "when the direction is changing" do
|
||||
assert {:ok, %Subject{} = conn} = Subject.acquire(pin: 1, direction: :out)
|
||||
|
||||
Driver
|
||||
|> expect(:set_direction, 1, fn ref, direction ->
|
||||
assert ref == conn.ref
|
||||
assert direction == :in
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = GPIO.direction(conn, :in)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.enable_interrupt/2" do
|
||||
test "subscribes the conn to interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Dispatcher
|
||||
|> expect(:enable, 1, fn conn1, trigger ->
|
||||
assert conn1 == conn
|
||||
assert trigger == :rising
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = GPIO.enable_interrupt(conn, :rising)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.disable_interrupt/2" do
|
||||
test "unsubscribes the conn from interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Dispatcher
|
||||
|> expect(:disable, 1, fn conn1, trigger ->
|
||||
assert conn1 == conn
|
||||
assert trigger == :rising
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = GPIO.disable_interrupt(conn, :rising)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.pull_mode/2" do
|
||||
test "sets the specified pull mode on the connection" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_pull_mode, 1, fn ref, mode ->
|
||||
assert ref == conn.ref
|
||||
assert mode == :pull_up
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = GPIO.pull_mode(conn, :pull_up)
|
||||
end
|
||||
end
|
||||
|
||||
defp conn, do: %Subject{ref: :erlang.make_ref(), pin: 1, direction: :out}
|
||||
end
|
207
test/drivers/circuits_i2c_test.exs
Normal file
207
test/drivers/circuits_i2c_test.exs
Normal file
|
@ -0,0 +1,207 @@
|
|||
defmodule WaferCircuitsI2CTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias Circuits.I2C, as: Driver
|
||||
alias Wafer.Chip
|
||||
alias Wafer.Driver.CircuitsI2C, as: Subject
|
||||
alias Wafer.I2C
|
||||
@moduledoc false
|
||||
|
||||
describe "acquire/1" do
|
||||
test "opens the bus and verifies that the device is present" do
|
||||
busref = :erlang.make_ref()
|
||||
busname = "i2c-1"
|
||||
address = 0x13
|
||||
|
||||
Driver
|
||||
|> expect(:open, 1, fn bus ->
|
||||
assert bus == busname
|
||||
{:ok, busref}
|
||||
end)
|
||||
|> expect(:detect_devices, 1, fn ref ->
|
||||
assert busref == ref
|
||||
[address]
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{} = conn} = Subject.acquire(bus_name: busname, address: address)
|
||||
end
|
||||
|
||||
test "when the device is not present on the bus" do
|
||||
busref = :erlang.make_ref()
|
||||
busname = "i2c-1"
|
||||
address = 0x13
|
||||
|
||||
Driver
|
||||
|> expect(:open, 1, fn bus ->
|
||||
assert bus == busname
|
||||
{:ok, busref}
|
||||
end)
|
||||
|> expect(:detect_devices, 1, fn ref ->
|
||||
assert busref == ref
|
||||
[]
|
||||
end)
|
||||
|
||||
assert {:error, _reason} = Subject.acquire(bus_name: busname, address: address)
|
||||
end
|
||||
|
||||
test "when the device is not present on the bus but an override is forced" do
|
||||
busref = :erlang.make_ref()
|
||||
busname = "i2c-1"
|
||||
address = 0x13
|
||||
|
||||
Driver
|
||||
|> expect(:open, 1, fn bus ->
|
||||
assert bus == busname
|
||||
{:ok, busref}
|
||||
end)
|
||||
|> expect(:detect_devices, 1, fn ref ->
|
||||
assert busref == ref
|
||||
[]
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{} = conn} =
|
||||
Subject.acquire(bus_name: busname, address: address, force: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "release/1" do
|
||||
test "closes the bus connection" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:close, 1, fn ref ->
|
||||
assert ref == conn.ref
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = Subject.release(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Chip.read_register/3" do
|
||||
test "reads from the device's register" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write_read, 1, fn ref, addr, data, bytes ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert data == <<0>>
|
||||
assert bytes == 2
|
||||
{:ok, <<0, 0>>}
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>} = Chip.read_register(conn, 0, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Chip.write_register/3" do
|
||||
test "writes to the device's register" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn ref, addr, data ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert data == <<1, 2, 3>>
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = Chip.write_register(conn, 1, <<2, 3>>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Chip.swap_register/3" do
|
||||
test "swaps the device's register value for a new value, returning the old value" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write_read, 1, fn ref, addr, data, bytes ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert data == <<0>>
|
||||
assert bytes == 2
|
||||
{:ok, <<0, 0>>}
|
||||
end)
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn ref, addr, data ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert data == <<0, 1, 1>>
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>, %Subject{}} = Chip.swap_register(conn, 0, <<1, 1>>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.read/2" do
|
||||
test "reads from the device" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:read, 1, fn ref, addr, bytes, opts ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert bytes == 2
|
||||
assert opts == []
|
||||
{:ok, <<0, 0>>}
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>} = I2C.read(conn, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.write/2" do
|
||||
test "it writes to the device" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn ref, addr, data, opts ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert data == <<0, 0>>
|
||||
assert opts == []
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = I2C.write(conn, <<0, 0>>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.write_read/3" do
|
||||
test "it writes to then reads from the device" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write_read, 1, fn ref, addr, data, bytes, opts ->
|
||||
assert ref == conn.ref
|
||||
assert addr == conn.address
|
||||
assert data == <<1>>
|
||||
assert bytes == 2
|
||||
assert opts == []
|
||||
|
||||
{:ok, <<0, 0>>}
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>, %Subject{}} = I2C.write_read(conn, <<1>>, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.detect_devices/1" do
|
||||
test "it detects devices" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:detect_devices, 1, fn ref ->
|
||||
assert conn.ref == ref
|
||||
[conn.address]
|
||||
end)
|
||||
|
||||
assert {:ok, [0x13]} = I2C.detect_devices(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp conn, do: %Subject{ref: :erlang.make_ref(), bus: "i2c-1", address: 0x13}
|
||||
end
|
52
test/drivers/circuits_spi_test.exs
Normal file
52
test/drivers/circuits_spi_test.exs
Normal file
|
@ -0,0 +1,52 @@
|
|||
defmodule WaferCircuitsSPITest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias Circuits.SPI, as: Driver
|
||||
alias Wafer.Driver.CircuitsSPI, as: Subject
|
||||
alias Wafer.SPI
|
||||
@moduledoc false
|
||||
|
||||
describe "acquire/1" do
|
||||
test "opens the bus" do
|
||||
Driver
|
||||
|> expect(:open, 1, fn bus, opts ->
|
||||
assert bus == "spidev0.0"
|
||||
assert opts == []
|
||||
{:ok, :erlang.make_ref()}
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = Subject.acquire(bus_name: "spidev0.0")
|
||||
end
|
||||
end
|
||||
|
||||
describe "release/1" do
|
||||
test "closes the bus connection" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:close, 1, fn ref ->
|
||||
assert ref == conn.ref
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = Subject.release(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "SPI.transfer/2" do
|
||||
test "transfers data to and from the bus" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:transfer, 1, fn ref, data ->
|
||||
assert ref == conn.ref
|
||||
assert data == <<0, 0>>
|
||||
{:ok, <<1, 1>>}
|
||||
end)
|
||||
|
||||
assert {:ok, <<1, 1>>, %Subject{}} = SPI.transfer(conn, <<0, 0>>)
|
||||
end
|
||||
end
|
||||
|
||||
defp conn, do: %Subject{ref: :erlang.make_ref(), bus: "spidev0.0"}
|
||||
end
|
53
test/drivers/elixie_ale_spi_test.exs
Normal file
53
test/drivers/elixie_ale_spi_test.exs
Normal file
|
@ -0,0 +1,53 @@
|
|||
defmodule WaferElixirALESPITest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias ElixirALE.SPI, as: Driver
|
||||
alias Wafer.Driver.ElixirALESPI, as: Subject
|
||||
alias Wafer.SPI
|
||||
@moduledoc false
|
||||
|
||||
describe "acquire/1" do
|
||||
test "opens the bus" do
|
||||
Driver
|
||||
|> expect(:start_link, 1, fn bus, spi_opts, opts ->
|
||||
assert bus == "spidev0.0"
|
||||
assert spi_opts == []
|
||||
assert opts == []
|
||||
{:ok, self()}
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = Subject.acquire(bus_name: "spidev0.0")
|
||||
end
|
||||
end
|
||||
|
||||
describe "release/1" do
|
||||
test "closes the bus connection" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:release, 1, fn pid ->
|
||||
assert pid == conn.pid
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = Subject.release(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "SPI.transfer/2" do
|
||||
test "transfers data to and from the bus" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:transfer, 1, fn pid, data ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<0, 0>>
|
||||
<<1, 1>>
|
||||
end)
|
||||
|
||||
assert {:ok, <<1, 1>>, %Subject{}} = SPI.transfer(conn, <<0, 0>>)
|
||||
end
|
||||
end
|
||||
|
||||
defp conn, do: %Subject{pid: self(), bus: "spidev0.0"}
|
||||
end
|
121
test/drivers/elixir_ale_gpio_dispatcher_test.exs
Normal file
121
test/drivers/elixir_ale_gpio_dispatcher_test.exs
Normal file
|
@ -0,0 +1,121 @@
|
|||
defmodule WaferDriverElixirALEGPIODispatcherTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
alias Wafer.Driver.ElixirALEGPIODispatcher, as: Dispatcher
|
||||
alias Wafer.InterruptRegistry
|
||||
import Mimic
|
||||
@moduledoc false
|
||||
|
||||
describe "handle_call/3" do
|
||||
test "enabling rising interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_int, 1, fn pid, trigger ->
|
||||
assert pid == conn.pid
|
||||
assert trigger == :rising
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:enable, conn, :rising, self()}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == [{self(), conn}]
|
||||
end
|
||||
|
||||
test "enabling falling interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_int, 1, fn pid, trigger ->
|
||||
assert pid == conn.pid
|
||||
assert trigger == :falling
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:enable, conn, :falling, self()}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == [
|
||||
{self(), conn}
|
||||
]
|
||||
end
|
||||
|
||||
test "enabling both interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:set_int, 1, fn pid, trigger ->
|
||||
assert pid == conn.pid
|
||||
assert trigger == :both
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:enable, conn, :both, self()}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == [
|
||||
{self(), conn}
|
||||
]
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == [
|
||||
{self(), conn}
|
||||
]
|
||||
end
|
||||
|
||||
test "disabling rising interrupts" do
|
||||
conn = conn()
|
||||
Dispatcher.handle_call({:enable, conn, :rising, self()}, nil, state())
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:disable, conn, :rising}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == []
|
||||
end
|
||||
|
||||
test "disabling falling interrupts" do
|
||||
conn = conn()
|
||||
Dispatcher.handle_call({:enable, conn, :falling, self()}, nil, state())
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:disable, conn, :falling}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == []
|
||||
end
|
||||
|
||||
test "disabling both interrupts" do
|
||||
conn = conn()
|
||||
Dispatcher.handle_call({:enable, conn, :both, self()}, nil, state())
|
||||
|
||||
assert {:reply, {:ok, conn}, _state} =
|
||||
Dispatcher.handle_call({:disable, conn, :both}, nil, state())
|
||||
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :rising}, conn) == []
|
||||
assert Registry.match(InterruptRegistry, {Dispatcher, 1, :falling}, conn) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "handle_info/2" do
|
||||
test "publishing rising interrupts" do
|
||||
{:reply, {:ok, conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state())
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:gpio_interrupt, 1, :rising}, state)
|
||||
|
||||
assert_received {:interrupt, ^conn, :rising}
|
||||
end
|
||||
|
||||
test "publishing falling interrupts" do
|
||||
{:reply, {:ok, conn}, state} =
|
||||
Dispatcher.handle_call({:enable, conn(), :both, self()}, nil, state())
|
||||
|
||||
{:noreply, _state} = Dispatcher.handle_info({:gpio_interrupt, 1, :falling}, state)
|
||||
|
||||
assert_received {:interrupt, ^conn, :falling}
|
||||
end
|
||||
end
|
||||
|
||||
defp conn(opts \\ []), do: Enum.into(opts, %{pin: pin(), pid: self()})
|
||||
defp state(opts \\ []), do: Enum.into(opts, %{})
|
||||
defp pin, do: 1
|
||||
end
|
110
test/drivers/elixir_ale_gpio_test.exs
Normal file
110
test/drivers/elixir_ale_gpio_test.exs
Normal file
|
@ -0,0 +1,110 @@
|
|||
defmodule WaferDriverElixirALEGPIOTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias ElixirALE.GPIO, as: Driver
|
||||
alias Wafer.Driver.ElixirALEGPIO, as: Subject
|
||||
alias Wafer.Driver.ElixirALEGPIODispatcher, as: Dispatcher
|
||||
alias Wafer.GPIO, as: GPIO
|
||||
@moduledoc false
|
||||
|
||||
describe "acquire/1" do
|
||||
test "opens the pin and creates the conn" do
|
||||
Driver
|
||||
|> expect(:start_link, 1, fn pin, direction, opts ->
|
||||
assert pin == 1
|
||||
assert direction == :out
|
||||
assert opts == []
|
||||
{:ok, self()}
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = Subject.acquire(pin: 1, direction: :out)
|
||||
end
|
||||
end
|
||||
|
||||
describe "release/1" do
|
||||
test "closes the pin" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:release, 1, fn pid ->
|
||||
assert pid == conn.pid
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = Subject.release(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.read/1" do
|
||||
test "can read the pin value" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:read, 1, fn pid ->
|
||||
assert pid == conn.pid
|
||||
0
|
||||
end)
|
||||
|
||||
assert {:ok, 0} = GPIO.read(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.write/2" do
|
||||
test "can set the pin value" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn pid, value ->
|
||||
assert pid == conn.pid
|
||||
assert value == 1
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = GPIO.write(conn, 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.direction/2" do
|
||||
test "is not supported" do
|
||||
assert {:error, :not_supported} = GPIO.direction(conn(), :in)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.enable_interrupt/2" do
|
||||
test "subscribes the conn to interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Dispatcher
|
||||
|> expect(:enable, 1, fn conn1, trigger ->
|
||||
assert conn1 == conn
|
||||
assert trigger == :rising
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = GPIO.enable_interrupt(conn, :rising)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.disable_interrupt/2" do
|
||||
test "unsubscribes the conn from interrupts" do
|
||||
conn = conn()
|
||||
|
||||
Dispatcher
|
||||
|> expect(:disable, 1, fn conn1, trigger ->
|
||||
assert conn1 == conn
|
||||
assert trigger == :rising
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = GPIO.disable_interrupt(conn, :rising)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GPIO.pull_mode/2" do
|
||||
test "is not supported" do
|
||||
assert {:error, :not_supported} = GPIO.pull_mode(conn(), :pull_up)
|
||||
end
|
||||
end
|
||||
|
||||
defp conn, do: %Subject{pid: self(), pin: 1, direction: :out}
|
||||
end
|
203
test/drivers/elixir_ale_i2c_test.exs
Normal file
203
test/drivers/elixir_ale_i2c_test.exs
Normal file
|
@ -0,0 +1,203 @@
|
|||
defmodule WaferElixirALEI2CTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Mimic
|
||||
alias ElixirALE.I2C, as: Driver
|
||||
alias Wafer.Chip
|
||||
alias Wafer.Driver.ElixirALEI2C, as: Subject
|
||||
alias Wafer.I2C
|
||||
@moduledoc false
|
||||
|
||||
describe "acquire/1" do
|
||||
test "opens the bus and verifies that the device is present" do
|
||||
buspid = self()
|
||||
busname = "i2c-1"
|
||||
address = 0x13
|
||||
|
||||
Driver
|
||||
|> expect(:start_link, 1, fn bus, address ->
|
||||
assert bus == busname
|
||||
assert address == 0x13
|
||||
{:ok, buspid}
|
||||
end)
|
||||
|> expect(:detect_devices, 1, fn pid ->
|
||||
assert buspid == pid
|
||||
[address]
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{} = conn} = Subject.acquire(bus_name: busname, address: address)
|
||||
end
|
||||
|
||||
test "when the device is not present on the bus" do
|
||||
buspid = self()
|
||||
busname = "i2c-1"
|
||||
address = 0x13
|
||||
|
||||
Driver
|
||||
|> expect(:start_link, 1, fn bus, address ->
|
||||
assert bus == busname
|
||||
assert address == 0x13
|
||||
{:ok, buspid}
|
||||
end)
|
||||
|> expect(:detect_devices, 1, fn pid ->
|
||||
assert buspid == pid
|
||||
[]
|
||||
end)
|
||||
|
||||
assert {:error, _reason} = Subject.acquire(bus_name: busname, address: address)
|
||||
end
|
||||
|
||||
test "when the device is not present on the bus but an override is forced" do
|
||||
buspid = self()
|
||||
busname = "i2c-1"
|
||||
address = 0x13
|
||||
|
||||
Driver
|
||||
|> expect(:start_link, 1, fn bus, address ->
|
||||
assert bus == busname
|
||||
assert address == 0x13
|
||||
{:ok, buspid}
|
||||
end)
|
||||
|> expect(:detect_devices, 1, fn pid ->
|
||||
assert buspid == pid
|
||||
[]
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{} = conn} =
|
||||
Subject.acquire(bus_name: busname, address: address, force: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "release/1" do
|
||||
test "closes the bus connection" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:release, 1, fn pid ->
|
||||
assert pid == conn.pid
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert :ok = Subject.release(conn)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Chip.read_register/3" do
|
||||
test "reads from the device's register" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write_read, 1, fn pid, data, bytes ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<0>>
|
||||
assert bytes == 2
|
||||
<<0, 0>>
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>} = Chip.read_register(conn, 0, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Chip.write_register/3" do
|
||||
test "writes to the device's register" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn pid, data ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<1, 2, 3>>
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = Chip.write_register(conn, 1, <<2, 3>>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "Chip.swap_register/3" do
|
||||
test "swaps the device's register value for a new value, returning the old value" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write_read, 1, fn pid, data, bytes ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<0>>
|
||||
assert bytes == 2
|
||||
<<0, 0>>
|
||||
end)
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn pid, data ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<0, 1, 1>>
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>, %Subject{}} = Chip.swap_register(conn, 0, <<1, 1>>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.read/2" do
|
||||
test "reads from the device" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:read, 1, fn pid, bytes, opts ->
|
||||
assert pid == conn.pid
|
||||
assert bytes == 2
|
||||
assert opts == []
|
||||
<<0, 0>>
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>} = I2C.read(conn, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.write/2" do
|
||||
test "it writes to the device" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write, 1, fn pid, data, opts ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<0, 0>>
|
||||
assert opts == []
|
||||
:ok
|
||||
end)
|
||||
|
||||
assert {:ok, %Subject{}} = I2C.write(conn, <<0, 0>>)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.write_read/3" do
|
||||
test "it writes to then reads from the device" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:write_read, 1, fn pid, data, bytes, opts ->
|
||||
assert pid == conn.pid
|
||||
assert data == <<1>>
|
||||
assert bytes == 2
|
||||
assert opts == []
|
||||
|
||||
<<0, 0>>
|
||||
end)
|
||||
|
||||
assert {:ok, <<0, 0>>, %Subject{}} = I2C.write_read(conn, <<1>>, 2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "I2C.detect_devices/1" do
|
||||
test "it detects devices" do
|
||||
conn = conn()
|
||||
|
||||
Driver
|
||||
|> expect(:detect_devices, 1, fn pid ->
|
||||
assert conn.pid == pid
|
||||
[conn.address]
|
||||
end)
|
||||
|
||||
assert {:ok, [0x13]} = I2C.detect_devices(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp conn, do: %Subject{pid: self(), bus: "i2c-1", address: 0x13}
|
||||
end
|
|
@ -137,7 +137,7 @@ defmodule WaferRegistersTest do
|
|||
end
|
||||
end
|
||||
|
||||
defp test_mod() do
|
||||
defp test_mod do
|
||||
mod = TestUtils.random_module_name()
|
||||
|
||||
defmodule mod do
|
||||
|
|
21
test/support/circuits_gpio.ex
Normal file
21
test/support/circuits_gpio.ex
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule Circuits.GPIO do
|
||||
import Wafer.Guards
|
||||
@moduledoc false
|
||||
|
||||
def set_interrupts(ref, pin_condition, opts \\ [])
|
||||
when is_reference(ref) and is_pin_condition(pin_condition) and is_list(opts),
|
||||
do: :ok
|
||||
|
||||
def open(pin_number, pin_direction, options \\ [])
|
||||
when is_pin_number(pin_number) and is_pin_direction(pin_direction) and is_list(options),
|
||||
do: {:ok, :erlang.make_ref()}
|
||||
|
||||
def close(ref) when is_reference(ref), do: :ok
|
||||
def read(ref) when is_reference(ref), do: 0
|
||||
def write(ref, value) when is_reference(ref) and is_pin_value(value), do: :ok
|
||||
|
||||
def set_direction(ref, direction) when is_reference(ref) and is_pin_direction(direction),
|
||||
do: :ok
|
||||
|
||||
def set_pull_mode(ref, mode) when is_reference(ref) and is_pin_pull_mode(mode), do: :ok
|
||||
end
|
26
test/support/circuits_i2c.ex
Normal file
26
test/support/circuits_i2c.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
defmodule Circuits.I2C do
|
||||
import Wafer.Guards
|
||||
@moduledoc false
|
||||
|
||||
def read(ref, address, bytes, opts \\ [])
|
||||
when is_reference(ref) and is_i2c_address(address) and is_byte_size(bytes) and is_list(opts) do
|
||||
bits = bytes * 8
|
||||
{:ok, <<0::unsigned-integer-size(bits)>>}
|
||||
end
|
||||
|
||||
def write_read(ref, address, data, bytes, opts \\ [])
|
||||
when is_reference(ref) and is_i2c_address(address) and is_binary(data) and
|
||||
is_byte_size(bytes) and is_list(opts) do
|
||||
bits = bytes * 8
|
||||
{:ok, <<0::unsigned-integer-size(bits)>>}
|
||||
end
|
||||
|
||||
def write(ref, address, data, opts \\ [])
|
||||
when is_reference(ref) and is_i2c_address(address) and is_binary(data) and is_list(opts),
|
||||
do: :ok
|
||||
|
||||
def open(name) when is_binary(name), do: {:ok, :erlang.make_ref()}
|
||||
def close(ref) when is_reference(ref), do: :ok
|
||||
|
||||
def detect_devices(bus) when is_reference(bus) or is_binary(bus), do: []
|
||||
end
|
13
test/support/circuits_spi.ex
Normal file
13
test/support/circuits_spi.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule Circuits.SPI do
|
||||
@moduledoc false
|
||||
def open(name, opts \\ [])
|
||||
when is_binary(name) and is_list(opts),
|
||||
do: {:ok, self()}
|
||||
|
||||
def close(ref) when is_reference(ref), do: :ok
|
||||
|
||||
def transfer(ref, data) when is_reference(ref) and is_binary(data) do
|
||||
bits = bit_size(data)
|
||||
{:ok, <<0::unsigned-integer-size(bits)>>}
|
||||
end
|
||||
end
|
15
test/support/elixir_ale_gpio.ex
Normal file
15
test/support/elixir_ale_gpio.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule ElixirALE.GPIO do
|
||||
@moduledoc false
|
||||
|
||||
def set_int(pid, condition) when is_pid(pid) and condition in [:rising, :falling, :both],
|
||||
do: :ok
|
||||
|
||||
def start_link(pin, direction, _opts \\ [])
|
||||
when is_integer(pin) and pin >= 0 and direction in [:in, :out],
|
||||
do: {:ok, self()}
|
||||
|
||||
def release(pid) when is_pid(pid), do: :ok
|
||||
|
||||
def read(pid) when is_pid(pid), do: 0
|
||||
def write(pid, value) when is_pid(pid) and value in [0, 1], do: :ok
|
||||
end
|
|
@ -1,9 +1,27 @@
|
|||
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
|
||||
import Wafer.Guards
|
||||
@moduledoc false
|
||||
|
||||
def read(pid, bytes, options \\ [])
|
||||
when is_pid(pid) and is_byte_size(bytes) and is_list(options) do
|
||||
bits = bytes * 8
|
||||
<<0::unsigned-integer-size(bits)>>
|
||||
end
|
||||
|
||||
def write(pid, data) when is_pid(pid) and is_bitstring(data), do: :ok
|
||||
def write(pid, data, options \\ []) when is_pid(pid) and is_binary(data) and is_list(options),
|
||||
do: :ok
|
||||
|
||||
def write_read(pid, data, bytes, options \\ [])
|
||||
when is_pid(pid) and is_binary(data) and is_byte_size(bytes) and is_list(options) do
|
||||
bits = bytes * 8
|
||||
<<0::unsigned-integer-size(bits)>>
|
||||
end
|
||||
|
||||
def start_link(name, address, opts \\ [])
|
||||
when is_binary(name) and is_i2c_address(address) and is_list(opts),
|
||||
do: {:ok, self()}
|
||||
|
||||
def release(pid) when is_pid(pid), do: :ok
|
||||
|
||||
def detect_devices(bus) when is_binary(bus) or is_pid(bus), do: []
|
||||
end
|
||||
|
|
13
test/support/elixir_ale_spi.ex
Normal file
13
test/support/elixir_ale_spi.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule ElixirALE.SPI do
|
||||
@moduledoc false
|
||||
def start_link(name, spi_opts \\ [], opts \\ [])
|
||||
when is_binary(name) and is_list(spi_opts) and is_list(opts),
|
||||
do: {:ok, self()}
|
||||
|
||||
def release(pid) when is_pid(pid), do: :ok
|
||||
|
||||
def transfer(pid, data) when is_pid(pid) and is_binary(data) do
|
||||
bits = bit_size(data)
|
||||
<<0::unsigned-integer-size(bits)>>
|
||||
end
|
||||
end
|
|
@ -1,4 +1,6 @@
|
|||
defmodule TestUtils do
|
||||
@moduledoc false
|
||||
|
||||
def random_module_name do
|
||||
name =
|
||||
16
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
Mimic.copy(Circuits.GPIO)
|
||||
Mimic.copy(Circuits.I2C)
|
||||
Mimic.copy(Circuits.SPI)
|
||||
Mimic.copy(ElixirALE.GPIO)
|
||||
Mimic.copy(ElixirALE.I2C)
|
||||
Mimic.copy(ElixirALE.SPI)
|
||||
Mimic.copy(Wafer.Chip)
|
||||
Mimic.copy(Wafer.Driver.CircuitsGPIODispatcher)
|
||||
Mimic.copy(Wafer.Driver.ElixirALEGPIODispatcher)
|
||||
ExUnit.start()
|
||||
|
|
Loading…
Reference in a new issue