defmodule Fledex.LedsDriver do
@moduledoc """
This module defines a GenServer that manages the LED strip (be it a real one with the
SpiDriver or a virtual one with e.g. the KinoDriver). Usually you only want to start
one server, even though it is possible to start several.
The LedsDriver will take several Leds definitions and merge them together to be displayed
on a single LED strip
The role the LedsDriver plays is similar to the one a window server plays on a normal computer
"""
@behaviour GenServer
use Fledex.Color.Types
require Logger
alias Fledex.LedStripDriver.Driver
alias Fledex.LedStripDriver.LoggerDriver
alias Fledex.Color.Utils
@type t :: %{
timer: %{
disabled: boolean,
counter: pos_integer,
update_timeout: pos_integer,
update_func: (t -> t),
only_dirty_update: boolean,
is_dirty: boolean,
ref: reference
},
led_strip: %{
merge_strategy: atom,
driver_modules: module,
config: map
},
namespaces: map
}
@default_update_timeout 50
@default_driver_modules [LoggerDriver]
#client code
@spec start_link(map, atom | {:global, any} | {:via, atom, any}) ::
:ignore | {:error, any} | {:ok, pid}
def start_link(init_arg, server_name \\ __MODULE__) do
GenServer.start_link(__MODULE__, init_arg, name: server_name)
end
@spec define_namespace(atom, atom) :: ({:ok, atom} | {:error, String.t})
def define_namespace(namespace, server_name \\ __MODULE__) do
GenServer.call(server_name, {:define_namespace, namespace})
end
@spec drop_namespace(atom, atom) :: :ok
def drop_namespace(namespace, server_name \\ __MODULE__) do
GenServer.call(server_name, {:drop_namespace, namespace})
end
@spec exist_namespace(atom, atom) :: boolean
def exist_namespace(namespace, server_name \\ __MODULE__) do
GenServer.call(server_name, {:exist_namespace, namespace})
end
@spec set_leds(atom, list(pos_integer), atom) :: (:ok | {:error, String.t})
def set_leds(namespace, leds, server_name \\ __MODULE__) do
GenServer.call(server_name, {:set_leds, namespace, leds})
end
# server code
@impl true
@spec init(map) :: {:ok, t} | {:stop, String.t}
def init(init_args) when is_map(init_args) do
state = init_state(init_args)
state = if (state[:timer][:disabled] == false), do: start_timer(state), else: state
{:ok, state}
end
def init(_) do
{:stop, "Init args need to be a map"}
end
@spec init_state(map) :: t
def init_state(init_args) when is_map(init_args) do
driver_modules = case init_args[:led_strip][:driver_modules] do
nil ->
#Logger.warn("No driver_modules defined/ #{inspect @default_driver_modules} will be used")
@default_driver_modules
driver_modules when not is_list(driver_modules) ->
Logger.warn("driver_modules is not a list")
[driver_modules]
driver_modules ->
driver_modules
end
only_dirty_update = if init_args[:timer][:only_dirty_update] == nil do
false
else
init_args[:timer][:only_dirty_update]
end
is_dirty = if init_args[:timer][:is_dirty] == nil do
false
else
init_args[:timer][:is_dirty]
end
state = %{
timer: %{
disabled: init_args[:timer][:disabled] || false,
counter: init_args[:timer][:counter] || 0,
update_timeout: init_args[:timer][:update_timeout] || @default_update_timeout,
update_func: init_args[:timer][:update_func] || &transfer_data/1,
only_dirty_update: only_dirty_update,
is_dirty: is_dirty,
ref: nil
},
led_strip: %{
merge_strategy: init_args[:led_strip][:merge_strategy] || :avg,
driver_modules: driver_modules,
config: init_args[:led_strip][:config] || %{}
},
namespaces: init_args[:namespaces] || %{}
}
Driver.init(init_args, state)
end
@impl true
@spec terminate(reason, state :: Fledex.LedDriver.t) :: :ok
when reason: :normal | :shutdown | {:shutdown, term()} | term()
def terminate(reason, state) do
Driver.terminate(reason, state)
end
@spec start_timer(t) :: t
defp start_timer(state) do
update_timeout = state[:timer][:update_timeout]
update_func = state[:timer][:update_func]
ref = Process.send_after(self(), {:update_timeout, update_func}, update_timeout)
state = update_in(state, [:timer, :ref], fn (_current_ref) -> ref end )
state
end
@impl true
@spec handle_call({:define_namespace, atom}, {pid, any}, t) :: {:reply, ({:ok, atom} | {:error, binary}), t}
def handle_call({:define_namespace, name}, _from, %{namespaces: namespaces} = state) do
state = put_in(state, [:timer, :is_dirty], true)
case Map.has_key?(namespaces, name) do
false -> {:reply, {:ok, name}, %{state | namespaces: Map.put_new(namespaces, name, [])}}
true -> {:reply, {:error, "namespace already exists"}, state}
end
end
@impl true
@spec handle_call({:drop_namespace, atom}, {pid, any}, t) :: {:reply, :ok, t}
def handle_call({:drop_namespace, name}, _from, %{namespaces: namespaces} = state) do
state = put_in(state, [:timer, :is_dirty], true)
{:reply, :ok, %{state | namespaces: Map.drop(namespaces, [name])}}
end
@impl true
@spec handle_call({:exist_namespace, atom}, {pid, any}, t) :: {:reply, boolean, t}
def handle_call({:exist_namespace, name}, _from, %{namespaces: namespaces} = state) do
exists = case Map.fetch(namespaces, name) do
{:ok, _} -> true
_ -> false
end
{:reply, exists, state}
end
@impl true
@spec handle_call({:set_leds, atom, list(colorint)}, {pid, any}, t)
:: {:reply, (:ok | {:error, String.t}), t}
def handle_call({:set_leds, name, leds}, _from, %{namespaces: namespaces} = state) do
state = put_in(state, [:timer, :is_dirty], true)
case Map.has_key?(namespaces, name) do
true -> {:reply, :ok, %{state | namespaces: Map.put(namespaces, name, leds)}}
false -> {:reply, {:error, "no such namespace, you need to define one first with :define_namespace"}, state}
end
end
@impl true
@spec handle_info({:update_timeout, (t -> t)}, t) :: {:noreply, t}
def handle_info({:update_timeout, func}, state) do
# here should now come some processing for now we just increase the counter and reschdule the timer
state = update_in(state, [:timer, :counter], &(&1+1))
state = start_timer(state)
# Logger.info "calling #{inspect func}"
state = func.(state)
{:noreply, state}
end
@spec transfer_data(t) :: t
def transfer_data(%{timer: %{is_dirty: is_dirty, only_dirty_updates: only_dirty_updates}} = state) when only_dirty_updates == true and is_dirty == false do
# we shortcut if there is nothing to update and if we are allowed to shortcut
state
end
def transfer_data(state) do
# state = :telemetry.span(
# [:transfer_data],
# %{timer_counter: state.timer.counter},
# fn ->
state = state.namespaces
|> merge_namespaces(state.led_strip.merge_strategy)
|> Driver.transfer(state)
|> put_in([:timer,:is_dirty], false)
# {state, %{metadata: "done"}}
# end
# )
state
end
@spec merge_namespaces(map, atom) :: list(colorint)
def merge_namespaces(namespaces, merge_strategy) do
namespaces
|> get_leds()
|> merge_leds(merge_strategy)
end
@spec get_leds(map) :: list(list(colorint))
def get_leds(namespaces) do
Enum.reduce(namespaces, [], fn {_key, value}, acc ->
acc ++ [value]
end)
end
@spec merge_leds(list(list(colorint)), atom) :: list(colorint)
def merge_leds(leds, merge_strategy) do
leds = match_length(leds)
Enum.zip_with(leds, fn elems ->
merge_pixels(elems, merge_strategy)
end)
end
@spec match_length(list(list(colorint))) :: list(list(colorint))
def match_length(leds) when leds == nil, do: leds
def match_length(leds) when length(leds) == 0, do: leds
def match_length(leds) do
max_length = Enum.reduce(leds, 0, fn(sequence, acc) -> max(acc, length(sequence)) end)
Enum.map(leds, fn(sequence) -> extend(sequence, max_length - length(sequence)) end)
end
@spec extend(list(colorint), pos_integer) :: list(colorint)
def extend(sequence, 0), do: sequence
def extend(sequence, extra) do
extra_length = Enum.reduce(1..extra, [], fn(_index, acc) -> acc ++ [0x000000] end)
sequence ++ extra_length
end
@spec merge_pixels(list(colorint), atom) :: colorint
def merge_pixels(elems, merge_strategy) do
count = length(elems)
elems
|> Enum.map(fn elem -> Utils.split_into_subpixels(elem) end)
|> Utils.add_subpixels()
|> apply_merge_strategy(count, merge_strategy)
|> Utils.combine_subpixels()
end
@spec apply_merge_strategy({pos_integer, pos_integer, pos_integer}, pos_integer, atom) :: rgb
def apply_merge_strategy(rgb, count, merge_strategy) do
case merge_strategy do
:avg -> Utils.avg(rgb, count)
:cap -> Utils.cap(rgb)
_ -> raise "Unknown merge strategy"
end
end
end