defmodule Inky.RpiIO do @moduledoc """ An `Inky.InkyIO` implementation intended for use with raspberry pis and relies on Circuits.GPIO and Cirtuits.SPI. """ @behaviour Inky.InkyIO alias Inky.InkyIO defmodule State do @moduledoc false @state_fields [ :gpio_mod, :spi_mod, :busy_pid, :dc_pid, :reset_pid, :spi_pid ] @enforce_keys @state_fields defstruct @state_fields end @busy_pin 17 @cs0_pin 0 @dc_pin 22 @reset_pin 27 @default_pin_mappings %{ busy_pin: @busy_pin, cs0_pin: @cs0_pin, dc_pin: @dc_pin, reset_pin: @reset_pin } @spi_speed_hz 488_000 @spi_command 0 @spi_data 1 @spi_chunk_bytes 4096 # API @impl InkyIO def init(opts \\ []) do gpio = opts[:gpio_mod] || Inky.TestGPIO spi = opts[:spi_mod] || Inky.TestSPI pin_mappings = opts[:pin_mappings] || @default_pin_mappings spi_address = "spidev0." <> to_string(pin_mappings[:cs0_pin]) {:ok, dc_pid} = gpio.open(pin_mappings[:dc_pin], :output) {:ok, reset_pid} = gpio.open(pin_mappings[:reset_pin], :output) {:ok, busy_pid} = gpio.open(pin_mappings[:busy_pin], :input) {:ok, spi_pid} = spi.open(spi_address, speed_hz: @spi_speed_hz) # Use binary pattern matching to pull out the ADC counts (low 10 bits) # <<_::size(6), counts::size(10)>> = SPI.transfer(spi_pid, <<0x78, 0x00>>) %State{ gpio_mod: gpio, spi_mod: spi, busy_pid: busy_pid, dc_pid: dc_pid, reset_pid: reset_pid, spi_pid: spi_pid } end @impl InkyIO def handle_sleep(_state, duration_ms) do :timer.sleep(duration_ms) end @impl InkyIO def handle_read_busy(state), do: gpio_call(state, :read, [state.busy_pid]) @impl InkyIO def handle_reset(state, value), do: :ok = gpio_call(state, :write, [state.reset_pid, value]) @impl InkyIO def handle_command(state, command, data) do write_command(state, command) write_data(state, data) end @impl InkyIO def handle_command(state, command) do write_command(state, command) end # IO primitives defp write_command(state, command) do value = maybe_wrap_integer(command) spi_write(state, @spi_command, value) end require Logger defp write_data(state, data) do value = maybe_wrap_integer(data) spi_write(state, @spi_data, value) end defp spi_write(state, data_or_command, values) when is_list(values), do: spi_write(state, data_or_command, :erlang.list_to_binary(values)) defp spi_write(state, data_or_command, value) when is_binary(value) do :ok = gpio_call(state, :write, [state.dc_pid, data_or_command]) case spi_call(state, :transfer, [state.spi_pid, value]) do {:ok, response} -> {:ok, response} {:error, :transfer_failed} -> spi_call_chunked(state, value) end end defp spi_call_chunked(state, value) do size = byte_size(value) parts = div(size - 1, @spi_chunk_bytes) for x <- 0..parts do offset = x * @spi_chunk_bytes # NOTE: grab the smallest of a chunk or the remainder length = min(@spi_chunk_bytes, size - offset) {:ok, <<_::binary>>} = spi_call(state, :transfer, [state.spi_pid, :binary.part(value, offset, length)]) end end # internals defp maybe_wrap_integer(value), do: if(is_integer(value), do: <>, else: value) defp gpio_call(state, op, args), do: apply(state.gpio_mod, op, args) defp spi_call(state, op, args), do: apply(state.spi_mod, op, args) end