I believe that everything I wanted now works. Yowza!

This commit is contained in:
James Harton 2019-12-30 13:12:42 +13:00
parent fb247dbead
commit 33e332ea22
38 changed files with 1744 additions and 260 deletions

32
.gitlab-ci.yml Normal file
View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

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

View 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

View 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

View 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

View 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

View file

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

View 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

View file

@ -1,4 +1,6 @@
defmodule TestUtils do
@moduledoc false
def random_module_name do
name =
16

View file

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