From 33e332ea22e8ecdb4d57c25035bce046462a0e42 Mon Sep 17 00:00:00 2001 From: James Harton Date: Mon, 30 Dec 2019 13:12:42 +1300 Subject: [PATCH] I believe that everything I wanted now works. Yowza! --- .gitlab-ci.yml | 32 +++ README.md | 8 +- lib/wafer/application.ex | 2 +- lib/wafer/chip.ex | 11 +- lib/wafer/conn.ex | 3 +- lib/wafer/drivers/circuits_gpio.ex | 36 +-- lib/wafer/drivers/circuits_gpio_dispatcher.ex | 139 +++++++----- lib/wafer/drivers/circuits_i2c.ex | 90 ++++++-- lib/wafer/drivers/circuits_spi.ex | 52 +++++ lib/wafer/drivers/elixir_ale_gpio.ex | 29 ++- .../drivers/elixir_ale_gpio_dispatcher.ex | 105 +++++---- lib/wafer/drivers/elixir_ale_i2c.ex | 87 ++++++-- lib/wafer/drivers/elixir_ale_spi.ex | 53 +++++ lib/wafer/gpio.ex | 40 ++-- lib/wafer/gpio_proto.ex | 54 ----- lib/wafer/guards.ex | 29 +++ lib/wafer/i2c.ex | 44 ++++ lib/wafer/registers.ex | 1 + lib/wafer/spi.ex | 19 ++ mix.exs | 8 +- mix.lock | 1 + .../drivers/circuits_gpio_dispatcher_test.exs | 156 +++++++++++++ test/drivers/circuits_gpio_test.exs | 136 ++++++++++++ test/drivers/circuits_i2c_test.exs | 207 ++++++++++++++++++ test/drivers/circuits_spi_test.exs | 52 +++++ test/drivers/elixie_ale_spi_test.exs | 53 +++++ .../elixir_ale_gpio_dispatcher_test.exs | 121 ++++++++++ test/drivers/elixir_ale_gpio_test.exs | 110 ++++++++++ test/drivers/elixir_ale_i2c_test.exs | 203 +++++++++++++++++ test/registers_test.exs | 2 +- test/support/circuits_gpio.ex | 21 ++ test/support/circuits_i2c.ex | 26 +++ test/support/circuits_spi.ex | 13 ++ test/support/elixir_ale_gpio.ex | 15 ++ test/support/elixir_ale_i2c.ex | 24 +- test/support/elixir_ale_spi.ex | 13 ++ test/support/utils.ex | 2 + test/test_helper.exs | 7 + 38 files changed, 1744 insertions(+), 260 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 lib/wafer/drivers/circuits_spi.ex create mode 100644 lib/wafer/drivers/elixir_ale_spi.ex delete mode 100644 lib/wafer/gpio_proto.ex create mode 100644 lib/wafer/guards.ex create mode 100644 lib/wafer/i2c.ex create mode 100644 lib/wafer/spi.ex create mode 100644 test/drivers/circuits_gpio_dispatcher_test.exs create mode 100644 test/drivers/circuits_gpio_test.exs create mode 100644 test/drivers/circuits_i2c_test.exs create mode 100644 test/drivers/circuits_spi_test.exs create mode 100644 test/drivers/elixie_ale_spi_test.exs create mode 100644 test/drivers/elixir_ale_gpio_dispatcher_test.exs create mode 100644 test/drivers/elixir_ale_gpio_test.exs create mode 100644 test/drivers/elixir_ale_i2c_test.exs create mode 100644 test/support/circuits_gpio.ex create mode 100644 test/support/circuits_i2c.ex create mode 100644 test/support/circuits_spi.ex create mode 100644 test/support/elixir_ale_gpio.ex create mode 100644 test/support/elixir_ale_spi.ex diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..558aa39 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/README.md b/README.md index aff98f4..552cc56 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/lib/wafer/application.ex b/lib/wafer/application.ex index 988f800..a5a2a3d 100644 --- a/lib/wafer/application.ex +++ b/lib/wafer/application.ex @@ -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 ] diff --git a/lib/wafer/chip.ex b/lib/wafer/chip.ex index 2903bd7..6ad8082 100644 --- a/lib/wafer/chip.ex +++ b/lib/wafer/chip.ex @@ -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 diff --git a/lib/wafer/conn.ex b/lib/wafer/conn.ex index 4fa238c..5d9e924 100644 --- a/lib/wafer/conn.ex +++ b/lib/wafer/conn.ex @@ -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. diff --git a/lib/wafer/drivers/circuits_gpio.ex b/lib/wafer/drivers/circuits_gpio.ex index 1ca4b73..12238f9 100644 --- a/lib/wafer/drivers/circuits_gpio.ex +++ b/lib/wafer/drivers/circuits_gpio.ex @@ -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 diff --git a/lib/wafer/drivers/circuits_gpio_dispatcher.ex b/lib/wafer/drivers/circuits_gpio_dispatcher.ex index 2bae770..22cff77 100644 --- a/lib/wafer/drivers/circuits_gpio_dispatcher.ex +++ b/lib/wafer/drivers/circuits_gpio_dispatcher.ex @@ -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 diff --git a/lib/wafer/drivers/circuits_i2c.ex b/lib/wafer/drivers/circuits_i2c.ex index 70ce39e..86f4ab0 100644 --- a/lib/wafer/drivers/circuits_i2c.ex +++ b/lib/wafer/drivers/circuits_i2c.ex @@ -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, <>, 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, <>, 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, <>) + 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, <>) 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 diff --git a/lib/wafer/drivers/circuits_spi.ex b/lib/wafer/drivers/circuits_spi.ex new file mode 100644 index 0000000..f418014 --- /dev/null +++ b/lib/wafer/drivers/circuits_spi.ex @@ -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 diff --git a/lib/wafer/drivers/elixir_ale_gpio.ex b/lib/wafer/drivers/elixir_ale_gpio.ex index c5b71bc..2ce02d8 100644 --- a/lib/wafer/drivers/elixir_ale_gpio.ex +++ b/lib/wafer/drivers/elixir_ale_gpio.ex @@ -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 diff --git a/lib/wafer/drivers/elixir_ale_gpio_dispatcher.ex b/lib/wafer/drivers/elixir_ale_gpio_dispatcher.ex index 22a0fea..b2d4269 100644 --- a/lib/wafer/drivers/elixir_ale_gpio_dispatcher.ex +++ b/lib/wafer/drivers/elixir_ale_gpio_dispatcher.ex @@ -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 diff --git a/lib/wafer/drivers/elixir_ale_i2c.ex b/lib/wafer/drivers/elixir_ale_i2c.ex index 29ab473..8a6e2cd 100644 --- a/lib/wafer/drivers/elixir_ale_i2c.ex +++ b/lib/wafer/drivers/elixir_ale_i2c.ex @@ -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, <>, 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, <>) + 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, <>) 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 diff --git a/lib/wafer/drivers/elixir_ale_spi.ex b/lib/wafer/drivers/elixir_ale_spi.ex new file mode 100644 index 0000000..73b4188 --- /dev/null +++ b/lib/wafer/drivers/elixir_ale_spi.ex @@ -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 diff --git a/lib/wafer/gpio.ex b/lib/wafer/gpio.ex index 1236ca5..51784e1 100644 --- a/lib/wafer/gpio.ex +++ b/lib/wafer/gpio.ex @@ -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 diff --git a/lib/wafer/gpio_proto.ex b/lib/wafer/gpio_proto.ex deleted file mode 100644 index b71174c..0000000 --- a/lib/wafer/gpio_proto.ex +++ /dev/null @@ -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 diff --git a/lib/wafer/guards.ex b/lib/wafer/guards.ex new file mode 100644 index 0000000..101e232 --- /dev/null +++ b/lib/wafer/guards.ex @@ -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 diff --git a/lib/wafer/i2c.ex b/lib/wafer/i2c.ex new file mode 100644 index 0000000..1151392 --- /dev/null +++ b/lib/wafer/i2c.ex @@ -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 diff --git a/lib/wafer/registers.ex b/lib/wafer/registers.ex index 3da96ca..f9c519b 100644 --- a/lib/wafer/registers.ex +++ b/lib/wafer/registers.ex @@ -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 diff --git a/lib/wafer/spi.ex b/lib/wafer/spi.ex new file mode 100644 index 0000000..daf2921 --- /dev/null +++ b/lib/wafer/spi.ex @@ -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 diff --git a/mix.exs b/mix.exs index fcf20d4..3b0786a 100644 --- a/mix.exs +++ b/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 diff --git a/mix.lock b/mix.lock index c864c22..b9329c5 100644 --- a/mix.lock +++ b/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"}, diff --git a/test/drivers/circuits_gpio_dispatcher_test.exs b/test/drivers/circuits_gpio_dispatcher_test.exs new file mode 100644 index 0000000..2e0be5c --- /dev/null +++ b/test/drivers/circuits_gpio_dispatcher_test.exs @@ -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 diff --git a/test/drivers/circuits_gpio_test.exs b/test/drivers/circuits_gpio_test.exs new file mode 100644 index 0000000..ad601ca --- /dev/null +++ b/test/drivers/circuits_gpio_test.exs @@ -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 diff --git a/test/drivers/circuits_i2c_test.exs b/test/drivers/circuits_i2c_test.exs new file mode 100644 index 0000000..2cdf8bb --- /dev/null +++ b/test/drivers/circuits_i2c_test.exs @@ -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 diff --git a/test/drivers/circuits_spi_test.exs b/test/drivers/circuits_spi_test.exs new file mode 100644 index 0000000..5f8fdea --- /dev/null +++ b/test/drivers/circuits_spi_test.exs @@ -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 diff --git a/test/drivers/elixie_ale_spi_test.exs b/test/drivers/elixie_ale_spi_test.exs new file mode 100644 index 0000000..5f0c311 --- /dev/null +++ b/test/drivers/elixie_ale_spi_test.exs @@ -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 diff --git a/test/drivers/elixir_ale_gpio_dispatcher_test.exs b/test/drivers/elixir_ale_gpio_dispatcher_test.exs new file mode 100644 index 0000000..201fc92 --- /dev/null +++ b/test/drivers/elixir_ale_gpio_dispatcher_test.exs @@ -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 diff --git a/test/drivers/elixir_ale_gpio_test.exs b/test/drivers/elixir_ale_gpio_test.exs new file mode 100644 index 0000000..c53c2eb --- /dev/null +++ b/test/drivers/elixir_ale_gpio_test.exs @@ -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 diff --git a/test/drivers/elixir_ale_i2c_test.exs b/test/drivers/elixir_ale_i2c_test.exs new file mode 100644 index 0000000..f3bdff8 --- /dev/null +++ b/test/drivers/elixir_ale_i2c_test.exs @@ -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 diff --git a/test/registers_test.exs b/test/registers_test.exs index 5d22355..95d4c8c 100644 --- a/test/registers_test.exs +++ b/test/registers_test.exs @@ -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 diff --git a/test/support/circuits_gpio.ex b/test/support/circuits_gpio.ex new file mode 100644 index 0000000..c868e22 --- /dev/null +++ b/test/support/circuits_gpio.ex @@ -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 diff --git a/test/support/circuits_i2c.ex b/test/support/circuits_i2c.ex new file mode 100644 index 0000000..364fa54 --- /dev/null +++ b/test/support/circuits_i2c.ex @@ -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 diff --git a/test/support/circuits_spi.ex b/test/support/circuits_spi.ex new file mode 100644 index 0000000..3130911 --- /dev/null +++ b/test/support/circuits_spi.ex @@ -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 diff --git a/test/support/elixir_ale_gpio.ex b/test/support/elixir_ale_gpio.ex new file mode 100644 index 0000000..af58e08 --- /dev/null +++ b/test/support/elixir_ale_gpio.ex @@ -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 diff --git a/test/support/elixir_ale_i2c.ex b/test/support/elixir_ale_i2c.ex index 4677a74..19de62b 100644 --- a/test/support/elixir_ale_i2c.ex +++ b/test/support/elixir_ale_i2c.ex @@ -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 diff --git a/test/support/elixir_ale_spi.ex b/test/support/elixir_ale_spi.ex new file mode 100644 index 0000000..e1816b4 --- /dev/null +++ b/test/support/elixir_ale_spi.ex @@ -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 diff --git a/test/support/utils.ex b/test/support/utils.ex index 4234911..6485942 100644 --- a/test/support/utils.ex +++ b/test/support/utils.ex @@ -1,4 +1,6 @@ defmodule TestUtils do + @moduledoc false + def random_module_name do name = 16 diff --git a/test/test_helper.exs b/test/test_helper.exs index b66ca89..1609c2a 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -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()