diff --git a/README.md b/README.md index 98528ef..bba600f 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ This is a port of Pimoroni's [python Inky library](https://github.com/pimoroni/inky) written in Elixir. This library is -intended to support both Inky pHAT and wHAT, but since we only have pHATs, the -wHAT support may not be fully functional. +intended to support the Inky pHAT and Inky Impression. Eventually it will support the Inky wHAT as well but it is not currently supported. See the [API reference](https://hexdocs.pm/inky/api-reference.html) for details on how to use the functionality provided. @@ -19,15 +18,15 @@ can not see results without using a physical device. ### Scenic Driver -A [basic driver](https://github.com/pappersverk/scenic_driver_inky) for scenic -is in the works, check it out, to follow how it is progressing. +There is [basic driver](https://github.com/pappersverk/scenic_driver_inky) for use with +[scenic](https://github.com/ScenicFramework/scenic). ## Getting started Inky is available on Hex. Add inky to your mix.exs deps: ```elixir -{:inky, "~> 1.0.1"}, +{:inky, "~> 1.0.2"}, ``` Run `mix deps.get` to get the new dep. @@ -46,7 +45,8 @@ config in init, adjust accordingly): ```elixir # Start your Inky process ... -{:ok, pid} = Inky.start_link(:phat, :red, %{name: InkySample}) +type = :phat_il91874 +{:ok, pid} = Inky.start_link(type, accent: :red, name: InkySample) painter = fn x, y, w, h, _pixels_so_far -> wh = w / 2 @@ -65,3 +65,12 @@ Inky.set_pixels(InkySample, painter, border: :white) # Flip a few pixels Inky.set_pixels(pid, %{{0,0}: :black, {3,49}: :red, {23, 4}: white}) ``` + +## Figuring out the value to pass for `type` + +- Inky pHAT ordered roughly before 2019 -> `:phat_il91874` +- Inky pHAT ordered roughly after 2019 -> `:phat_ssd1608` +- Inky Impression 4" -> `:impression_4` +- Inky Impression 5.7" -> `:impression_5_7` +- Inky Impression 7.3" -> `:impression_7_3` +- Inky wHAT: **Not currently supported** diff --git a/config/config.exs b/config/config.exs index 6ba0b54..66c2fc2 100644 --- a/config/config.exs +++ b/config/config.exs @@ -3,4 +3,4 @@ # # This configuration file is loaded before any dependency and # is restricted to this project. -use Mix.Config +import Config diff --git a/lib/display/display.ex b/lib/display/display.ex index dec2385..cda3ee4 100644 --- a/lib/display/display.ex +++ b/lib/display/display.ex @@ -12,13 +12,68 @@ defmodule Inky.Display do width: 0, height: 0, packed_dimensions: %{}, + packed_resolution: nil, rotation: 0, accent: :black, luts: <<>> - @spec spec_for(:phat_ssd1608 | :phat | :what, :black | :red | :yellow) :: Inky.Display.t() - def spec_for(type, accent \\ :black) + @spec spec_for(:impression_7_3, :none) :: Inky.Display.t() + def spec_for(type = :impression_7_3, :none) do + width = 800 + height = 480 + %__MODULE__{ + type: type, + width: width, + height: height, + packed_dimensions: %{}, + packed_resolution: + <> <> <>, + rotation: 0, + accent: nil, + luts: <<>> + } + end + + @spec spec_for(:impression_5_7, :none) :: Inky.Display.t() + def spec_for(type = :impression_5_7, :none) do + width = 600 + height = 448 + + %__MODULE__{ + type: type, + width: width, + height: height, + packed_dimensions: %{}, + packed_resolution: + <> <> <>, + rotation: 0, + accent: nil, + luts: <<>> + } + end + + # WARNING: This is untested on actual hardware + @spec spec_for(:impression_4, :none) :: Inky.Display.t() + def spec_for(type = :impression_4, :none) do + width = 640 + height = 400 + + %__MODULE__{ + type: type, + width: width, + height: height, + packed_dimensions: %{}, + packed_resolution: + <> <> <>, + rotation: 0, + accent: nil, + luts: <<>> + } + end + + @spec spec_for(:phat_il91874 | :phat_ssd1608 | :what, :black | :red | :yellow) :: + Inky.Display.t() def spec_for(type = :phat_ssd1608, accent) do # Keep it minimal. Details are specified in `Inky.HAL.PhatSSD1608`. %__MODULE__{ @@ -32,7 +87,7 @@ defmodule Inky.Display do } end - def spec_for(type = :phat, accent) do + def spec_for(type = :phat_il91874, accent) do %__MODULE__{ type: type, width: 212, @@ -78,7 +133,7 @@ defmodule Inky.Display do columns = case type do :what -> width - :phat -> height + :phat_il91874 -> height :test_small -> height end @@ -89,7 +144,7 @@ defmodule Inky.Display do rows = case type do :what -> height - :phat -> width + :phat_il91874 -> width :test_small -> width end diff --git a/lib/display/pixelutil.ex b/lib/display/pixelutil.ex index 05b1646..b2a6e43 100644 --- a/lib/display/pixelutil.ex +++ b/lib/display/pixelutil.ex @@ -3,7 +3,7 @@ defmodule Inky.PixelUtil do PixelUtil maps pixels to bitstrings to be sent to an Inky screen """ - def pixels_to_bits(pixels, width, height, rotation_degrees, color_map) do + def pixels_to_bits(pixels, width, height, rotation_degrees, color_map, bit_count \\ 1) do {outer_axis, dimension_vectors} = rotation_degrees |> normalised_rotation() @@ -13,7 +13,8 @@ defmodule Inky.PixelUtil do |> rotated_ranges(width, height) |> do_pixels_to_bits( &pixels[pixel_key(outer_axis, &1, &2)], - &(color_map[&1] || color_map.miss) + &(color_map[&1] || color_map.miss), + bit_count ) end @@ -61,10 +62,10 @@ defmodule Inky.PixelUtil do defp rotated_dimension(_width, height, {:y, 1}), do: 0..(height - 1) defp rotated_dimension(_width, height, {:y, -1}), do: (height - 1)..0 - defp do_pixels_to_bits({i_range, j_range}, pixel_picker, cmap) do + defp do_pixels_to_bits({i_range, j_range}, pixel_picker, cmap, bit_count) do for i <- i_range, j <- j_range, - do: <>, + do: <>, into: <<>> end diff --git a/lib/hal/hal_impression_ac073tc1a.ex b/lib/hal/hal_impression_ac073tc1a.ex new file mode 100644 index 0000000..55e8167 --- /dev/null +++ b/lib/hal/hal_impression_ac073tc1a.ex @@ -0,0 +1,309 @@ +defmodule Inky.HAL.ImpressionAC073TC1A do + @default_io_mod Inky.IO.Impression + + @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 + + @color_map %{black: 0, white: 1, green: 2, blue: 3, red: 4, yellow: 5, orange: 6, miss: 1} + # @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 + # :clear => 7 + # } + + # PANEL SETTING + @psr 0x00 + # POWER SETTING + @pwr 0x01 + # POWER OFF + @pof 0x02 + # POWER OFF SEQUENCE SETTING + @pofs 0x03 + # POWER ON + @pon 0x04 + + # BTST1 + @btst1 0x05 + @btst2 0x06 + # @dslp 0x07 + @btst3 0x08 + + # DATA START TRANSMISSION 1 + @dtm 0x10 + # DISPLAY REFRESH + @drf 0x12 + + #? + @ipc 0x13 + + #? + @tse 0x41 + + # 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 + # 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 + + # New in impression_7_3 + @vdcs 0x82 + @t_vdcs 0x84 + @agid 0x86 + @cmdh 0xAA + # @ccset 0xE0 + + @ccset 0xE0 + @pws 0xE3 + @tsset 0xE6 + + 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 + Logger.info("push_policy: #{inspect(push_policy, pretty: true)}") + display = %Display{width: w, height: h, rotation: r} = state.display + buffer = PixelUtil.pixels_to_bits(pixels, w, h, r, @color_map, 4) + reset(state) + + case pre_update(state, push_policy) do + :cont -> do_update(state, display, border, buffer) + :halt -> {:error, :device_busy} + end + 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 do_update(state, _display, _border, buffer) do + state + |> set_cmdh() + |> set_power_pwr() + |> set_panel_psr() + |> power_off_sequence_pofs() + |> write_command(@btst1, [0x40, 0x1F, 0x1F, 0x2C]) + |> write_command(@btst2, [0x6F, 0x1F, 0x16, 0x25]) + |> write_command(@btst3, [0x6F, 0x1F, 0x1F, 0x22]) + |> write_command(@ipc, [0x00, 0x04]) + |> set_pll_clock_frequency() + |> write_command(@tse, [0x00]) + # border? + # Could be combined with `set_vcom_data_interval_setting`? + |> write_command(@cdi, [0x3F]) + |> write_command(@tcon, [0x02, 0x00]) + # resolution? + |> write_command(@tres, [0x03, 0x20, 0x01, 0xE0]) + # |> set_resolution(display.packed_resolution) + # cont + |> write_command(@vdcs, [0x1E]) + |> write_command(@t_vdcs, [0x00]) + |> write_command(@agid, [0x00]) + |> write_command(@pws, [0x2F]) + |> write_command(@ccset, [0x00]) + |> write_command(@tsset, [0x00]) + + # End of setup + # Need to force white somehow? + # The python driver is doing some bit manipulation before this call + # https://github.com/pimoroni/inky/blob/98383c5d47928b90ee3951ed72576b7064e573e7/library/inky/inky_ac073tc1a.py#L300 + |> push_pixel_buffer(buffer) + |> pon() + |> await_device() + |> drf() + |> await_device() + |> pof() + |> await_device() + + + # |> set_vcom_data_interval_setting(border) + # |> set_gate_source_non_overlap_period() + # |> disable_external_flash() + # |> set_pws_whatever_that_means() + # |> power_off_sequence_pofs() + # |> push_pixel_buffer(buffer) + # |> await_device() + # |> pon() + # |> await_device() + # |> drf() + # |> await_device() + # |> pof() + # |> await_device() + + {: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) + |> set_reset(0) + |> sleep(100) + |> set_reset(1) + |> sleep(100) + # busy wait? + end + + # >HH struct.pack, so big-endian, unsigned-short * 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_psr(state), do: write_command(state, @psr, [0x5F, 0x69]) + + defp set_cmdh(state), do: write_command(state, @cmdh, [0x49, 0x55, 0x20, 0x08, 0x09, 0x18]) + + defp set_power_pwr(state), do: write_command(state, @pwr, [0x3F, 0x00, 0x32, 0x2A, 0x0E, 0x2A]) + + # 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, [0x02]) + + # defp set_vcom_data_interval_setting(state, border), + # do: write_command(state, @cdi, [bor(@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_pofs(state), do: write_command(state, @pofs, [0x00, 0x54, 0x00, 0x44]) + defp push_pixel_buffer(state, buffer), do: write_command(state, @dtm, buffer) + defp pon(state), do: write_command(state, @pon) + defp drf(state), do: write_command(state, @drf, [0x00]) + defp pof(state), do: write_command(state, @pof, [0x00]) + + # + # waiting + # + + # busy_wait + 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/hal_impression_uc8159.ex b/lib/hal/hal_impression_uc8159.ex new file mode 100644 index 0000000..4410f89 --- /dev/null +++ b/lib/hal/hal_impression_uc8159.ex @@ -0,0 +1,264 @@ +defmodule Inky.HAL.ImpressionUC8159 do + import Bitwise + + @default_io_mod Inky.IO.Impression + + @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 + + @color_map %{black: 0, white: 1, green: 2, blue: 3, red: 4, yellow: 5, orange: 6, miss: 1} + @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 + :clear => 7 + } + + # PANEL SETTING + @psr 0x00 + # POWER SETTING + @pwr 0x01 + # POWER OFF + @pof 0x02 + # POWER OFF SEQUENCE SETTING + @pfs 0x03 + # POWER ON + @pon 0x04 + # DATA START TRANSMISSION 1 + @dtm1 0x10 + # DISPLAY REFRESH + @drf 0x12 + # 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 + # 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 + # WARN: Not found in datasheet + # python driver calls it "UC8159_7C" + @pws 0xE3 + + 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 + buffer = PixelUtil.pixels_to_bits(pixels, w, h, r, @color_map, 4) + reset(state) + + case pre_update(state, push_policy) do + :cont -> do_update(state, display, border, buffer) + :halt -> {:error, :device_busy} + end + 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 do_update(state, display, border, buffer) do + state + |> set_resolution(display.packed_resolution) + |> set_panel() + |> set_power() + |> set_pll_clock_frequency() + |> set_tse_register() + |> set_vcom_data_interval_setting(border) + |> set_gate_source_non_overlap_period() + |> disable_external_flash() + |> set_pws_whatever_that_means() + |> power_off_sequence() + |> push_pixel_buffer(buffer) + |> await_device() + |> pon() + |> await_device() + |> drf() + |> await_device() + |> pof() + |> await_device() + + {: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 + + # >HH struct.pack, so big-endian, unsigned-short * 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, [ + # ??? - not documented in UC8159 datasheet + 0x06 <<< 3 + # SOURCE_INTERNAL_DC_DC + |> bor(0x01 <<< 2) + # GATE_INTERNAL_DC_DC + |> bor(0x01 <<< 1) + # LV_SOURCE_INTERNAL_DC_DC + |> bor(0x01), + # VGx_20V + 0x00, + # 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, [bor(@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/rpihal.ex b/lib/hal/hal_phat_il91874.ex similarity index 96% rename from lib/hal/rpihal.ex rename to lib/hal/hal_phat_il91874.ex index f0e304e..a546d08 100644 --- a/lib/hal/rpihal.ex +++ b/lib/hal/hal_phat_il91874.ex @@ -1,10 +1,13 @@ -defmodule Inky.RpiHAL do - @default_io_mod Inky.RpiIO +defmodule Inky.HAL.PhatIL91874 do + @default_io_mod Inky.IO.Phat @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)} + + Specific to the IL91874 chip which was in the original batch of pHAT's: + https://github.com/pimoroni/inky-phat/blob/4c0ac9ffae25d2ee055f41f1d958c64b17f574bd/library/inkyphat/inky212x104.py#L2 """ @behaviour Inky.HAL diff --git a/lib/hal/hal_ssd1608.ex b/lib/hal/hal_phat_ssd1608.ex similarity index 98% rename from lib/hal/hal_ssd1608.ex rename to lib/hal/hal_phat_ssd1608.ex index dc9b794..dcf67ad 100644 --- a/lib/hal/hal_ssd1608.ex +++ b/lib/hal/hal_phat_ssd1608.ex @@ -1,5 +1,5 @@ defmodule Inky.HAL.PhatSSD1608 do - @default_io_mod Inky.RpiIO + @default_io_mod Inky.IO.Phat @moduledoc """ An `Inky.HAL` implementation responsible for sending commands to the Inky @@ -10,7 +10,7 @@ defmodule Inky.HAL.PhatSSD1608 do @behaviour Inky.HAL alias Inky.PixelUtil - use Bitwise, only_operators: true + import Bitwise @color_map_black %{black: 0, miss: 1} @color_map_accent %{red: 1, yellow: 1, accent: 1, miss: 0} diff --git a/lib/hal/io_impression.ex b/lib/hal/io_impression.ex new file mode 100644 index 0000000..d6ee0a0 --- /dev/null +++ b/lib/hal/io_impression.ex @@ -0,0 +1,149 @@ +defmodule Inky.IO.Impression 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 + + @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 3_000_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[:spi]) + + 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 + } + 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]) + + 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/hal/rpiio.ex b/lib/hal/io_phat.ex similarity index 95% rename from lib/hal/rpiio.ex rename to lib/hal/io_phat.ex index 44dd68f..e25f173 100644 --- a/lib/hal/rpiio.ex +++ b/lib/hal/io_phat.ex @@ -1,7 +1,8 @@ -defmodule Inky.RpiIO do +defmodule Inky.IO.Phat do @moduledoc """ - An `Inky.InkyIO` implementation intended for use with raspberry pis and relies on - Circuits.GPIO and Cirtuits.SPI. + An `Inky.InkyIO` implementation used for the pHATs + + Relies on Circuits.GPIO and Cirtuits.SPI. """ @behaviour Inky.InkyIO diff --git a/lib/impression_buttons.ex b/lib/impression_buttons.ex new file mode 100644 index 0000000..6fb9c59 --- /dev/null +++ b/lib/impression_buttons.ex @@ -0,0 +1,145 @@ +defmodule Inky.ImpressionButtons do + @moduledoc """ + Adds support for the 4 buttons on the Inky Impressions + + The 4 buttons are monitored independently of the display and can be started in the + supervison tree. + + Supply the `:handler` option as an atom, a pid, or `{module, function, args}` tuple + specifying where to send events to. If no handler is supplied, events are simply logged. + + ```elixir + Inky.ImpressionButtons.start_link(handler: self()) + ``` + + You can also query the current value of a button at any time + + ```elixir + Inky.ImpressionButtons.get_value(:a) + ``` + """ + + use GenServer + + alias Circuits.GPIO + + require Logger + + @typedoc """ + Button name for Inky Impression button + + These are labelled A, B, X, and Y on the board. + """ + @type name() :: :a | :b | :x | :y + + defmodule Event do + @moduledoc """ + Represents an event from the buttons + """ + defstruct [:action, :name, :value, :timestamp] + + @type t :: %Event{ + action: :pressed | :released, + name: Inky.ImpressionsButtons.name(), + value: 1 | 0, + timestamp: non_neg_integer() + } + end + + @pin_a 5 + @pin_b 6 + @pin_x 16 + @pin_y 24 + + @doc """ + Start a GenServer to watch the buttons on the Inky Impression + + Options: + + * `:handler` - pass an atom a pid, or an MFA to receive button events + MFA stands for Module Function Args, here's an example MFA that would print out the events `{IO, :inspect, []}` + Note: the event will be prepended to the argument list + """ + @spec start_link(keyword) :: GenServer.on_start() + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @doc """ + Return the current state of the button + + `0` - released + `1` - pressed + """ + @spec get_value(name()) :: 0 | 1 + def get_value(button) do + GenServer.call(__MODULE__, {:get_value, button}) + end + + @impl GenServer + def init(opts) do + {:ok, %{button_to_ref: %{}, pin_to_button: %{}, handler: opts[:handler]}, {:continue, :init}} + end + + @impl GenServer + def handle_continue(:init, state) do + {:ok, a} = GPIO.open(@pin_a, :input, pull_mode: :pullup) + {:ok, b} = GPIO.open(@pin_b, :input, pull_mode: :pullup) + {:ok, x} = GPIO.open(@pin_x, :input, pull_mode: :pullup) + {:ok, y} = GPIO.open(@pin_y, :input, pull_mode: :pullup) + :ok = GPIO.set_interrupts(a, :both) + :ok = GPIO.set_interrupts(b, :both) + :ok = GPIO.set_interrupts(x, :both) + :ok = GPIO.set_interrupts(y, :both) + + button_to_ref = %{a: a, b: b, x: x, y: y} + + pin_to_button = %{ + @pin_a => :a, + @pin_b => :b, + @pin_x => :x, + @pin_y => :y + } + + {:noreply, %{state | button_to_ref: button_to_ref, pin_to_button: pin_to_button}} + end + + @impl GenServer + def handle_call({:get_value, name}, _from, state) do + inverted_value = GPIO.read(state.button_to_ref[name]) + value = 1 - inverted_value + + {:reply, value, state} + end + + @impl GenServer + def handle_info({:circuits_gpio, pin, timestamp, inverted_value}, state) do + value = 1 - inverted_value + action = if value != 0, do: :pressed, else: :released + + event = %Event{ + action: action, + name: state.pin_to_button[pin], + value: value, + timestamp: timestamp + } + + _ = send_event(state.handler, event) + + {:noreply, state} + end + + def handle_info(_other, state), do: {:noreply, state} + + defp send_event(handler, event) when is_atom(handler), do: send(handler, event) + + defp send_event(handler, event) when is_pid(handler), do: send(handler, event) + + defp send_event({m, f, a}, event) when is_atom(m) and is_atom(f) and is_list(a) do + apply(m, f, [event | a]) + end + + defp send_event(_, event) do + Logger.info("[Inky] unhandled button event - #{inspect(event)}") + end +end diff --git a/lib/inky.ex b/lib/inky.ex index d916210..b222c83 100644 --- a/lib/inky.ex +++ b/lib/inky.ex @@ -9,13 +9,20 @@ defmodule Inky do require Logger alias Inky.Display - alias Inky.RpiHAL @typedoc "The Inky process name" @type name :: atom | {:global, term} | {:via, module, term} @default_border :black @push_timeout 5000 + @valid_accents [:black, :red, :yellow] + @valid_types [ + :phat_il91874, + :phat_ssd1608, + :impression_4, + :impression_5_7, + :impression_7_3 + ] defmodule State do @moduledoc false @@ -23,7 +30,7 @@ defmodule Inky do @enforce_keys [:display, :hal_state] defstruct border: :black, display: nil, - hal_mod: RpiHAL, + hal_mod: Inky.HAL.PhatIL91874, hal_state: nil, pixels: %{}, type: nil, @@ -36,16 +43,16 @@ defmodule Inky do @doc """ Start an Inky GenServer for a display of type `type`, with the color `accent` - using the optionally provided options `opts`. + (not needed for Inky Impression) using the optionally provided options `opts`. The GenServer deals with the HAL state and pushing pixels to the physical display. ## Parameters - - `type` - An atom, representing the display type, either `:phat` or `:what` + - `type` - An atom, representing the display type, one of `#{inspect(@valid_types)}` - `accent` - An atom, representing the display's third color, one of - `:black`, `:red` or `:yellow`. + `#{inspect(@valid_accents)}`. ## Options @@ -54,9 +61,44 @@ defmodule Inky do See `GenServer.start_link/3` for return values. """ - def start_link(type, accent, opts \\ %{}) do + 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, accent, opts], genserver_opts) + accent = verify_required_accent!(type, opts[:accent]) + opts = Keyword.put(opts, :accent, accent) + GenServer.start_link(__MODULE__, {type, opts}, genserver_opts) + end + + # For backwards compatibility + def start_link(type, opts) when is_map(opts) do + opts = Enum.map(opts, fn {key, val} -> {key, val} end) + start_link(type, opts) + end + + def start_link(type, accent, opts) when is_list(opts) do + genserver_opts = if(opts[:name], do: [name: opts[:name]], else: []) + accent = verify_required_accent!(type, accent) + opts = Keyword.put(opts, :accent, accent) + GenServer.start_link(__MODULE__, {type, opts}, genserver_opts) + end + + # For backwards compatibility + def start_link(type, accent, opts) when is_map(opts) do + opts = Enum.map(opts, fn {key, val} -> {key, val} end) + start_link(type, accent, opts) + end + + defp verify_required_accent!(type, accent) + when type in [:phat_il91874, :phat_ssd1608, :what] do + if accent in @valid_accents do + accent + else + raise ":accent is required for the #{type}. And must be one of #{inspect(@valid_accents)}. Received #{inspect(accent)}" + end + end + + defp verify_required_accent!(type, _accent) + when type in [:impression_4, :impression_5_7, :impression_7_3] do + :none end @doc """ @@ -139,13 +181,22 @@ defmodule Inky do # @impl GenServer - def init([type, accent, opts]) do + def init({type, opts}) do border = opts[:border] || @default_border + accent = Access.get(opts, :accent, :none) + hal_mod = - case type do - :phat_ssd1608 -> opts[:hal_mod] || Inky.HAL.PhatSSD1608 - _ -> opts[:hal_mod] || RpiHAL + if opts[:hal_mod] do + opts[:hal_mod] + else + case type do + :phat_il91874 -> Inky.HAL.PhatIL91874 + :phat_ssd1608 -> Inky.HAL.PhatSSD1608 + :impression_4 -> Inky.HAL.ImpressionUC8159 + :impression_5_7 -> Inky.HAL.ImpressionUC8159 + :impression_7_3 -> Inky.HAL.ImpressionAC073TC1A + end end display = Display.spec_for(type, accent) @@ -285,7 +336,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 6b3739b..db07293 100644 --- a/mix.exs +++ b/mix.exs @@ -5,7 +5,8 @@ defmodule Inky.MixProject do [ app: :inky, version: "1.0.2", - elixir: "~> 1.8", + elixir: "~> 1.9", + elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, source_url: "https://github.com/pappersverk/inky/", deps: deps(), @@ -21,13 +22,17 @@ defmodule Inky.MixProject do ] end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + # Run "mix help deps" to learn about dependencies. defp deps do [ - {:circuits_gpio, "~> 0.4"}, - {:circuits_spi, "~> 0.1"}, - {:circuits_i2c, "~> 0.3"}, - {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, + {:circuits_gpio, "~> 0.4 or ~> 1.0"}, + {:circuits_spi, "~> 0.1 or ~> 1.0"}, + {:circuits_i2c, "~> 0.3 or ~> 1.0"}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev} ] end diff --git a/mix.lock b/mix.lock index ecf8327..02d746a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,14 +1,16 @@ %{ - "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_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, "0.1.3", "a94889abc874e9976f397c649152776d9c0863e5fd3377203c7f0cf992d9609c", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "09569214ccdff871149dc5d738a20206647b60769a919b5bb1a6a7f843a10f66"}, - "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"}, - "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"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "circuits_gpio": {:hex, :circuits_gpio, "1.1.0", "cda895fd0a12fdf50e27f6d61cc349587dff29755fca640b93233a661925d97a", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "11dab3c7b39cbe08588e9527c9fd98117be485f70b61641874abdda50340e991"}, + "circuits_i2c": {:hex, :circuits_i2c, "1.2.2", "1666bf1763fe60ab835462d13f04ca75b0de000a7d5b89e24d8f35a06ef6d097", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "a7fffcb1bf128e4112b0734796f26b0b4f2359e4c60e86d9718c31834e10a343"}, + "circuits_spi": {:hex, :circuits_spi, "1.4.0", "ca1674c4c955bbd3e6e8d4390c63514da6b6d976b098ed99d375d09b94259a5b", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "bf37c24a720c30a5201b7d098276ef074c1b5a79105e11dec11ddd54d6fb793f"}, + "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"}, + "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, + "ex_doc": {:hex, :ex_doc, "0.30.4", "e8395c8e3c007321abb30a334f9f7c0858d80949af298302daf77553468c0c39", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9a19f0c50ffaa02435668f5242f2b2a61d46b541ebf326884505dfd3dd7af5e4"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, } diff --git a/test/rpihal_test.exs b/test/hal_phat_il91874_test.exs similarity index 67% rename from test/rpihal_test.exs rename to test/hal_phat_il91874_test.exs index d777ec7..2a0a94d 100644 --- a/test/rpihal_test.exs +++ b/test/hal_phat_il91874_test.exs @@ -1,10 +1,10 @@ -defmodule Inky.RpiHALTest do +defmodule Inky.HAL.PhatIL91874Test do @moduledoc false use ExUnit.Case alias Inky.Display - alias Inky.RpiHAL + alias Inky.HAL.PhatIL91874 alias Inky.TestIO import Inky.TestUtil, only: [gather_messages: 0, pos2col: 2] @@ -19,8 +19,7 @@ defmodule Inky.RpiHALTest do setup_all do pixels = - :phat - |> Display.spec_for() + Display.spec_for(:phat_il91874, :black) |> init_pixels() %{pixels: pixels} @@ -28,9 +27,9 @@ defmodule Inky.RpiHALTest do describe "happy paths" do test "that init dispatches properly" do - display = Display.spec_for(:phat) + display = Display.spec_for(:phat_il91874, :black) # act - RpiHAL.init(%{ + PhatIL91874.init(%{ display: display, io_args: [], io_mod: TestIO @@ -43,7 +42,7 @@ defmodule Inky.RpiHALTest do test "that update dispatches properly when the device is never busy", ctx do # arrange, read_busy always returns 0 - display = Display.spec_for(:phat) + display = Display.spec_for(:phat_il91874, :black) init_args = %{ display: display, @@ -53,13 +52,13 @@ defmodule Inky.RpiHALTest do io_mod: TestIO } - state = RpiHAL.init(init_args) + state = PhatIL91874.init(init_args) # act - :ok = RpiHAL.handle_update(ctx.pixels, display.accent, :await, state) + :ok = PhatIL91874.handle_update(ctx.pixels, display.accent, :await, state) # assert - assert_received {:init, init_args} + assert_received {:init, _} assert TestIO.assert_expectations() == :ok spec = load_spec("data/success1.dat", __DIR__) mailbox = gather_messages() @@ -68,7 +67,7 @@ defmodule Inky.RpiHALTest do test "that update dispatches properly when the device is a little busy", ctx do # arrange, read_busy is a little busy each time, we expect two wait-loops. - display = Display.spec_for(:phat) + display = Display.spec_for(:phat_il91874, :black) init_args = %{ display: display, @@ -78,13 +77,13 @@ defmodule Inky.RpiHALTest do io_mod: TestIO } - state = RpiHAL.init(init_args) + state = PhatIL91874.init(init_args) # act - :ok = RpiHAL.handle_update(ctx.pixels, display.accent, :await, state) + :ok = PhatIL91874.handle_update(ctx.pixels, display.accent, :await, state) # assert - assert_received {:init, init_args} + assert_received {:init, _} assert TestIO.assert_expectations() == :ok spec = load_spec("data/success2.dat", __DIR__) mailbox = gather_messages() @@ -106,46 +105,46 @@ defmodule Inky.RpiHALTest do end test "test border, black accent", ctx do - display = Display.spec_for(:phat) + display = Display.spec_for(:phat_il91874, :black) init_black = %{display: display, io_args: [read_busy: 0], io_mod: TestIO} - black_state = RpiHAL.init(init_black) + black_state = PhatIL91874.init(init_black) # black accent, black border - :ok = RpiHAL.handle_update(ctx.pixels, :black, :await, black_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :black, :await, black_state) assert get_border_command() == [send_command: {60, 0}] # black accent, white border - :ok = RpiHAL.handle_update(ctx.pixels, :white, :await, black_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :white, :await, black_state) assert get_border_command() == [send_command: {60, 49}] end test "red accent, red border", ctx do # arrange - display = Display.spec_for(:phat, :red) + display = Display.spec_for(:phat_il91874, :red) init_red = %{display: display, io_args: [read_busy: 0], io_mod: TestIO} - red_state = RpiHAL.init(init_red) + red_state = PhatIL91874.init(init_red) # act, explicit border - :ok = RpiHAL.handle_update(ctx.pixels, :red, :await, red_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :red, :await, red_state) assert get_border_command() == [send_command: {60, 115}] # act, implicit border - :ok = RpiHAL.handle_update(ctx.pixels, :accent, :await, red_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :accent, :await, red_state) assert get_border_command() == [send_command: {60, 115}] end test "yellow accent, yellow border", ctx do # arrange - display = Display.spec_for(:phat, :yellow) + display = Display.spec_for(:phat_il91874, :yellow) init_yellow = %{display: display, io_args: [read_busy: 0], io_mod: TestIO} - yellow_state = RpiHAL.init(init_yellow) + yellow_state = PhatIL91874.init(init_yellow) # act, explicit border - :ok = RpiHAL.handle_update(ctx.pixels, :yellow, :await, yellow_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :yellow, :await, yellow_state) assert get_border_command() == [send_command: {60, 51}] # act, implicit border - :ok = RpiHAL.handle_update(ctx.pixels, :accent, :await, yellow_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :accent, :await, yellow_state) assert get_border_command() == [send_command: {60, 51}] end @@ -153,19 +152,19 @@ defmodule Inky.RpiHALTest do # arrange display = Display.spec_for(:what, :yellow) init_red = %{display: display, io_args: [read_busy: 0], io_mod: TestIO} - red_state = RpiHAL.init(init_red) + red_state = PhatIL91874.init(init_red) - :ok = RpiHAL.handle_update(ctx.pixels, :accent, :await, red_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :accent, :await, red_state) assert get_vhs_and_vhl_voltage_command() == [send_command: {0x04, 0x07}] end test "yellow display, phat", ctx do # arrange - display = Display.spec_for(:phat, :yellow) + display = Display.spec_for(:phat_il91874, :yellow) init_red = %{display: display, io_args: [read_busy: 0], io_mod: TestIO} - red_state = RpiHAL.init(init_red) + red_state = PhatIL91874.init(init_red) - :ok = RpiHAL.handle_update(ctx.pixels, :accent, :await, red_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :accent, :await, red_state) assert get_vhs_and_vhl_voltage_command() == [send_command: {0x04, 0x07}] end @@ -173,9 +172,9 @@ defmodule Inky.RpiHALTest do # arrange display = Display.spec_for(:what, :red) init_red = %{display: display, io_args: [read_busy: 0], io_mod: TestIO} - red_state = RpiHAL.init(init_red) + red_state = PhatIL91874.init(init_red) - :ok = RpiHAL.handle_update(ctx.pixels, :red, :await, red_state) + :ok = PhatIL91874.handle_update(ctx.pixels, :red, :await, red_state) assert get_vhs_and_vhl_voltage_command() == [send_command: {0x04, <<0x30, 0xAC, 0x22>>}] end end diff --git a/test/inky_test.exs b/test/inky_test.exs index ba8a256..0bd9b20 100644 --- a/test/inky_test.exs +++ b/test/inky_test.exs @@ -1,5 +1,3 @@ -Code.require_file("test/support/testhal.exs") - defmodule Inky.InkyTest do @moduledoc false @@ -13,7 +11,7 @@ defmodule Inky.InkyTest do doctest Inky setup_all do - init_args = [:test_small, :red, [hal_mod: TestHAL]] + init_args = {:test_small, [accent: :red, hal_mod: TestHAL]} {:ok, inited_state} = Inky.init(init_args) receive do @@ -52,7 +50,46 @@ defmodule Inky.InkyTest do assert state.pixels == %{{1, 2} => :white, {0, 0} => :black, {2, 3} => :red} end - # TODO: painter tests + test "assert that painter works", %{inited_state: is} do + TestHAL.on_update(:ok) + + painter = fn x, y, w, h, _pixels_so_far -> + wh = w / 2 + hh = h / 2 + + case {x >= wh, y >= hh} do + {true, true} -> :red + {false, true} -> if(rem(x, 2) == 0, do: :black, else: :white) + {true, false} -> :black + {false, false} -> :white + end + end + + {:reply, :ok, state} = + Inky.handle_call({:set_pixels, painter, %{border: :white}}, :from, is) + + # Flip a few pixels + pixels = %{{0, 0} => :black, {3, 49} => :red, {23, 4} => :white} + {:reply, :ok, state} = Inky.handle_call({:set_pixels, pixels, %{}}, :from, state) + + expected_pixels = %{ + {0, 0} => :black, + {3, 49} => :red, + {23, 4} => :white, + {0, 1} => :white, + {0, 2} => :black, + {0, 3} => :black, + {1, 0} => :white, + {1, 1} => :white, + {1, 2} => :white, + {1, 3} => :white, + {2, 0} => :black, + {2, 1} => :black, + {2, 2} => :red + } + + assert state.pixels == expected_pixels + end end describe "Inky timeout" do diff --git a/test/inky_timer_test.exs b/test/inky_timer_test.exs index e235cec..5d7c2aa 100644 --- a/test/inky_timer_test.exs +++ b/test/inky_timer_test.exs @@ -1,5 +1,3 @@ -Code.require_file("test/support/testhal.exs") - defmodule Inky.InkyTimerTest do @moduledoc false @@ -13,14 +11,13 @@ defmodule Inky.InkyTimerTest do doctest Inky setup_all do - init_args = [:test_small, :red, [hal_mod: TestHAL]] - {:ok, inited_state} = Inky.init(init_args) + {:ok, inited_state} = Inky.init({:test_small, accent: :red, hal_mod: TestHAL}) receive do {TestHAL, :init} -> :ok end - %{inited_state: inited_state, init_args: init_args} + %{inited_state: inited_state} end # AWAIT, timer cleared diff --git a/test/rpiio_test.exs b/test/io_phat_test.exs similarity index 82% rename from test/rpiio_test.exs rename to test/io_phat_test.exs index c2aa80b..58a62cf 100644 --- a/test/rpiio_test.exs +++ b/test/io_phat_test.exs @@ -1,4 +1,4 @@ -defmodule Inky.RpiIOTest do +defmodule Inky.IO.PhatTest do @moduledoc false use ExUnit.Case @@ -8,10 +8,10 @@ defmodule Inky.RpiIOTest do import Inky.TestUtil, only: [gather_messages: 0] test "spi_write only sets the dc pin once" do - state = Inky.RpiIO.init() + state = Inky.IO.Phat.init() # NOTE: discard init messages gather_messages() - Inky.RpiIO.handle_command(state, 0x42, [0x1, 0x2, 0x4]) + Inky.IO.Phat.handle_command(state, 0x42, [0x1, 0x2, 0x4]) assert gather_messages() == [ {TestGPIO, {{:gpio, :write}, {{:pid, 22}, 0}}}, diff --git a/test/support/testhal.exs b/test/support/testhal.ex similarity index 100% rename from test/support/testhal.exs rename to test/support/testhal.ex