diff --git a/README.md b/README.md index a132275..c75789d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # OLED -[![CircleCI](https://circleci.com/gh/pappersverk/oled.svg?style=svg)](https://circleci.com/gh/pappersverk/oled) +![Test Status](https://github.com/pappersverk/oled/actions/workflows/tests.yml/badge.svg) [![Hex version](https://img.shields.io/hexpm/v/oled.svg "Hex version")](https://hex.pm/packages/oled) diff --git a/lib/oled/display.ex b/lib/oled/display.ex index f3f8a62..1dd2518 100644 --- a/lib/oled/display.ex +++ b/lib/oled/display.ex @@ -79,12 +79,21 @@ defmodule OLED.Display do def display_frame(data, opts \\ []), do: Server.display_frame(@me, data, opts) + def display_raw_frame(data, opts \\ []), + do: Server.display_raw_frame(@me, data, opts) + def clear(), do: Server.clear(@me) def clear(pixel_state), do: Server.clear(@me, pixel_state) + def put_buffer(data), + do: Server.put_buffer(@me, data) + + def get_buffer(), + do: Server.get_buffer(@me) + def put_pixel(x, y, opts \\ []), do: Server.put_pixel(@me, x, y, opts) @@ -118,10 +127,21 @@ defmodule OLED.Display do @callback display() :: :ok @doc """ - Transfer a data frame to the display buffer. + Transfer a data frame to the screen. The data frame format is equal to the display buffer + that gets altered via the drawing commands. + + Calling this function transfers the data frame directly to the screen and does not alter the display buffer. """ @callback display_frame(data :: binary(), opts :: Server.display_frame_opts()) :: :ok + @doc """ + Transfer a raw data frame to the screen. + + A raw data frame is in a different format than the display buffer. + To transform a display buffer to a raw data frame, `OLED.Display.Impl.SSD1306.translate_buffer/3` can be used. + """ + @callback display_raw_frame(data :: binary(), opts :: Server.display_frame_opts()) :: :ok + @doc """ Clear the buffer. """ @@ -132,6 +152,21 @@ defmodule OLED.Display do """ @callback clear(pixel_state :: Server.pixel_state()) :: :ok + @doc """ + Override the current buffer which is the internal data structure that is sent to the screen with `c:display/0`. + + A possible use-case is to draw some content, get the buffer via `c:get_buffer/0` + and set it again at a later time to save calls to the draw functions. + """ + @callback put_buffer(data :: binary()) :: :ok | {:error, term()} + + @doc """ + Get the current buffer which is the internal data structure that is changed by the draw methods + and sent to the screen with `c:display/0`. + """ + @callback get_buffer() :: {:ok, binary()} + + @doc """ Put a pixel on the buffer. The pixel can be on or off and be drawed in xor mode (if the pixel is already on is turned off). """ diff --git a/lib/oled/display/impl/ssd_1306.ex b/lib/oled/display/impl/ssd_1306.ex index 7efb976..80c8df8 100644 --- a/lib/oled/display/impl/ssd_1306.ex +++ b/lib/oled/display/impl/ssd_1306.ex @@ -129,29 +129,21 @@ defmodule OLED.Display.Impl.SSD1306 do buffer = translate_buffer(buffer, width, opts[:memory_mode]) - display_frame(state, buffer, opts) + display_raw_frame(state, buffer, opts) end def display(error, _opts), do: error - defp translate_buffer(buffer, width, :horizontal) do - for <> do - for(<>, do: b) - |> Enum.chunk_every(width) - |> Enum.zip() - |> Enum.map(fn bits -> - bits - |> Tuple.to_list() - |> Enum.reverse() - |> Enum.into(<<>>, fn bit -> <> end) - end) - end - |> List.flatten() - |> Enum.into(<<>>) + def display_frame(%__MODULE__{width: width} = state, data, opts) do + opts = Keyword.merge(@display_opts, opts) + + buffer = translate_buffer(data, width, opts[:memory_mode]) + + display_raw_frame(state, buffer, opts) end - def display_frame(%__MODULE__{} = state, data, opts) do + def display_raw_frame(%__MODULE__{} = state, data, opts) do memory_mode = get_memory_mode(opts[:memory_mode] || :horizontal) if byte_size(data) == state.width * state.height / 8 do @@ -165,6 +157,21 @@ defmodule OLED.Display.Impl.SSD1306 do end end + def translate_buffer(buffer, width, :horizontal) do + transformation = + for x <- 0..(width - 1), y <- 0..7 do + (7 - y) * width + x + end + + for <>, into: <<>> do + for source <- transformation, into: <<>> do + rest = width * 8 - source - 1 + <<_::size(source)-unit(1), b::1, _::size(rest)-unit(1)>> = page + <> + end + end + end + def clear_buffer(%__MODULE__{width: w, height: h} = state, pixel_state) when pixel_state in [:on, :off] do value = @@ -187,6 +194,17 @@ defmodule OLED.Display.Impl.SSD1306 do def clear_buffer(error, _pixel_state), do: error + def put_buffer(%__MODULE__{} = state, data) do + if byte_size(data) == state.width * state.height / 8 do + %{state | buffer: data} + else + {:error, :invalid_data_size} + end + end + + def get_buffer(%__MODULE__{buffer: buffer}), + do: {:ok, buffer} + def get_dimensions(%__MODULE__{width: width, height: height}), do: {:ok, width, height} diff --git a/lib/oled/display/server.ex b/lib/oled/display/server.ex index 3e66245..5797231 100644 --- a/lib/oled/display/server.ex +++ b/lib/oled/display/server.ex @@ -46,10 +46,22 @@ defmodule OLED.Display.Server do def display_frame(server, data, opts \\ []), do: GenServer.call(server, {:display_frame, data, opts}) + @doc false + def display_raw_frame(server, data, opts \\ []), + do: GenServer.call(server, {:display_raw_frame, data, opts}) + @doc false def clear(server, pixel_state \\ :off), do: GenServer.call(server, {:clear, pixel_state}) + @doc false + def put_buffer(server, data), + do: GenServer.call(server, {:put_buffer, data}) + + @doc false + def get_buffer(server), + do: GenServer.call(server, :get_buffer) + @doc false def put_pixel(server, x, y, opts \\ []), do: GenServer.call(server, {:put_pixel, x, y, opts}) @@ -96,12 +108,31 @@ defmodule OLED.Display.Server do |> handle_response(impl, state) end + @doc false + def handle_call({:display_raw_frame, data, opts}, _from, {impl, state}) do + state + |> impl.display_raw_frame(data, opts) + |> handle_response(impl, state) + end + def handle_call({:clear, pixel_state}, _from, {impl, state}) do state |> impl.clear_buffer(pixel_state) |> handle_response(impl, state) end + def handle_call({:put_buffer, data}, _from, {impl, state}) do + state + |> impl.put_buffer(data) + |> handle_response(impl, state) + end + + def handle_call(:get_buffer, _from, {impl, state}) do + res = impl.get_buffer(state) + + {:reply, res, {impl, state}} + end + def handle_call({:put_pixel, x, y, opts}, _from, {impl, state}) do state |> impl.put_pixel(x, y, opts) diff --git a/test/oled/display/impl/ssd_1306_test.exs b/test/oled/display/impl/ssd_1306_test.exs index c5d9ffb..e6233ee 100644 --- a/test/oled/display/impl/ssd_1306_test.exs +++ b/test/oled/display/impl/ssd_1306_test.exs @@ -35,6 +35,27 @@ defmodule OLED.Display.Impl.SSD1306Test do end describe "display_frame/2" do + test "with valid data" do + data = + for v <- 1..8, into: <<>> do + <> + end + + state = %SSD1306{ + dev: %DummyDev{}, + width: 8, + height: 8, + } + + assert %SSD1306{} = SSD1306.display_frame(state, data, []) + + assert_received {:command, 32} + assert_received {:command, 0} + assert_received {:transfer, <<0, 0, 0, 0, 128, 120, 102, 85>>} + end + end + + describe "display_raw_frame/2" do test "with valid data" do state = %SSD1306{ dev: %DummyDev{}, @@ -47,7 +68,7 @@ defmodule OLED.Display.Impl.SSD1306Test do <<0>> end - assert %SSD1306{} = SSD1306.display_frame(state, data, memory_mode: :vertical) + assert %SSD1306{} = SSD1306.display_raw_frame(state, data, memory_mode: :vertical) assert_received {:command, 32} assert_received {:command, 1} @@ -67,7 +88,87 @@ defmodule OLED.Display.Impl.SSD1306Test do end assert {:error, :invalid_data_size} = - SSD1306.display_frame(state, data, memory_mode: :vertical) + SSD1306.display_raw_frame(state, data, memory_mode: :vertical) + end + end + + describe "put_buffer/1" do + test "with valid data" do + state = %SSD1306{ + dev: %DummyDev{}, + width: 8, + height: 8, + } + + data = + for v <- 1..8, into: <<>> do + <> + end + + assert %SSD1306{buffer: buffer} = SSD1306.put_buffer(state, data) + + assert buffer = <<0, 0, 0, 0, 128, 120, 102, 85>> + end + + test "with invalid data" do + state = %SSD1306{ + dev: %DummyDev{}, + width: 8, + height: 8 + } + + data = + for v <- 1..16, into: <<>> do + <> + end + + assert {:error, :invalid_data_size} = + SSD1306.put_buffer(state, data) + end + end + + describe "get_buffer/0" do + test "with valid data" do + data = + for v <- 1..8, into: <<>> do + <> + end + + state = %SSD1306{ + dev: %DummyDev{}, + width: 8, + height: 8, + buffer: data + } + + assert {:ok, buffer} = SSD1306.get_buffer(state) + assert buffer = <<0, 0, 0, 0, 128, 120, 102, 85>> + end + end + + describe "translate_buffer/3" do + test "with valid data" do + # Buffer is generated using the following draw functions: + # buffer = + # OLED.BufferTestHelper.build_state(32, 16) + # |> OLED.Display.Impl.SSD1306.Draw.line_h(1, 0, 30, []) + # |> OLED.Display.Impl.SSD1306.Draw.line_h(1, 15, 30, []) + # |> OLED.Display.Impl.SSD1306.Draw.line_v(0, 1, 14, []) + # |> OLED.Display.Impl.SSD1306.Draw.line_v(31, 1, 14, []) + # |> OLED.Display.Impl.SSD1306.Draw.circle(10, 8, 6, []) + # |> OLED.Display.Impl.SSD1306.Draw.circle(21, 8, 6, []) + # |> Map.get(:buffer) + + buffer = <<127, 255, 255, 254, 128, 0, 0, 1, 128, 248, 31, 1, 131, 6, 96, 193, 132, 1, + 128, 33, 132, 1, 128, 33, 136, 1, 128, 17, 136, 1, 128, 17, 136, 1, 128, 17, + 136, 1, 128, 17, 136, 1, 128, 17, 132, 1, 128, 33, 132, 1, 128, 33, 131, 6, + 96, 193, 128, 248, 31, 1, 127, 255, 255, 254>> + + assert SSD1306.translate_buffer(buffer, 32, :horizontal) + == <<254, 1, 1, 1, 193, 49, 9, 9, 5, 5, 5, 5, 5, 9, 9, 241, 241, 9, 9, 5, 5, 5, 5, + 5, 9, 9, 49, 193, 1, 1, 1, 254, 127, 128, 128, 128, 135, 152, 160, 160, 192, + 192, 192, 192, 192, 160, 160, 159, 159, 160, 160, 192, 192, 192, 192, 192, + 160, 160, 152, 135, 128, 128, 128, 127>> end end end