Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Do not merge] Jax inky impression messy #45

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 57 additions & 3 deletions lib/display/display.ex
Expand Up @@ -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,
Expand Down Expand Up @@ -83,4 +109,32 @@ defmodule Inky.Display do
# Little endian, unsigned short
<<rows::unsigned-little-integer-16>>
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
82 changes: 82 additions & 0 deletions 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(
<<width::little-16, height::little-16, color, pcb_variant, display_variant, _,
timestamp::binary>>
)
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
2 changes: 1 addition & 1 deletion lib/hal/hal.ex
Expand Up @@ -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
200 changes: 200 additions & 0 deletions 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