lib/tm1638.ex

defmodule TM1638 do
  defstruct dio: nil,
            clk: nil,
            stb: nil,
            brightness: 1

  @type t :: map()
  @type bit :: 1 | 0
  @type brightness :: 1..7
  @type pin :: non_neg_integer()

  import Bitwise
  alias TM1638
  alias Circuits.GPIO

  @read_mode 0x02
  @write_mode 0x00
  @incr_addr 0x00
  @fixed_addr 0x04
  @display_length 8
  @default_brightness 3

  @spec init(list) :: {:ok, TM1638.t()}
  def init(_args \\ []) do
    dio = Application.fetch_env!(:tm1638, :dio)
    clk = Application.fetch_env!(:tm1638, :clk)
    stb = Application.fetch_env!(:tm1638, :stb)
    brightness = Application.get_env(:tm1638, :brightness, @default_brightness)

    {:ok, init(dio, clk, stb, brightness)}
  end

  @spec init(pin, pin, pin, integer) :: TM1638.t()
  def init(dio, clk, stb, brightness \\ @default_brightness) do
    {:ok, dio_id} = GPIO.open(dio, :output)
    {:ok, clk_id} = GPIO.open(clk, :output)
    {:ok, stb_id} = GPIO.open(stb, :output)

    tm = %__MODULE__{
      dio: dio_id,
      clk: clk_id,
      stb: stb_id,
      brightness: brightness
    }

    init_display(tm)
  end

  @spec init_display(TM1638.t()) :: TM1638.t()
  def init_display(tm) do
    tm
    |> cs_disable()
    |> clock_high()
    |> turn_on()
    |> clear_display()
  end

  @spec cs_disable(TM1638.t()) :: TM1638.t()
  def cs_disable(tm) do
    GPIO.write(tm.stb, 1)
    tm
  end

  @spec cs_enable(TM1638.t()) :: TM1638.t()
  def cs_enable(tm) do
    GPIO.write(tm.stb, 0)
    tm
  end

  @spec clock_high(TM1638.t()) :: TM1638.t()
  def clock_high(tm) do
    GPIO.write(tm.clk, 1)
    tm
  end

  @spec clock_low(TM1638.t()) :: TM1638.t()
  def clock_low(tm) do
    GPIO.write(tm.clk, 0)
    tm
  end

  @spec data_high(TM1638.t()) :: TM1638.t()
  def data_high(tm) do
    GPIO.write(tm.dio, 1)
    tm
  end

  @spec data_low(TM1638.t()) :: TM1638.t()
  def data_low(tm) do
    GPIO.write(tm.dio, 0)
    tm
  end

  @spec data_set_to(TM1638.t(), bit) :: TM1638.t()
  def data_set_to(tm, value) do
    GPIO.write(tm.dio, value)
    tm
  end

  @doc """
  Clear the display
  Turn off every led
  """
  @spec clear_display(TM1638.t()) :: TM1638.t()
  def clear_display(tm) do
    tm
    |> cs_enable()
    # set data read mode (automatic address increased)
    |> set_data_mode(@write_mode, @incr_addr)
    # address command set to the 1st address
    |> send_byte(0xC0)

    for _i <- 1..(@display_length * 2) do
      # set to zero all the addresses
      send_byte(tm, 0x00)
    end

    cs_disable(tm)
  end

  @doc """
  Turn off (physically) the leds
  """
  @spec turn_off(TM1638.t()) :: TM1638.t()
  def turn_off(tm) do
    send_command(tm, 0x80)
  end

  @doc """
  Turn on the display and set the brightness
  The pulse width used is set to:

  0 => 1/16
  1 => 2/16
  2 => 4/16
  3 => 10/16
  4 => 11/16
  5 => 12/16
  6 => 13/16
  7 => 14/16
  """
  @spec turn_on(TM1638.t()) :: TM1638.t()
  def turn_on(tm) do
    send_command(tm, 0x88 ||| (tm.brightness &&& 7))
  end

  def turn_on(tm, _brightness), do: tm

  @doc """
  Set Leds
  values accepts an integer in range 0..255
  and sets all leds according to the bits in integer
  """
  @spec leds(TM1638.t(), byte) :: TM1638.t()
  def leds(tm, values) when values in 0..255 do
    for <<(bit::1 <- <<values>>)>> do
      bit
    end
    |> Enum.with_index()
    |> Enum.each(fn {bit, index} -> led(tm, index, bit) end)

    tm
  end

  @doc """
  Set single led to on or off
  the leds are on the bit 0 of the odd addresses 
  (led_0 on address 1, led_1 on address 3)
  """
  @spec led(TM1638.t(), 0..7, bit) :: TM1638.t()
  def led(tm, index, value) when index in 0..7 and value in 0..1 do
    send_data(tm, rem(index, @display_length) * 2 + 1, value)
  end

  @doc """
  Send a command
  """
  @spec send_command(TM1638.t(), byte) :: TM1638.t()
  def send_command(tm, cmd) do
    tm
    |> cs_enable()
    |> send_byte(cmd)
    |> cs_disable()
  end

  @doc """
  Send a data at address
  """
  @spec send_data(TM1638.t(), byte, byte) :: TM1638.t()
  def send_data(tm, address, data) do
    tm
    |> cs_enable()
    |> set_data_mode(@write_mode, @fixed_addr)
    # set address and send byte (stb must go high and low before sending address)
    |> cs_disable()
    |> cs_enable()
    |> send_byte(0xC0 ||| address)
    |> send_byte(data)
    |> cs_disable()
  end

  @doc """
  Set the data modes
  :param wr_mode: read_mode (read the key scan) or write_mode (write data)
  :param addr_mode: incr_addr (automatic address increased) or fixed_addr
  """
  @spec set_data_mode(TM1638.t(), byte, byte) :: TM1638.t()
  def set_data_mode(tm, wr_mode, addr_mode) do
    send_byte(tm, 0x40 ||| wr_mode ||| addr_mode)
  end

  @doc """
  Send a byte (STROBE must be Low)

  Sending a bit from a byte consists of setting the STROBE to low
  Then setting to CLOCK to low, sending the bit via dio and
  setting the clock back to HIGH..
  """
  @spec send_byte(TM1638.t(), byte, integer) :: TM1638.t()
  def send_byte(tm, data, counter \\ 8)

  def send_byte(tm, data, counter) when counter > 0 do
    tm
    |> clock_low()
    |> data_set_to(data &&& 1)
    |> clock_high()

    send_byte(tm, data >>> 1, counter - 1)
  end

  def send_byte(tm, _data, _counter), do: tm

  @doc """
  Get the data buttons the four octets read
  on rpi ver B it takes around 16ms to get the whole 4 bits
  """
  @spec get_data(TM1638.t()) :: TM1638.t()
  def get_data(tm) do
    # set in read mode
    tm
    |> cs_enable()
    |> set_data_mode(@read_mode, @incr_addr)

    GPIO.set_pull_mode(tm.dio, :pullup)
    GPIO.set_direction(tm.dio, :input)
    GPIO.set_interrupts(tm.dio, :both)
    bytes = get_bytes(tm, 4)
    GPIO.set_direction(tm.dio, :output)

    cs_disable(tm)

    bytes
  end

  @doc """
  Receive the bytes_count from the board

  """
  @spec get_bytes(TM1638.t(), integer) :: TM1638.t()
  def get_bytes(tm, bytes_count \\ 4) do
    # read 4 bytes
    Enum.reduce(1..bytes_count, [], fn _x, bytes ->
      # read 8 bits
      byte =
        Enum.reduce(1..8, 0, fn _x, bit ->
          clock_low(tm)
          bit = bit >>> 1
          bit = if Circuits.GPIO.read(tm.dio) == 1, do: bit ||| 0x80, else: bit
          clock_high(tm)
          bit
        end)

      [byte | bytes]
    end)
  end

  @doc """
  Example:
  TM1638.display_segment(tm,1, "")
  -> set the i-th 7-segment display (and all the following, according to the length of value1)
  all the 7-segment displays after the #i are filled by the characters in value1
  this could be one-character string (so 7-segment #i is set to that character)
  or a longer string, and the following 7-segment displays are modified accordingly
  """
  @spec display_segment(TM1638.t(), integer, byte) :: TM1638.t()
  def display_segment(tm, index, value) do
    charbyte = Map.get(TM1638.Font.font(), value, " ")

    send_data(tm, rem(index, @display_length) * 2, charbyte)
  end

  @doc """
  Displays text starting from the correct position
  """
  @spec display_text(TM1638.t(), String.t(), integer) :: TM1638.t()
  def display_text(tm, text, start \\ 0)

  def display_text(tm, text, start) when is_binary(text) and start < 0 do
    text = String.duplicate(" ", -start) <> text
    display_text(tm, text)
  end

  def display_text(tm, text, start) when is_binary(text) do
    char_list =
      text
      |> String.slice(start..(start + @display_length))
      |> String.pad_trailing(@display_length)
      |> String.split("", trim: true)

    display_text(tm, char_list)
  end

  @spec display_text(TM1638.t(), [String.t()], integer) :: TM1638.t()
  def display_text(tm, char_list, _start) do
    char_list
    |> Enum.take(@display_length)
    |> Enum.with_index()
    |> Enum.each(fn {char, location} ->
      display_segment(tm, location, char)
    end)

    tm
  end

  @spec display_moving_text(TM1638.t(), String.t(), integer) :: TM1638.t()
  def display_moving_text(tm, text, speed \\ 500) do
    text_length = String.length(text)

    -@display_length
    |> Range.new(text_length + @display_length, 1)
    |> Enum.map(fn index ->
      display_text(tm, text, index)
      Process.sleep(speed)
    end)

    tm
  end
end