diff --git a/lib/display/display.ex b/lib/display/display.ex index 2743c5a..496e0ca 100644 --- a/lib/display/display.ex +++ b/lib/display/display.ex @@ -4,21 +4,47 @@ defmodule Inky.Display do """ alias Inky.LookupTables - + @type t() :: %__MODULE__{} - @enforce_keys [:type, :width, :height, :packed_dimensions, :rotation, :accent, :luts] + @enforce_keys [:type, :width, :height, :rotation, :accent] defstruct type: nil, width: 0, height: 0, packed_dimensions: %{}, + packed_resolution: nil, rotation: 0, accent: :black, luts: <<>> - @spec spec_for(:phat | :what, :black | :red | :yellow) :: Inky.Display.t() + @spec spec_for(:impression) :: Inky.Display.t() + def spec_for(type = :impression) do + %__MODULE__{ + type: type, + width: 600, + height: 448, + packed_resolution: <<2, 88, 1, 192>>, # I used the struct.pack in Python to generate this + rotation: 0, + accent: nil, + } + end + + @spec spec_for(:phat_ssd1608 | :phat | :what, :black | :red | :yellow) :: Inky.Display.t() def spec_for(type, accent \\ :black) + def spec_for(type = :phat_ssd1608, accent) do + # Keep it minimal. Details are specified in `Inky.HAL.PhatSSD1608`. + %__MODULE__{ + type: type, + width: 250, + height: 122, + packed_dimensions: %{}, + rotation: -90, + accent: accent, + luts: <<>> + } + end + def spec_for(type = :phat, accent) do %__MODULE__{ type: type, @@ -83,4 +109,32 @@ defmodule Inky.Display do # Little endian, unsigned short <> end + + # colorsets from pimoroni library + # https://github.com/pimoroni/inky/blob/54684464b2f35bfd52208cdfb922c09685644181/library/inky/inky_uc8159.py#L26-L46 + defp get_colorset(:desaturated) do + [ + [0, 0, 0], + [255, 255, 255], + [0, 255, 0], + [0, 0, 255], + [255, 0, 0], + [255, 255, 0], + [255, 140, 0], + [255, 255, 255] + ] + end + + defp get_colorset(:saturated) do + [ + [57, 48, 57], + [255, 255, 255], + [58, 91, 70], + [61, 59, 94], + [156, 72, 75], + [208, 190, 71], + [177, 106, 73], + [255, 255, 255] + ] + end end diff --git a/lib/display/eeprom.ex b/lib/display/eeprom.ex new file mode 100644 index 0000000..2483906 --- /dev/null +++ b/lib/display/eeprom.ex @@ -0,0 +1,82 @@ +defmodule Inky.EEPROM do + @moduledoc false + + @eep_address 0x50 + + @display_variants { + :unknown, + "Red pHAT (High-Temp)", + "Yellow wHAT", + "Black wHAT", + "Black pHAT", + "Yellow pHAT", + "Red wHAT", + "Red wHAT (High-Temp)", + "Red wHAT", + :unknown, + "Black pHAT (SSD1608)", + "Red pHAT (SSD1608)", + "Yellow pHAT (SSD1608)", + :unknown, + "7-Color (UC8159)" + } + + @colors {:unknown, :black, :red, :yellow, :unknown, :seven_color} + + @fields [:width, :height, :color, :pcb_variant, :display_variant, :timestamp] + + @enforce_keys @fields + defstruct @fields + + @type t :: %__MODULE__{} + + @doc """ + Reads the device info from the EEPROM. This operation seems to work successfully only once. + + ## Examples + + iex> Inky.EEPROM.read() + {:ok, + %Inky.EEPROM{ + color: :red, + display_variant: "Red pHAT (SSD1608)", + height: 104, + pcb_variant: 12, + timestamp: "2021-03-30 08:58:28.9", + width: 212 + }} + + """ + @spec read(atom()) :: {:ok, Inky.EEPROM.t()} | {:error, any()} + def read(i2c_mod \\ Circuits.I2C) do + with {:ok, ref} <- i2c_mod.open("i2c-1"), + {:ok, data} <- i2c_mod.write_read(ref, @eep_address, <<0>>, 29) do + i2c_mod.close(ref) + parse_data(data) + end + end + + @doc """ + Parses data from Inky's EEPROM. + """ + @spec parse_data(<<_::232>>) :: {:ok, t()} | {:error, {:invalid_data, any()}} + def parse_data( + <> + ) + when color in 0..5 and display_variant in 0..14 do + {:ok, + __struct__( + width: width, + height: height, + color: elem(@colors, color), + pcb_variant: pcb_variant, + display_variant: elem(@display_variants, display_variant), + timestamp: timestamp + )} + end + + def parse_data(data) do + {:error, {:invalid_data, data}} + end +end diff --git a/lib/hal/hal.ex b/lib/hal/hal.ex index 44081ff..9787df6 100644 --- a/lib/hal/hal.ex +++ b/lib/hal/hal.ex @@ -12,5 +12,5 @@ defmodule Inky.HAL do policy :: :await | :once, state :: Inky.IOCommands.State.t() ) :: - :ok | {:error, :device_busy} + io_state() | :ok | {:error, :device_busy} end diff --git a/lib/hal/hal_ssd1608.ex b/lib/hal/hal_ssd1608.ex new file mode 100644 index 0000000..dc9b794 --- /dev/null +++ b/lib/hal/hal_ssd1608.ex @@ -0,0 +1,200 @@ +defmodule Inky.HAL.PhatSSD1608 do + @default_io_mod Inky.RpiIO + + @moduledoc """ + An `Inky.HAL` implementation responsible for sending commands to the Inky + screen with SSD1608 display driver. It delegates to whatever IO module its + user provides at init, but defaults to #{inspect(@default_io_mod)} + """ + + @behaviour Inky.HAL + + alias Inky.PixelUtil + use Bitwise, only_operators: true + + @color_map_black %{black: 0, miss: 1} + @color_map_accent %{red: 1, yellow: 1, accent: 1, miss: 0} + + @cols 136 + @rows 250 + @rotation -90 + @lut_data <<0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, 0x69, 0x59, 0x58, 0x99, + 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, + 0x01, 0x00>> + + @cmd_set_driver_output 0x01 + @cmd_set_data_entry_mode 0x11 + @cmd_soft_reset 0x12 + @cmd_activate_display_update_sequence 0x20 + @cmd_write_ram 0x24 + @cmd_write_alt_ram 0x26 + @cmd_write_vcom 0x2C + @cmd_write_lut 0x32 + @cmd_set_dummy_line_period 0x3A + @cmd_set_gate_line_width 0x3B + @cmd_set_border_waveform 0x3C + @cmd_set_ram_x_position 0x44 + @cmd_set_ram_y_position 0x45 + @cmd_set_ram_x_address 0x4E + @cmd_set_ram_y_address 0x4F + + defmodule State do + @moduledoc false + + @state_fields [:display, :io_mod, :io_state] + + @enforce_keys @state_fields + defstruct @state_fields + + @type t :: %__MODULE__{} + end + + # + # API + # + + @impl Inky.HAL + def init(args) do + display = args[:display] || raise(ArgumentError, message: ":display missing in args") + io_mod = args[:io_mod] || @default_io_mod + + io_args = args[:io_args] || [] + io_args = if :gpio_mod in io_args, do: io_args, else: [gpio_mod: Circuits.GPIO] ++ io_args + io_args = if :spi_mod in io_args, do: io_args, else: [spi_mod: Circuits.SPI] ++ io_args + + %State{ + display: display, + io_mod: io_mod, + io_state: io_mod.init(io_args) + } + end + + @impl Inky.HAL + def handle_update(pixels, border, push_policy, state = %State{}) do + black_bits = PixelUtil.pixels_to_bits(pixels, @rows, @cols, @rotation, @color_map_black) + accent_bits = PixelUtil.pixels_to_bits(pixels, @rows, @cols, @rotation, @color_map_accent) + + state |> set_reset(0) |> sleep(500) |> set_reset(1) |> sleep(500) + state |> write_command(@cmd_soft_reset) |> sleep(1000) + + case pre_update(state, push_policy) do + :cont -> do_update(state, state.display, border, black_bits, accent_bits) + :halt -> {:error, :device_busy} + end + end + + # + # procedures + # + + @spec pre_update(State.t(), :await | :once) :: :cont | :halt + defp pre_update(state, :await) do + await_device(state) + :cont + end + + defp pre_update(state, :once) do + case read_busy(state) do + 0 -> :cont + 1 -> :halt + end + end + + @spec do_update(State.t(), Inky.Display.t(), atom(), binary(), binary()) :: :ok + defp do_update(state, _display, border, black_bits, accent_bits) do + state + |> write_command(@cmd_set_driver_output, [@rows - 1, (@rows - 1) >>> 8, 0x00]) + |> write_command(@cmd_set_dummy_line_period, [0x1B]) + |> write_command(@cmd_set_gate_line_width, [0x0B]) + |> write_command(@cmd_set_data_entry_mode, [0x03]) + |> write_command(@cmd_set_ram_x_position, [0x00, div(@cols, 8) - 1]) + |> write_command(@cmd_set_ram_y_position, [0x00, 0x00, @rows - 1, (@rows - 1) >>> 8]) + |> write_command(@cmd_write_vcom, [0x70]) + |> write_command(@cmd_write_lut, @lut_data) + |> set_border_color(border) + |> write_command(@cmd_set_ram_x_address, [0x00]) + |> write_command(@cmd_set_ram_y_address, [0x00, 0x00]) + |> write_command(@cmd_write_ram, black_bits) + |> write_command(@cmd_write_alt_ram, accent_bits) + |> await_device() + |> write_command(@cmd_activate_display_update_sequence) + + :ok + end + + @spec set_border_color(State.t(), atom()) :: State.t() + defp set_border_color(state, border) do + accent = state.display.accent + + cond do + # GS Transition + Waveform 00 + GSA 0 + GSB 0 + border == :black -> + write_command(state, @cmd_set_border_waveform, 0b00000000) + + # GS Transition + Waveform 01 + GSA 1 + GSB 0 + border in [:red, :accent] and accent == :red -> + write_command(state, @cmd_set_border_waveform, 0b00000110) + + # GS Transition + Waveform 11 + GSA 1 + GSB 1 + border in [:yellow, :accent] and accent == :yellow -> + write_command(state, @cmd_set_border_waveform, 0b00001111) + + # GS Transition + Waveform 00 + GSA 0 + GSB 1 + border == :white -> + write_command(state, @cmd_set_border_waveform, 0b00000001) + + true -> + raise ArgumentError, + message: "Invalid border #{inspect(border)} provided. Accent was #{inspect(accent)}" + end + end + + # + # waiting + # + + @spec await_device(State.t()) :: State.t() + defp await_device(state) do + case read_busy(state) do + 1 -> state |> sleep(10) |> await_device() + 0 -> state + end + end + + # + # pipe-able wrappers + # + + defp sleep(state, sleep_time) do + io_call(state, :handle_sleep, [sleep_time]) + state + end + + defp set_reset(state, value) do + io_call(state, :handle_reset, [value]) + state + end + + defp read_busy(state) do + io_call(state, :handle_read_busy) + end + + defp write_command(state, command) do + io_call(state, :handle_command, [command]) + state + end + + defp write_command(state, command, data) do + io_call(state, :handle_command, [command, data]) + state + end + + # + # Behaviour dispatching + # + + # Dispatch to the IO callback module that's held in state, using the previously obtained state + defp io_call(state, op, args \\ []) do + apply(state.io_mod, op, [state.io_state | args]) + end +end diff --git a/lib/hal/impression/rpihal.ex b/lib/hal/impression/rpihal.ex new file mode 100644 index 0000000..fb58958 --- /dev/null +++ b/lib/hal/impression/rpihal.ex @@ -0,0 +1,516 @@ +defmodule Inky.Impression.RpiHAL do + @default_io_mod Inky.Impression.RpiIO + + @moduledoc """ + An `Inky.HAL` implementation responsible for sending commands to the Inky + screen. It delegates to whatever IO module its user provides at init, but + defaults to #{inspect(@default_io_mod)} + """ + + @behaviour Inky.HAL + + @colors %{ + :black => 0, + :white => 1, + :green => 2, + :blue => 3, + :red => 4, + :yellow => 5, + :orange => 6, + # Not a color, but is used to clear the display + :clean => 7 + } + + # PANEL SETTING + @psr 0x00 + # POWER SETTING + @pwr 0x01 + # POWER OFF + @pof 0x02 + # POWER OFF SEQUENCE SETTING + @pfs 0x03 + # POWER ON + @pon 0x04 + @btst 0x06 + @dslp 0x07 + # DATA START TRANSMISSION 1 + @dtm1 0x10 + # TODO: Why are we not using the data stop command? + # DATA STOP + @dsp 0x11 + # DISPLAY REFRESH + @drf 0x12 + @ipc 0x13 + # PLL (Phased Lock Loop) CONTROL + # https://en.wikipedia.org/wiki/Phase-locked_loop + @pll 0x30 + # TEMPERATURE SENSOR CALIBRATION + # This command reads the temperature sensed by the temperature sensor. + @tsc 0x40 + # TEMPERATURE SENSOR CALIBRATION + # This command selects Internal or External temperature sensor. + @tse 0x41 + @tws 0x42 + @tsr 0x43 + # VCOM AND DATA INTERVAL SETTING + # This command indicates the interval of Vcom and data output. When setting + # the vertical back porch, the total blanking will be kept (20 Hsync). + @cdi 0x50 + # LOW POWER DETECTION + # This command indicates the input power condition. Host can read this flag to learn the battery condition. + @lpd 0x51 + # TCON SETTING + # This command defines non-overlap period of Gate and Source. + @tcon 0x60 + # RESOLUTION SETTING (TRES) + # This command defines alternative resolution and this setting is of higher priority than the RES[1:0] in R00H (PSR). + @tres 0x61 + # SPI FLASH CONTROL + # This command defines MCU host direct access external memory mode. + # This might allow us to specify our own lookup tables! Which might mean our own colors! + @dam 0x65 + # REVISION + # The REV is read from OTP address = 0x001 + @rev 0x70 + # GET STATUS + # This command reads the IC status. + # I2C? + @flg 0x71 + # AUTO MEASURE VCOM + # This command reads the IC status. + @amv 0x80 + # VCOM VALUE + # This command gets the Vcom value. + @vv 0x81 + # VCM_DC SETTING + # This command sets VCOM_DC value. + @vdcs 0x82 + # WARN: Not found in datasheet + # python driver calls it "UC8159_7C" + @pws 0xE3 + @tsset 0xE5 + + require Logger + + alias Inky.Display + alias Inky.HAL + alias Inky.PixelUtil + + defmodule State do + @moduledoc false + + @state_fields [:display, :io_mod, :io_state, :setup?] + + @enforce_keys @state_fields + defstruct @state_fields + end + + # + # API + # + + @impl HAL + def init(args) do + display = args[:display] || raise(ArgumentError, message: ":display missing in args") + io_mod = args[:io_mod] || @default_io_mod + + io_args = args[:io_args] || [] + io_args = if :gpio_mod in io_args, do: io_args, else: [gpio_mod: Circuits.GPIO] ++ io_args + io_args = if :spi_mod in io_args, do: io_args, else: [spi_mod: Circuits.SPI] ++ io_args + + %State{ + display: display, + io_mod: io_mod, + io_state: io_mod.init(io_args), + setup?: false + } + end + + @impl HAL + def handle_update(pixels, border, push_policy, state = %State{}) do + display = %Display{width: w, height: h, rotation: r} = state.display + Logger.info("display: #{inspect(display)}") + IO.puts("Generating buffer2...") + + # I think this buffer is being generated incorrectly + # The resolution isn't right + # Also only the black is actually solid, it's like black is being interspersed with the other colors + # In the python library this is a uint8 which is an unsigned 8-bit integer, + # which should be similar to a byte in elixir... + # buffer = gen_buf() + + # 600 x 448 + # w = 1200 + # h = 224 + + # In order to draw on row/colums in the way we expect we should treat the display as if it was 300x896 + w = 300 + h = 896 + + buffer = + for y <- 0..(h - 1), x <- 0..(w - 1), into: <<>> do + cond do + x > 150 && y > 224 -> <<@colors[:green]::4, @colors[:green]::4>> + # y > 20 -> <<@colors[:green]::4, @colors[:green]::4>> + x < 100 -> <<@colors[:blue]::4, @colors[:blue]::4>> + # x > 160 -> <<@colors[:black]>> + # x > 160 -> <<0::4, @colors[:red]::4>> + # y > 115 -> <<@colors[:green]>> + true -> <<@colors[:orange]::4, @colors[:orange]::4>> + # true -> <> + end + # color = floor(0 / w * 7) + + # <<@colors[:yellow]>> + # <<@colors[:green]>> + end + # buffer = dup_bytes(buffer) + + # buffer = + # for y <- 0..(div(h, 3) - 1), x <- 0..(div(w, 3) - 1), into: <<>> do + # cond do + # x > 180 && y > 200 -> <<@colors[:yellow]::4, @colors[:yellow]::4>> + # # x > 160 -> <<@colors[:black]>> + # x > 160 -> <<@colors[:red]::4, @colors[:red]::4>> + # # y > 115 -> <<@colors[:green]>> + # true -> <<@colors[:green]::4, @colors[:green]::4>> + # # true -> <> + # end + # # color = floor(0 / w * 7) + + # <<@colors[:blue]::4, @colors[:blue]::4>> + # # <<7::4,7::4>> + # end + + # buffer = [buffer | [buffer2]] + log("Generated buffer of size: #{IO.iodata_length(buffer)}") + + log("Resetting device") + reset(state) + + case pre_update(state, push_policy) do + :cont -> do_update(state, display, border, buffer) + :halt -> {:error, :device_busy} + end + end + + def pack_bytes(buffer) do + # TODO + end + + def dup_bytes(buffer) do + for << <<_upper::4, byte::4>> <- buffer>>, into: <<>> do + <> + end + end + + def gen_buf do + IO.puts("Start generating buffer") + w = 600 + h = 448 + + buffer = + for y <- 0..(h - 1), x <- 0..(w - 1), into: <<>> do + cond do + #x > 150 && y > 200 -> <<@colors[:orange]>> + # x > 160 -> <<@colors[:black]>> + # y > 115 -> <<@colors[:green]>> + x > 300 -> <<@colors[:black]>> + y > 224 -> <<@colors[:green]>> + # true -> <> + true -> <<@colors[:orange]>> + end + # color = floor(0 / w * 7) + end + + log("Generated buffer of size: #{byte_size(buffer)}") + log("buffer: #{inspect(buffer)}") + + buffer = prep_buffer(buffer) + + log("Generated buffer of size: #{byte_size(buffer)}") + log("buffer: #{inspect(buffer)}") + buf_list = :binary.bin_to_list(buffer) + |> Enum.chunk_every(448) + |> IO.inspect(label: "buf_list (rpihal.ex:150)") + # IO.inspect(buffer, label: "buffer (rpihal.ex:149)", limit: :infinity) + buf_list + end + + defp prep_buffer(buffer) do + import Bitwise + + buf_list = :binary.bin_to_list(buffer) + + # Python + # buf = ((buf[::2] << 4) & 0xF0) | (buf[1::2] & 0x0F) + # Takes every second byte of `buf`, multiplies it by 2^4 (16) and bytewise ands it with 0xF0 + # 2 << 4 is equivalent to Bitwise.<<<(2, 4) + # 17 & 0xF0 is equivalent to Bitwise.&&&(17, 0xF0) + # 16 | 0xF is equivalent to Bitwise.|||(16, 0xF) + # buf[::2] takes every second byte (starting at index 0) + # buf[1::2] takes every second byte (starting at index 1) + + # buf[::2] and buf[1::2] + IO.puts("before split") + {buf_a, buf_b} = split_buf_list(buf_list) + IO.puts("after split") + IO.inspect(buf_a, label: "buf_a (rpihal.ex:168)") + IO.inspect(buf_b, label: "buf_b (rpihal.ex:169)") + + # (buf[::2] << 4) & 0xF0 + # high_bytes = (buf_a <<< 4) &&& 0xF0 + + high_bytes = for <>, into: <<>> do + <<(byte <<< 4) &&& 0xF0>> + end + IO.inspect(high_bytes, label: "high_bytes (rpihal.ex:180)") + + # (buf[1::2] & 0x0F) + low_bytes = for <>, into: <<>> do + <> + end + IO.inspect(low_bytes, label: "low_bytes (rpihal.ex:186)") + + + # buf = ((buf[::2] << 4) & 0xF0) | (buf[1::2] & 0x0F) + binary_bitwise_or(high_bytes, low_bytes) + |> :binary.list_to_bin() + + # ((buf[::2] << 4) & 0xF0) | (buf[1::2] & 0x0F) + # (| + # (& ) + # (buf[::2] << 4) & 0xF0) + # (buf[1::2] & 0x0F) + + # buf = ((buf[::2] << 4) & 0xF0) | (buf[1::2] & 0x0F) + end + + def bitwise_map(bin, op) when is_binary(bin) do + IO.inspect(bin, label: "bin (rpihal.ex:201)") + IO.inspect(op, label: "op (rpihal.ex:202)") + for <>, into: <<>> do + <> + end + end + + def binary_bitwise_or(<<>>, <<>>), do: [] + def binary_bitwise_or(<>, <>) do + import Bitwise + byte = a ||| b + [byte | binary_bitwise_or(rest_a, rest_b)] + end + + def split_buf_list(buf_list) do + {list_a, list_b} = + buf_list + |> Enum.with_index() + |> Enum.split_with(fn {_val, index} -> rem(index, 2) == 0 end) + + {list_a, _indexes} = Enum.unzip(list_a) + {list_b, _indexes} = Enum.unzip(list_b) + + {:binary.list_to_bin(list_a), :binary.list_to_bin(list_b)} + end + + # + # procedures + # + + defp pre_update(state, :await) do + await_device(state) + :cont + end + + defp pre_update(state, :once) do + case read_busy(state) do + 1 -> :cont + 0 -> :halt + end + end + + defp log(msg) when is_binary(msg) do + IO.puts(msg) + Logger.info(msg) + end + + defp log(state, msg) when is_binary(msg) do + IO.puts(msg) + Logger.info(msg) + state + end + + defp do_update(state, display, border, buffer) do + Logger.info("border: #{inspect(border)}") + border = :red + + state + |> log("setting resolution") + |> set_resolution(display.packed_resolution) + |> log("setting panel") + |> set_panel() + |> log("setting power") + |> set_power() + |> log("setting pll") + |> set_pll_clock_frequency() + |> log("set tse register") + |> set_tse_register() + |> log("set vcom") + |> set_vcom_data_interval_setting(border) + |> log("set gate") + |> set_gate_source_non_overlap_period() + |> log("disable external flash") + |> disable_external_flash() + |> log("set pws") + |> set_pws_whatever_that_means() + |> log("power off seq") + |> power_off_sequence() + |> log("push pixels") + |> push_pixel_buffer(buffer) + |> log("await") + |> await_device() + |> log("pon") + |> pon() + |> log("await") + |> await_device() + |> log("drf") + |> drf() + |> log("await") + |> await_device() + |> log("pof") + |> pof() + |> log("await") + |> await_device() + |> log("done") + + {:ok, %State{state | setup?: true}} + end + + # + # "routines" and serial commands + # + + defp reset(state) do + state + |> set_reset(0) + |> sleep(100) + |> set_reset(1) + |> sleep(100) + end + + defp soft_reset(state), do: write_command(state, 0x12) + + # >HH struct.pack, so big-endian, unsigned-sort * 2 + defp set_resolution(state, packed_resolution), + do: write_command(state, @tres, packed_resolution) + + # Panel Setting + # 0b11000000 = Resolution select, 0b00 = 640x480, our panel is 0b11 = 600x448 + # 0b00100000 = LUT selection, 0 = ext flash, 1 = registers, we use ext flash + # 0b00010000 = Ignore + # 0b00001000 = Gate scan direction, 0 = down, 1 = up (default) + # 0b00000100 = Source shift direction, 0 = left, 1 = right (default) + # 0b00000010 = DC-DC converter, 0 = off, 1 = on + # 0b00000001 = Soft reset, 0 = Reset, 1 = Normal (Default) + defp set_panel(state), do: write_command(state, @psr, [0b11101111, 0x08]) + + defp set_power(state), + do: + write_command(state, @pwr, [ + Bitwise.bor( + Bitwise.bor( + Bitwise.bor( + # ??? - not documented in UC8159 datasheet + Bitwise.<<<(0x06, 3), + # SOURCE_INTERNAL_DC_DC + Bitwise.<<<(0x01, 2) + ), + # GATE_INTERNAL_DC_DC + Bitwise.<<<(0x01, 1) + ), + # LV_SOURCE_INTERNAL_DC_DC + 0x01 + ), + # VGx_20V + 0x00, + # UC8159_7C + 0x23, + # UC8159_7C + 0x23 + ]) + + # Set the PLL clock frequency to 50Hz + # 0b11000000 = Ignore + # 0b00111000 = M + # 0b00000111 = N + # PLL = 2MHz * (M / N) + # PLL = 2MHz * (7 / 4) + # PLL = 2,800,000 ??? + defp set_pll_clock_frequency(state), do: write_command(state, 0x3C) + + defp set_tse_register(state), do: write_command(state, 0x00) + + defp set_vcom_data_interval_setting(state, border), + do: write_command(state, @cdi, [Bitwise.bor(Bitwise.<<<(@colors[border], 5), 0x17)]) + + defp set_gate_source_non_overlap_period(state), do: write_command(state, @tcon, 0x22) + defp disable_external_flash(state), do: write_command(state, @dam, 0x00) + defp set_pws_whatever_that_means(state), do: write_command(state, @pws, 0xAA) + defp power_off_sequence(state), do: write_command(state, @pfs, 0x00) + defp push_pixel_buffer(state, buffer), do: write_command(state, @dtm1, buffer) + defp pon(state), do: write_command(state, @pon) + defp drf(state), do: write_command(state, @drf) + defp pof(state), do: write_command(state, @pof) + + # + # waiting + # + + defp await_device(state) do + case read_busy(state) do + 0 -> + sleep(state, 10) + await_device(state) + + 1 -> + state + end + end + + # + # pipe-able wrappers + # + + defp sleep(state, sleep_time) do + io_call(state, :handle_sleep, [sleep_time]) + state + end + + defp set_reset(state, value) do + io_call(state, :handle_reset, [value]) + state + end + + defp read_busy(state) do + io_call(state, :handle_read_busy) + end + + defp write_command(state, command) do + io_call(state, :handle_command, [command]) + state + end + + defp write_command(state, command, data) do + io_call(state, :handle_command, [command, data]) + state + end + + # + # Behaviour dispatching + # + + # Dispatch to the IO callback module that's held in state, using the previously obtained state + defp io_call(state, op, args \\ []) do + apply(state.io_mod, op, [state.io_state | args]) + end +end diff --git a/lib/hal/impression/rpiio.ex b/lib/hal/impression/rpiio.ex new file mode 100644 index 0000000..5d96a32 --- /dev/null +++ b/lib/hal/impression/rpiio.ex @@ -0,0 +1,169 @@ +defmodule Inky.Impression.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, + # The python library uses a CS pin but we haven't been able to use pin 8 as a CS pin + # :cs_pid + ] + + @enforce_keys @state_fields + defstruct @state_fields + end + + + @default_palette [ + [57, 48, 57], + [255, 255, 255], + [58, 91, 70], + [61, 59, 94], + [156, 72, 75], + [208, 190, 71], + [177, 106, 73], + [255, 255, 255] + ] + + @reset_pin 27 + @busy_pin 17 + @dc_pin 22 + @cs0_pin 8 + + @default_pin_mappings %{ + busy_pin: @busy_pin, + cs0_pin: @cs0_pin, + spi: 0, + dc_pin: @dc_pin, + reset_pin: @reset_pin + } + + @spi_speed_hz 3000000 + @spi_command 0 + @spi_data 1 + @spi_chunk_bytes 4096 + + @resolution %{ + {600, 448} => {600, 448, 0, 0, 0} + } + + @border :white + + # 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[:spi]) + + IO.inspect(pin_mappings) + # MAYBE_DO: Open CS pin + IO.puts("opening DC pin") + {:ok, dc_pid} = gpio.open(pin_mappings[:dc_pin], :output, initial_value: 0) + IO.puts("opening reset pin") + {:ok, reset_pid} = gpio.open(pin_mappings[:reset_pin], :output, initial_value: 1) + IO.puts("opening busy pin") + {:ok, busy_pid} = gpio.open(pin_mappings[:busy_pin], :input) + IO.puts("opening SPI device") + {: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 + } + |> IO.inspect(label: "init complete") + 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 + # MAYBE_DO: Write a 0 to CS pin + :ok = gpio_call(state, :write, [state.dc_pid, data_or_command]) + + result = case spi_call(state, :transfer, [state.spi_pid, value]) do + {:ok, response} -> {:ok, response} + {:error, :transfer_failed} -> spi_call_chunked(state, value) + end + # MAYBE_DO: Write a 1 to CS pin + 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 diff --git a/lib/inky.ex b/lib/inky.ex index 719f588..8c8d7ca 100644 --- a/lib/inky.ex +++ b/lib/inky.ex @@ -10,6 +10,7 @@ defmodule Inky do alias Inky.Display alias Inky.RpiHAL + alias Inky.Impression.RpiHAL, as: ImpressionHAL @typedoc "The Inky process name" @type name :: atom | {:global, term} | {:via, module, term} @@ -33,6 +34,21 @@ defmodule Inky do # # API # + def go, do: 45 + + def impress do + IO.puts("Starting impression") + {:ok, pid} = Inky.start_link(:impression, name: Inky.Foo) + IO.puts("Started... #{inspect(pid)}") + + IO.puts("Seting pixels...") + Inky.set_pixels(Inky.Foo, %{}) + end + + def start_link(type, opts) when is_list(opts) do + genserver_opts = if(opts[:name], do: [name: opts[:name]], else: []) + GenServer.start_link(__MODULE__, [type, opts], genserver_opts) + end @doc """ Start an Inky GenServer for a display of type `type`, with the color `accent` @@ -141,7 +157,12 @@ defmodule Inky do @impl GenServer def init([type, accent, opts]) do border = opts[:border] || @default_border - hal_mod = opts[:hal_mod] || RpiHAL + + hal_mod = + case type do + :phat_ssd1608 -> opts[:hal_mod] || Inky.HAL.PhatSSD1608 + _ -> opts[:hal_mod] || RpiHAL + end display = Display.spec_for(type, accent) hal_state = hal_mod.init(%{display: display}) @@ -155,6 +176,23 @@ defmodule Inky do }} end + @impl GenServer + def init([:impression = type, opts]) do + border = opts[:border] || @default_border + hal_mod = opts[:hal_mod] || ImpressionHAL + + display = Display.spec_for(type) + hal_state = hal_mod.init(%{display: display}) + + {:ok, + %State{ + border: border, + display: display, + hal_mod: hal_mod, + hal_state: hal_state + }} + end + # GenServer calls @impl GenServer @@ -173,7 +211,16 @@ defmodule Inky do end def handle_call(:push, _from, state) do - {:reply, push(:await, state), state} + result = push(:await, state) + + state = + case result do + :ok -> state + {:error, _} -> state + {:ok, state} -> state + end + + {:reply, result, state} end def handle_call(request, from, state) do @@ -185,7 +232,13 @@ defmodule Inky do @impl GenServer def handle_cast(:push, state) do - push(:await, state) + state = + case push(:await, state) do + :ok -> state + {:error, _} -> state + {:ok, state} -> state + end + {:noreply, state} end @@ -280,7 +333,7 @@ defmodule Inky do # Internals - defp push(push_policy, state) when not (push_policy in [:await, :once]), do: push(:await, state) + defp push(push_policy, state) when push_policy not in [:await, :once], do: push(:await, state) defp push(push_policy, state) do hm = state.hal_mod diff --git a/mix.exs b/mix.exs index 62b8168..9f055d7 100644 --- a/mix.exs +++ b/mix.exs @@ -24,8 +24,9 @@ defmodule Inky.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + # {:circuits_gpio, "~> 1.0"}, {:circuits_gpio, "~> 0.4"}, - {:circuits_spi, "~> 0.1"}, + {:circuits_spi, "~> 1.0"}, {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev} ] diff --git a/mix.lock b/mix.lock index b9f884a..d2a920b 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,11 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "circuits_gpio": {:hex, :circuits_gpio, "0.4.1", "344dd34f2517687fd28723e5552571babff5469db05b697181cab860fe7eff23", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8031c53c1813f52ccd2926c5e806d192ec5625cb12c3a2bf6b63fecd34999010"}, - "circuits_spi": {:hex, :circuits_spi, "0.1.3", "a94889abc874e9976f397c649152776d9c0863e5fd3377203c7f0cf992d9609c", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "09569214ccdff871149dc5d738a20206647b60769a919b5bb1a6a7f843a10f66"}, + "circuits_gpio": {:hex, :circuits_gpio, "0.4.8", "75a62b07131f119c0ffa1dd49f79fbe29c46fd39b20ef0acc036d449fb780b89", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "05e341a5de7e9181a0ee6b3281bd2dc278ae4651825e95c9e83c9eabc421448e"}, + "circuits_i2c": {:hex, :circuits_i2c, "0.3.4", "d86951092e44487fe6fecbd6544511d894510be2856280eb93258a171bdc1552", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3845f9f8140a791b7db0b103899e88c58e437295a76309bb5e4bdab216209955"}, + "circuits_spi": {:hex, :circuits_spi, "1.0.0", "ecc0db489048799f7a49fcf3fd1ff5fddc3c1d9faaea5bc01dfe3189d66b7fc3", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b119f4f41c8b1ab51ff1bc7f2ffe556ebc14619c32fd39ab64efdf5f25d8a1d8"}, "credo": {:hex, :credo, "1.0.5", "fdea745579f8845315fe6a3b43e2f9f8866839cfbc8562bb72778e9fdaa94214", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "16105fac37c5c4b3f6e1f70ba0784511fec4275cd8bb979386e3c739cf4e6455"}, "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"}, - "elixir_make": {:hex, :elixir_make, "0.5.2", "96a28c79f5b8d34879cd95ebc04d2a0d678cfbbd3e74c43cb63a76adf0ee8054", [:mix], [], "hexpm", "382eeea8e02dfe6c468f6729b6cf20fe5b14390671d38c7363e59621c7ab4efc"}, + "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "8e24fc8ff9a50b9f557ff020d6c91a03cded7e59ac3e0eec8a27e771430c7d27"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, diff --git a/test/eeprom_test.exs b/test/eeprom_test.exs new file mode 100644 index 0000000..6a6a20b --- /dev/null +++ b/test/eeprom_test.exs @@ -0,0 +1,40 @@ +defmodule Inky.EEPROMTest do + @moduledoc false + + use ExUnit.Case + import Inky.EEPROM + + describe "parse_data/1" do + test "returns correct struct for Inky pHAT SSD1608" do + inky_ssd1608_eeprom = + <<212, 0, 104, 0, 1, 12, 10, 21, 50, 48, 50, 49, 45, 48, 55, 45, 49, 50, 32, 49, 48, 58, + 49, 49, 58, 52, 57, 46, 56>> + + assert {:ok, + %Inky.EEPROM{ + color: :black, + display_variant: "Black pHAT (SSD1608)", + height: 104, + pcb_variant: 12, + timestamp: "2021-07-12 10:11:49.8", + width: 212 + }} = parse_data(inky_ssd1608_eeprom) + end + + test "returns correct struct for Inky Impression" do + inky_impression_eeprom = + <<88, 2, 192, 1, 0, 12, 14, 21, 50, 48, 50, 49, 45, 48, 56, 45, 50, 53, 32, 49, 55, 58, + 49, 50, 58, 49, 48, 46, 51>> + + assert {:ok, + %Inky.EEPROM{ + color: :unknown, + display_variant: "7-Color (UC8159)", + height: 448, + pcb_variant: 12, + timestamp: "2021-08-25 17:12:10.3", + width: 600 + }} = parse_data(inky_impression_eeprom) + end + end +end