Make all our protocol derivable.
This commit is contained in:
parent
ba68653925
commit
13f79bf72e
18 changed files with 431 additions and 16 deletions
|
@ -43,6 +43,15 @@ defmodule HTS221 do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
I've included stub implementations of the parts of `ElixirALE` and `Circuits`
|
||||||
|
that are interacted with by this project, so the tests should run and pass on
|
||||||
|
machines without physical hardware interfaces. If you have a Raspberry Pi with
|
||||||
|
a Pi Sense Hat connected you can run the tests with the `FAKE_DRIVERS=false`
|
||||||
|
environment variable set and it will perform integration tests with two of the
|
||||||
|
sensors on this device.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
|
|
@ -4,6 +4,32 @@ defprotocol Wafer.Chip do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A `Chip` is a physical peripheral with registers which can be read from and
|
A `Chip` is a physical peripheral with registers which can be read from and
|
||||||
written to.
|
written to.
|
||||||
|
|
||||||
|
Rather than interacting with this protocol directly, it's a lot easier to use
|
||||||
|
the macros in `Wafer.Registers` to do it for you.
|
||||||
|
|
||||||
|
## Deriving
|
||||||
|
|
||||||
|
If you're implementing your own `Conn` type which simply delegates to one of
|
||||||
|
the lower level drivers then you can derive this protocol automatically:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule MyConnection do
|
||||||
|
@derive Wafer.Chip
|
||||||
|
defstruct [:conn]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If your type uses a key other than `conn` for the inner connection you can
|
||||||
|
specify it while deriving:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule MyConnection do
|
||||||
|
@derive {Wafer.Chip, key: :i2c_conn}
|
||||||
|
defstruct [:i2c_conn]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type register_address :: non_neg_integer
|
@type register_address :: non_neg_integer
|
||||||
|
@ -41,7 +67,7 @@ defprotocol Wafer.Chip do
|
||||||
|
|
||||||
iex> {:ok, conn} = ElixirALEI2C.acquire(bus: "i2c", address: 0x68)
|
iex> {:ok, conn} = ElixirALEI2C.acquire(bus: "i2c", address: 0x68)
|
||||||
...> Chip.write_register(conn, 0, <<0>>)
|
...> Chip.write_register(conn, 0, <<0>>)
|
||||||
:ok
|
{:ok, conn}
|
||||||
"""
|
"""
|
||||||
@spec write_register(Conn.t(), register_address, data :: binary) ::
|
@spec write_register(Conn.t(), register_address, data :: binary) ::
|
||||||
{:ok, t} | {:error, reason :: any}
|
{:ok, t} | {:error, reason :: any}
|
||||||
|
@ -66,9 +92,53 @@ defprotocol Wafer.Chip do
|
||||||
|
|
||||||
iex> {:ok, conn} = ElixirALEI2C.acquire(bus: "i2c", address: 0x68)
|
iex> {:ok, conn} = ElixirALEI2C.acquire(bus: "i2c", address: 0x68)
|
||||||
...> Chip.swap_register(conn, 0, <<1>>)
|
...> Chip.swap_register(conn, 0, <<1>>)
|
||||||
{:ok, <<0>>}
|
{:ok, <<0>>, conn}
|
||||||
"""
|
"""
|
||||||
@spec swap_register(Conn.t(), register_address, new_data :: binary) ::
|
@spec swap_register(Conn.t(), register_address, new_data :: binary) ::
|
||||||
{:ok, data :: binary, t} | {:error, reason :: any}
|
{:ok, data :: binary, t} | {:error, reason :: any}
|
||||||
def swap_register(conn, register_address, new_data)
|
def swap_register(conn, register_address, new_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl Wafer.Chip, for: Any do
|
||||||
|
defmacro __deriving__(module, struct, options) do
|
||||||
|
key = Keyword.get(options, :key, :conn)
|
||||||
|
|
||||||
|
unless Map.has_key?(struct, key) do
|
||||||
|
raise(
|
||||||
|
"Unable to derive `Wafer.Chip` for `#{module}`: key `#{inspect(key)}` not present in struct."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
quote do
|
||||||
|
defimpl Wafer.Chip, for: unquote(module) do
|
||||||
|
import Wafer.Guards
|
||||||
|
alias Wafer.Chip
|
||||||
|
|
||||||
|
def read_register(%{unquote(key) => inner_conn}, register_address, bytes)
|
||||||
|
when is_register_address(register_address) and is_byte_size(bytes),
|
||||||
|
do: Chip.read_register(inner_conn, register_address, bytes)
|
||||||
|
|
||||||
|
def write_register(%{unquote(key) => inner_conn} = conn, register_address, data)
|
||||||
|
when is_register_address(register_address) and is_binary(data) do
|
||||||
|
with {:ok, inner_conn} <- Chip.write_register(inner_conn, register_address, data),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def swap_register(%{unquote(key) => inner_conn} = conn, register_address, new_data) do
|
||||||
|
with {:ok, data, inner_conn} <-
|
||||||
|
Chip.swap_register(inner_conn, register_address, new_data),
|
||||||
|
do: {:ok, data, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_register(unknown, _register_address, _bytes),
|
||||||
|
do: {:error, "`Wafer.Chip` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def write_register(unknown, _register_address, _data),
|
||||||
|
do: {:error, "`Wafer.Chip` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def swap_register(unknown, _register_address, _new_data),
|
||||||
|
do: {:error, "`Wafer.Chip` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
end
|
||||||
|
|
|
@ -1,6 +1,32 @@
|
||||||
defmodule Wafer.Conn do
|
defmodule Wafer.Conn do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Defines a protocol and behaviour for connecting to a peripheral.
|
Defines a behaviour for connecting to a peripheral.
|
||||||
|
|
||||||
|
This behaviour is used by all the driver types in `Wafer` and you should
|
||||||
|
implement it for your devices also.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Implementing `Conn` for a `HTS221` chip connected via Circuits' I2C driver.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule HTS221 do
|
||||||
|
defstruct ~w[conn]a
|
||||||
|
alias Wafer.Drivers.CircuitsI2C, as: Driver
|
||||||
|
@behaviour Wafer.Conn
|
||||||
|
@default_bus "i2c-1"
|
||||||
|
@default_address 0x5F
|
||||||
|
|
||||||
|
def acquire(opts) when is_list(opts) do
|
||||||
|
bus = Keyword.get(opts, :bus, @default_bus)
|
||||||
|
address = Keyword.get(opts, :address, @default_address)
|
||||||
|
with {:ok, conn} <- Driver.acquire(bus_name: bus, address: address),
|
||||||
|
do: {:ok, %HTS221{conn: conn}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def release(%HTS221{conn: conn}), do: Driver.release(conn)
|
||||||
|
end
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type options :: [option]
|
@type options :: [option]
|
||||||
|
|
|
@ -7,6 +7,8 @@ defmodule Wafer.Driver.CircuitsGPIO do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A connection to a native GPIO pin via Circuits' GPIO driver.
|
A connection to a native GPIO pin via Circuits' GPIO driver.
|
||||||
|
|
||||||
|
Implements the `Wafer.Conn` behaviour as well as the `Wafer.GPIO` protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: %__MODULE__{ref: reference, pin: non_neg_integer, direction: GPIO.pin_direction()}
|
@type t :: %__MODULE__{ref: reference, pin: non_neg_integer, direction: GPIO.pin_direction()}
|
||||||
|
@ -41,7 +43,7 @@ defmodule Wafer.Driver.CircuitsGPIO do
|
||||||
Note that other connections may still be using the pin.
|
Note that other connections may still be using the pin.
|
||||||
"""
|
"""
|
||||||
@spec release(t) :: :ok | {:error, reason :: any}
|
@spec release(t) :: :ok | {:error, reason :: any}
|
||||||
def release(%__MODULE__{ref: ref}) when is_reference(ref), do: Driver.close(ref)
|
def release(%__MODULE__{ref: ref} = _conn) when is_reference(ref), do: Driver.close(ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Wafer.GPIO, for: Wafer.Driver.CircuitsGPIO do
|
defimpl Wafer.GPIO, for: Wafer.Driver.CircuitsGPIO do
|
||||||
|
@ -68,7 +70,9 @@ defimpl Wafer.GPIO, for: Wafer.Driver.CircuitsGPIO do
|
||||||
|
|
||||||
def direction(%{ref: ref} = conn, direction)
|
def direction(%{ref: ref} = conn, direction)
|
||||||
when is_reference(ref) and is_pin_direction(direction) do
|
when is_reference(ref) and is_pin_direction(direction) do
|
||||||
case(Driver.set_direction(ref, direction)) do
|
pin_dir = String.to_atom(Enum.join([direction, "put"], ""))
|
||||||
|
|
||||||
|
case(Driver.set_direction(ref, pin_dir)) do
|
||||||
:ok -> {:ok, %{conn | direction: direction}}
|
:ok -> {:ok, %{conn | direction: direction}}
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,8 @@ defmodule Wafer.Driver.CircuitsI2C do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A connection to a chip via Circuits' I2C driver.
|
A connection to a chip via Circuits' I2C driver.
|
||||||
|
|
||||||
|
Implements the `Wafer.Conn` behaviour as well as the `Wafer.Chip` and `Wafer.I2C` protocols.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: %__MODULE__{address: I2C.address(), bus: binary, ref: reference}
|
@type t :: %__MODULE__{address: I2C.address(), bus: binary, ref: reference}
|
||||||
|
@ -38,7 +40,7 @@ defmodule Wafer.Driver.CircuitsI2C do
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec release(t) :: :ok | {:error, reason :: any}
|
@spec release(t) :: :ok | {:error, reason :: any}
|
||||||
def release(%__MODULE__{ref: ref}), do: Driver.close(ref)
|
def release(%__MODULE__{ref: ref} = _conn), do: Driver.close(ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Wafer.Chip, for: Wafer.Driver.CircuitsI2C do
|
defimpl Wafer.Chip, for: Wafer.Driver.CircuitsI2C do
|
||||||
|
|
|
@ -5,6 +5,8 @@ defmodule Wafer.Driver.CircuitsSPI do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A connection to a chip via Circuits's SPI driver.
|
A connection to a chip via Circuits's SPI driver.
|
||||||
|
|
||||||
|
Implements the `Wafer.Conn` behaviour as well as the `Wafer.SPI` protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: %__MODULE__{bus: binary, ref: reference}
|
@type t :: %__MODULE__{bus: binary, ref: reference}
|
||||||
|
@ -37,7 +39,7 @@ defmodule Wafer.Driver.CircuitsSPI do
|
||||||
Close the SPI bus connection.
|
Close the SPI bus connection.
|
||||||
"""
|
"""
|
||||||
@spec release(t) :: :ok | {:error, reason :: any}
|
@spec release(t) :: :ok | {:error, reason :: any}
|
||||||
def release(%__MODULE__{ref: ref}) when is_reference(ref), do: Driver.close(ref)
|
def release(%__MODULE__{ref: ref} = _conn) when is_reference(ref), do: Driver.close(ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Wafer.SPI, for: Wafer.Driver.CircuitsSPI do
|
defimpl Wafer.SPI, for: Wafer.Driver.CircuitsSPI do
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule Wafer.Driver.ElixirALEGPIO do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A connection to a native GPIO pin via ElixirALE's GPIO driver.
|
A connection to a native GPIO pin via ElixirALE's GPIO driver.
|
||||||
|
|
||||||
|
Implements the `Wafer.Conn` behaviour as well as the `Wafer.GPIO` protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: %__MODULE__{pid: pid}
|
@type t :: %__MODULE__{pid: pid}
|
||||||
|
@ -39,7 +41,7 @@ defmodule Wafer.Driver.ElixirALEGPIO do
|
||||||
Note that other connections may still be using the pin.
|
Note that other connections may still be using the pin.
|
||||||
"""
|
"""
|
||||||
@spec release(t) :: :ok | {:error, reason :: any}
|
@spec release(t) :: :ok | {:error, reason :: any}
|
||||||
def release(%__MODULE__{pid: pid}), do: Driver.release(pid)
|
def release(%__MODULE__{pid: pid} = _conn), do: Driver.release(pid)
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Wafer.GPIO, for: Wafer.Driver.ElixirALEGPIO do
|
defimpl Wafer.GPIO, for: Wafer.Driver.ElixirALEGPIO do
|
||||||
|
|
|
@ -7,6 +7,8 @@ defmodule Wafer.Driver.ElixirALEI2C do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A connection to a chip via ElixirALE's I2C driver.
|
A connection to a chip via ElixirALE's I2C driver.
|
||||||
|
|
||||||
|
Implements the `Wafer.Conn` behaviour as well as the `Wafer.Chip` and `Wafer.I2C` protocols.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: %__MODULE__{address: I2C.address(), bus: binary, pid: pid}
|
@type t :: %__MODULE__{address: I2C.address(), bus: binary, pid: pid}
|
||||||
|
@ -39,7 +41,7 @@ defmodule Wafer.Driver.ElixirALEI2C do
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec release(t) :: :ok | {:error, reason :: any}
|
@spec release(t) :: :ok | {:error, reason :: any}
|
||||||
def release(%__MODULE__{pid: pid}) when is_pid(pid), do: ElixirALE.I2C.release(pid)
|
def release(%__MODULE__{pid: pid} = _conn) when is_pid(pid), do: ElixirALE.I2C.release(pid)
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Wafer.Chip, for: Wafer.Driver.ElixirALEI2C do
|
defimpl Wafer.Chip, for: Wafer.Driver.ElixirALEI2C do
|
||||||
|
|
|
@ -5,6 +5,8 @@ defmodule Wafer.Driver.ElixirALESPI do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A connection to a chip via ElixirALE's SPI driver.
|
A connection to a chip via ElixirALE's SPI driver.
|
||||||
|
|
||||||
|
Implements the `Wafer.Conn` behaviour as well as the `Wafer.SPI` protocol.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type t :: %__MODULE__{bus: binary, pid: pid}
|
@type t :: %__MODULE__{bus: binary, pid: pid}
|
||||||
|
@ -38,7 +40,7 @@ defmodule Wafer.Driver.ElixirALESPI do
|
||||||
Close the SPI bus connection.
|
Close the SPI bus connection.
|
||||||
"""
|
"""
|
||||||
@spec release(t) :: :ok | {:error, reason :: any}
|
@spec release(t) :: :ok | {:error, reason :: any}
|
||||||
def release(%__MODULE__{pid: pid}) when is_pid(pid), do: Driver.release(pid)
|
def release(%__MODULE__{pid: pid} = _conn) when is_pid(pid), do: Driver.release(pid)
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Wafer.SPI, for: Wafer.Driver.ElixirALESPI do
|
defimpl Wafer.SPI, for: Wafer.Driver.ElixirALESPI do
|
||||||
|
|
|
@ -3,6 +3,31 @@ defprotocol Wafer.GPIO do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A `GPIO` is a physical pin which can be read from and written to.
|
A `GPIO` is a physical pin which can be read from and written to.
|
||||||
|
|
||||||
|
Some hardware supports interrupts, some has internal pull up/down resistors.
|
||||||
|
Wafer supports all of these, however not all drivers do.
|
||||||
|
|
||||||
|
## Deriving
|
||||||
|
|
||||||
|
If you're implementing your own `Conn` type that simply delegates to one of
|
||||||
|
the lower level drivers then you can derive this protocol automatically:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defstruct MyPin do
|
||||||
|
@derive Wafer.GPIO
|
||||||
|
defstruct [:conn]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If your type uses a key other than `conn` for the inner connection you can
|
||||||
|
specify it while deriving:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defstruct MyPin do
|
||||||
|
@derive {Wafer.GPIO, key: :gpio_conn}
|
||||||
|
defstruct [:gpio_conn]
|
||||||
|
end
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type pin_direction :: :in | :out
|
@type pin_direction :: :in | :out
|
||||||
|
@ -64,3 +89,73 @@ defprotocol Wafer.GPIO do
|
||||||
@spec pull_mode(Conn.t(), pull_mode) :: {:ok, Conn.t()} | {:error, reason :: any}
|
@spec pull_mode(Conn.t(), pull_mode) :: {:ok, Conn.t()} | {:error, reason :: any}
|
||||||
def pull_mode(conn, pull_mode)
|
def pull_mode(conn, pull_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl Wafer.GPIO, for: Any do
|
||||||
|
defmacro __deriving__(module, struct, options) do
|
||||||
|
key = Keyword.get(options, :key, :conn)
|
||||||
|
|
||||||
|
unless Map.has_key?(struct, key) do
|
||||||
|
raise(
|
||||||
|
"Unable to derive `Wafer.GPIO` for `#{module}`: key `#{inspect(key)}` not present in struct."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
quote do
|
||||||
|
defimpl Wafer.GPIO, for: unquote(module) do
|
||||||
|
import Wafer.Guards
|
||||||
|
alias Wafer.GPIO
|
||||||
|
|
||||||
|
def read(%{unquote(key) => inner_conn} = conn) do
|
||||||
|
with {:ok, pin_value, inner_conn} <- GPIO.read(inner_conn),
|
||||||
|
do: {:ok, pin_value, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(%{unquote(key) => inner_conn} = conn, pin_value) when is_pin_value(pin_value) do
|
||||||
|
with {:ok, inner_conn} <- GPIO.write(inner_conn, pin_value),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def direction(%{unquote(key) => inner_conn} = conn, pin_direction)
|
||||||
|
when is_pin_direction(pin_direction) do
|
||||||
|
with {:ok, inner_conn} <- GPIO.direction(inner_conn, pin_direction),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable_interrupt(%{unquote(key) => inner_conn} = conn, pin_condition)
|
||||||
|
when is_pin_condition(pin_condition) do
|
||||||
|
with {:ok, inner_conn} <- GPIO.enable_interrupt(inner_conn, pin_condition),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable_interrupt(%{unquote(key) => inner_conn} = conn, pin_condition)
|
||||||
|
when is_pin_condition(pin_condition) do
|
||||||
|
with {:ok, inner_conn} <- GPIO.disable_interrupt(inner_conn, pin_condition),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def pull_mode(%{unquote(key) => inner_conn} = conn, pull_mode)
|
||||||
|
when is_pin_pull_mode(pull_mode) do
|
||||||
|
with {:ok, inner_conn} <- GPIO.pull_mode(inner_conn, pull_mode),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(unknown), do: {:error, "`Wafer.GPIO` not implemented for `#{inspect(unknown)}"}
|
||||||
|
|
||||||
|
def write(unknown, _pin_value),
|
||||||
|
do: {:error, "`Wafer.GPIO` not implemented for `#{inspect(unknown)}"}
|
||||||
|
|
||||||
|
def direction(unknown, _pin_direction),
|
||||||
|
do: {:error, "`Wafer.GPIO` not implemented for `#{inspect(unknown)}"}
|
||||||
|
|
||||||
|
def enable_interrupt(unknown, _pin_condition),
|
||||||
|
do: {:error, "`Wafer.GPIO` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def disable_interrupt(unknown, _pin_condition),
|
||||||
|
do: {:error, "`Wafer.GPIO` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def pull_mode(unknown, _pull_mode),
|
||||||
|
do: {:error, "`Wafer.GPIO` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Wafer.Guards do
|
||||||
Handy guards which you can use in your code to assert correct values.
|
Handy guards which you can use in your code to assert correct values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc "A positive integer"
|
@doc "Pin numbers are non negative integers"
|
||||||
defguard is_pin_number(pin) when is_integer(pin) and pin >= 0
|
defguard is_pin_number(pin) when is_integer(pin) and pin >= 0
|
||||||
|
|
||||||
@doc "Either `:in` or `:out`"
|
@doc "Either `:in` or `:out`"
|
||||||
|
@ -21,9 +21,9 @@ defmodule Wafer.Guards do
|
||||||
@doc "An integer between `0` and `0x7F`"
|
@doc "An integer between `0` and `0x7F`"
|
||||||
defguard is_i2c_address(address) when is_integer(address) and address >= 0 and address <= 0x7F
|
defguard is_i2c_address(address) when is_integer(address) and address >= 0 and address <= 0x7F
|
||||||
|
|
||||||
@doc "A positive integer"
|
@doc "Register addresses are non negative integers usually only one byte, but we don't enforce that here"
|
||||||
defguard is_register_address(address) when is_integer(address) and address >= 0
|
defguard is_register_address(address) when is_integer(address) and address >= 0
|
||||||
|
|
||||||
@doc "A positive integer"
|
@doc "Byte sizes are non negative integers"
|
||||||
defguard is_byte_size(bytes) when is_integer(bytes) and bytes >= 0
|
defguard is_byte_size(bytes) when is_integer(bytes) and bytes >= 0
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,30 @@ defprotocol Wafer.I2C do
|
||||||
A protocol for interacting with I2C devices directly. Most of the time you'll
|
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
|
want to use the `Chip` protocol for working with registers, but this is
|
||||||
provided for consistency's sake.
|
provided for consistency's sake.
|
||||||
|
|
||||||
|
This API is extremely similar to the `ElixirALE.I2C` and `Circuits.I2C` APIs,
|
||||||
|
except that it takes a `Conn` which implements `I2C` as an argument.
|
||||||
|
|
||||||
|
## Deriving
|
||||||
|
|
||||||
|
If you're implementing your own `Conn` type that simply delegates to one of
|
||||||
|
the lower level drivers then you can derive this protocol automatically:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defstruct MyI2CDevice do
|
||||||
|
@derive Wafer.I2C
|
||||||
|
defstruct [:conn]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If your type uses a key other than `conn` for the inner connection you can
|
||||||
|
specify it while deriving:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defstruct MyI2CDevice do
|
||||||
|
@derive {Wafer.I2C, key: :i2c_conn}
|
||||||
|
defstruct [:i2c_conn]
|
||||||
|
end
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type address :: 0..0x7F
|
@type address :: 0..0x7F
|
||||||
|
@ -42,3 +66,54 @@ defprotocol Wafer.I2C do
|
||||||
@spec detect_devices(Conn.t()) :: {:ok, [address]}
|
@spec detect_devices(Conn.t()) :: {:ok, [address]}
|
||||||
def detect_devices(conn)
|
def detect_devices(conn)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl Wafer.I2C, for: Any do
|
||||||
|
defmacro __deriving__(module, struct, options) do
|
||||||
|
key = Keyword.get(options, :key, :conn)
|
||||||
|
|
||||||
|
unless Map.has_key?(struct, key) do
|
||||||
|
raise(
|
||||||
|
"Unable to derive `Wafer.I2C` for `#{module}`: key `#{inspect(key)}` not present in struct."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
quote do
|
||||||
|
defimpl Wafer.I2C, for: unquote(module) do
|
||||||
|
import Wafer.Guards
|
||||||
|
alias Wafer.I2C
|
||||||
|
|
||||||
|
def read(%{unquote(key) => inner_conn}, bytes_to_read, options \\ [])
|
||||||
|
when is_byte_size(bytes_to_read) and is_list(options) do
|
||||||
|
I2C.read(inner_conn, bytes_to_read, options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def write(%{unquote(key) => inner_conn} = conn, data, options \\ [])
|
||||||
|
when is_binary(data) and is_list(options) do
|
||||||
|
with {:ok, inner_conn} <- I2C.write(inner_conn, data, options),
|
||||||
|
do: {:ok, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_read(%{unquote(key) => inner_conn} = conn, data, bytes_to_read, options \\ [])
|
||||||
|
when is_binary(data) and is_byte_size(bytes_to_read) and is_list(options) do
|
||||||
|
with {:ok, data, inner_conn} <-
|
||||||
|
I2C.write_read(inner_conn, data, bytes_to_read, options),
|
||||||
|
do: {:ok, data, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def detect_devices(%{unquote(key) => inner_conn}), do: I2C.detect_devices(inner_conn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read(unknown, _bytes_to_read, _options \\ []),
|
||||||
|
do: {:error, "`Wafer.I2C` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def write(unknown, _data, _options \\ []),
|
||||||
|
do: {:error, "`Wafer.I2C` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def write_read(unknown, _data, _bytes_to_read, _options \\ []),
|
||||||
|
do: {:error, "`Wafer.I2C` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
|
||||||
|
def detect_devices(unknown),
|
||||||
|
do: {:error, "`Wafer.I2C` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
end
|
||||||
|
|
|
@ -3,6 +3,9 @@ defmodule Wafer.Registers do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
This module provides helpful macros for specifying the registers used to
|
This module provides helpful macros for specifying the registers used to
|
||||||
communicate with your device.
|
communicate with your device.
|
||||||
|
|
||||||
|
This can be a massive time saver, and means you can basically just copy them
|
||||||
|
straight out of the datasheet.
|
||||||
"""
|
"""
|
||||||
alias Wafer.Chip
|
alias Wafer.Chip
|
||||||
alias Wafer.Conn
|
alias Wafer.Conn
|
||||||
|
|
|
@ -3,6 +3,32 @@ defprotocol Wafer.SPI do
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
A (very simple) protocol for interacting with SPI connected devices.
|
A (very simple) protocol for interacting with SPI connected devices.
|
||||||
|
|
||||||
|
This API is a minimal version of the `ElixirALE.SPI` and `Circuits.SPI` APIs,
|
||||||
|
except that it takes a `Conn` which implements `SPI` as an argument. If you
|
||||||
|
want to use any advanced features, such as bus detection, I advise you to
|
||||||
|
interact with the underlying driver directly.
|
||||||
|
|
||||||
|
## Deriving
|
||||||
|
|
||||||
|
If you're implementing your own `Conn` type that simply delegates to one of
|
||||||
|
the lower level drivers then you can derive this protocol automatically:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defstruct MySPIDevice do
|
||||||
|
@derive Wafer.SPI
|
||||||
|
defstruct [:conn]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If your type uses a key other than `conn` for the inner connection you can
|
||||||
|
specify it while deriving:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defstruct MySPIDevice do
|
||||||
|
@derive {Wafer.SPI, key: :spi_conn}
|
||||||
|
defstruct [:spi_conn]
|
||||||
|
end
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@type data :: binary
|
@type data :: binary
|
||||||
|
@ -17,3 +43,31 @@ defprotocol Wafer.SPI do
|
||||||
@spec transfer(Conn.t(), data) :: {:ok, data, Conn.t()} | {:error, reason :: any}
|
@spec transfer(Conn.t(), data) :: {:ok, data, Conn.t()} | {:error, reason :: any}
|
||||||
def transfer(conn, data)
|
def transfer(conn, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl Wafer.SPI, for: Any do
|
||||||
|
defmacro __deriving__(module, struct, options) do
|
||||||
|
key = Keyword.get(options, :key, :conn)
|
||||||
|
|
||||||
|
unless Map.has_key?(struct, key) do
|
||||||
|
raise(
|
||||||
|
"Unable to derive `Wafer.SPI` for `#{module}`: key `#{inspect(key)}` not present in struct."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
quote do
|
||||||
|
defimpl Wafer.SPI, for: unquote(module) do
|
||||||
|
import Wafer.Guards
|
||||||
|
alias Wafer.SPI
|
||||||
|
|
||||||
|
def transfer(%{unquote(key) => inner_conn}, data)
|
||||||
|
when is_binary(data) do
|
||||||
|
with {:ok, data, inner_conn} <- SPI.transfer(inner_conn, data),
|
||||||
|
do: {:ok, data, Map.put(conn, unquote(key), inner_conn)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def transfer(unknown, _data),
|
||||||
|
do: {:error, "`Wafer.SPI` not implemented for `#{inspect(unknown)}`"}
|
||||||
|
end
|
||||||
|
|
25
mix.exs
25
mix.exs
|
@ -2,14 +2,23 @@ defmodule Wafer.MixProject do
|
||||||
use Mix.Project
|
use Mix.Project
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
|
@description """
|
||||||
|
Wafer is an Elixir library to make writing drivers for i2c and SPI connected
|
||||||
|
peripherals and interacting with GPIO pins easier.
|
||||||
|
"""
|
||||||
|
@version "0.1.0"
|
||||||
|
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :wafer,
|
app: :wafer,
|
||||||
version: "0.1.0",
|
version: @version,
|
||||||
elixir: "~> 1.9",
|
elixir: "~> 1.9",
|
||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps()
|
package: package(),
|
||||||
|
description: @description,
|
||||||
|
deps: deps(),
|
||||||
|
consolidate_protocols: Mix.env() != :test
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -21,9 +30,21 @@ defmodule Wafer.MixProject do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def package do
|
||||||
|
[
|
||||||
|
maintainers: ["James Harton <james@automat.nz>"],
|
||||||
|
licenses: ["MIT"],
|
||||||
|
links: %{
|
||||||
|
"Source" => "https://gitlab.com/jimsy/wafer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
# Run "mix help deps" to learn about dependencies.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
|
{:ex_doc, ">= 0.0.0", only: :dev},
|
||||||
|
{:earmark, ">= 0.0.0", only: :dev},
|
||||||
{:mimic, "~> 1.1", only: :test},
|
{:mimic, "~> 1.1", only: :test},
|
||||||
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.1", only: [:dev, :test], runtime: false},
|
||||||
{:elixir_ale, "~> 1.2", optional: true},
|
{:elixir_ale, "~> 1.2", optional: true},
|
||||||
|
|
5
mix.lock
5
mix.lock
|
@ -4,8 +4,13 @@
|
||||||
"circuits_i2c": {:hex, :circuits_i2c, "0.3.5", "43e043d7efc3aead364061f8a7ed627f81ff7cef52bfa47cb629d8a68ca56a9f", [: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"},
|
"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"},
|
"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"},
|
||||||
|
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "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_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"},
|
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"},
|
||||||
|
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
|
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"},
|
"mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"},
|
||||||
|
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"},
|
||||||
}
|
}
|
||||||
|
|
43
test/chip_test.exs
Normal file
43
test/chip_test.exs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
defmodule WaferChipTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Wafer.Chip
|
||||||
|
|
||||||
|
describe "__deriving__/3" do
|
||||||
|
test "deriving with default key name" do
|
||||||
|
mod = test_mod()
|
||||||
|
assert Chip.impl_for!(struct(mod, conn: :noop))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "deriving with a specified key name" do
|
||||||
|
mod = test_mod(:marty)
|
||||||
|
assert Chip.impl_for!(struct(mod, fruit: :noop))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp test_mod(key \\ :conn) do
|
||||||
|
mod = random_module_name()
|
||||||
|
|
||||||
|
if key == :conn do
|
||||||
|
defmodule mod do
|
||||||
|
@derive Chip
|
||||||
|
defstruct [:conn]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
defmodule mod do
|
||||||
|
@derive {Chip, key: key}
|
||||||
|
defstruct [key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mod
|
||||||
|
end
|
||||||
|
|
||||||
|
defp random_module_name do
|
||||||
|
name =
|
||||||
|
16
|
||||||
|
|> :crypto.strong_rand_bytes()
|
||||||
|
|> Base.encode64(padding: false)
|
||||||
|
|
||||||
|
Module.concat(__MODULE__, name)
|
||||||
|
end
|
||||||
|
end
|
|
@ -79,7 +79,7 @@ defmodule WaferDriverCircuitsGPIOTest do
|
||||||
Driver
|
Driver
|
||||||
|> expect(:set_direction, 1, fn ref, direction ->
|
|> expect(:set_direction, 1, fn ref, direction ->
|
||||||
assert ref == conn.ref
|
assert ref == conn.ref
|
||||||
assert direction == :in
|
assert direction == :input
|
||||||
:ok
|
:ok
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue