defmodule Fledex.Leds do import Bitwise use Fledex.Color.Types use Fledex.Color.Names require Logger alias Fledex.Functions alias Fledex.LedsDriver alias Fledex.Color.Utils @enforce_keys [:count, :leds, :opts] defstruct count: 0, leds: %{}, opts: %{}, meta: %{index: 1} #, fill: :none @type t :: %__MODULE__{count: integer, leds: map, opts: map, meta: map} @func_ids %{ rainbow: &Fledex.Leds.rainbow/2, gradient: &Fledex.Leds.gradient/2 } @spec new() :: t def new() do new(0) end @spec new(integer) :: t def new(count) do new(count, %{}) end @spec new(integer, map) :: t def new(count, opts) do new(count, %{}, opts) end @spec new(integer, map, map) :: t def new(count, leds, opts) do new(count, leds, opts, %{index: 1}) end @spec new(integer, map, map, map) :: t def new(count, leds, opts, meta) do %__MODULE__{count: count, leds: leds, opts: opts, meta: meta} end @spec rainbow(t, map) :: t def rainbow(leds, config) do num_leds = config[:num_leds] || leds.count initial_hue = config[:initial_hue] || 0 reversed = if config[:reversed], do: config[:reversed], else: false offset = config[:offset] || 0 led_values = Functions.create_rainbow_circular_rgb(num_leds, initial_hue, reversed) |> convert_to_leds_structure(offset) put_in(leds.leds, Map.merge(leds.leds, led_values)) end @spec convert_to_leds_structure(list(rgb), integer) :: map def convert_to_leds_structure(rgbs, offset \\ 0) do offset_oneindex = offset + 1 Enum.zip_with(offset_oneindex..(offset_oneindex + length(rgbs)), rgbs, fn(index,{r,g,b}) -> {index, (r <<< 16) + (g <<< 8) + b} end) |> Map.new end @spec gradient(t, map) :: t def gradient(leds, %{start_color: start_color, end_color: end_color} = config) do num_leds = config[:num_leds] || leds.count offset = config[:offset] || 0 start_color = Utils.convert_to_subpixels(start_color) end_color = Utils.convert_to_subpixels(end_color) led_values = Functions.create_gradient_rgb(num_leds, start_color, end_color) |> convert_to_leds_structure(offset) put_in(leds.leds, Map.merge(leds.leds, led_values)) end def gradient(_leds, _config) do raise "You need to specify at least a start_color and end_color" end @spec light(t, (colorint | t | atom)) :: t def light(leds, rgb) do do_update(leds, rgb) end @spec light(t, (colorint | t | atom), pos_integer) :: t def light(leds, led, offset) do do_update(leds, led, offset) end @spec func(t, atom, map) :: t def func(leds, func_id, config \\ %{}) do func = @func_ids[func_id] func.(leds, config) end @spec update(t, (colorint | rgb | atom)) :: t def update(leds, led) do do_update(leds, led) end @doc """ :offset is 1 indexed. Offset needs to be >0 if it's bigger than the :count then the led will be stored, but ignored """ @spec update(t, (colorint | t), pos_integer) :: t def update(leds, led, offset) when offset > 0 do do_update(leds,led,offset) end def update(_leds, _led, offset) do raise ArgumentError, message: "the offset needs to be > 0 (found: #{offset})" end @spec do_update(t, (colorint | rgb | atom)) :: t defp do_update(%__MODULE__{meta: meta} = leds, rgb) do index = meta[:index] || 1 do_update(leds, rgb, index) end @spec do_update(t, colorint, pos_integer) :: t defp do_update(%__MODULE__{count: count, leds: leds, opts: opts, meta: meta}, rgb, offset) when is_integer(rgb) do __MODULE__.new(count, Map.put(leds, offset, rgb), opts, %{meta | index: offset+1}) end @spec do_update(t, t, pos_integer) :: t defp do_update(%__MODULE__{count: count1, leds: leds1, opts: opts1, meta: meta1}, %__MODULE__{count: count2, leds: leds2}, offset) do # remap the indicies (1 indexed) remapped_new_leds = Map.new(Enum.map(leds2, fn {key, value} -> index = offset + key - 1 {index, value} end)) leds = Map.merge(leds1, remapped_new_leds) __MODULE__.new(count1, leds, opts1, %{meta1 | index: offset+count2}) end @spec do_update(t, atom, pos_integer) :: t defp do_update(leds, atom, offset) when is_atom(atom) do color_int = get_color_int(atom) do_update(leds, color_int ,offset) end defp do_update(leds, led, offset) do raise ArgumentError, message: "unknown data #{inspect leds}, #{inspect led}, #{inspect offset}" end @spec to_binary(t) :: binary def to_binary(%__MODULE__{count: count, leds: _leds, opts: _opts, meta: _meta}=leds) do Enum.reduce(1..count, <<>>, fn index, acc -> acc <> <> end) end @spec to_list(t) :: list[integer] def to_list(%__MODULE__{count: count, leds: _leds, opts: _opts, meta: _meta} = leds) do Enum.reduce(1..count, [], fn index, acc -> acc ++ [get_light(leds, index)] end) end @spec send(t, map) :: any def send(leds, opts \\ %{}) do namespace = opts[:namespace] || :default server_name = opts[:server_name] || Fledex.LedsDriver offset = opts[:offset] || 0 rotate_left = if opts[:rotate_left] != nil, do: opts[:rotate_left], else: true # we probably want to do some validation here and probably # want to optimise it a bit # a) is the server running? if Process.whereis(server_name) == nil do Logger.warn("The server wasn't started. You should start it before using this function") {:ok, _pid} = LedsDriver.start_link(%{}, server_name) end # b) Is a namespace defined? exists = LedsDriver.exist_namespace(namespace, server_name) if not exists do Logger.warn("The namespace hasn't been defined. This should be done before calling this function") LedsDriver.define_namespace(namespace, server_name) end vals = rotate(to_list(leds), offset, rotate_left) LedsDriver.set_leds(namespace, vals, server_name) end @spec get_light(t, pos_integer) :: colorint def get_light(%__MODULE__{leds: leds} = _leds, index) do case Map.fetch(leds, index) do {:ok, value} -> value _ -> 0 end end @spec rotate(list(colorint), pos_integer, boolean) :: list(colorint) def rotate(vals, offset, rotate_left \\ true) def rotate(vals, 0, _rotate_left), do: vals def rotate(vals, offset, rotate_left) do count = Enum.count(vals) offset = rem(offset, count) offset = if rotate_left, do: offset, else: count-offset Enum.slide(vals, 0..rem(offset-1 + count, count), count) end end