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

Move animation management out of Engine #95

Merged
merged 8 commits into from
Aug 1, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions lib/rgb_matrix/animation.ex
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ defmodule RGBMatrix.Animation do
}
defstruct [:type, :config, :state]

@callback new(leds :: [LED.t()], config :: Config.t()) :: {render_in, animation_state}
@callback new(leds :: [LED.t()], config :: Config.t()) :: animation_state
@callback render(state :: animation_state, config :: Config.t()) ::
{render_in, [RGBMatrix.any_color_model()], animation_state}
@callback interact(state :: animation_state, config :: Config.t(), led :: LED.t()) ::
@@ -59,19 +59,17 @@ defmodule RGBMatrix.Animation do
@doc """
Returns an animation's initial state.
"""
@spec new(animation_type :: type, leds :: [LED.t()]) :: {render_in, t}
@spec new(animation_type :: type, leds :: [LED.t()]) :: t
def new(animation_type, leds) do
config_module = Module.concat([animation_type, "Config"])
animation_config = config_module.new()
{render_in, animation_state} = animation_type.new(leds, animation_config)
animation_state = animation_type.new(leds, animation_config)

animation = %__MODULE__{
%__MODULE__{
type: animation_type,
config: animation_config,
state: animation_state
}

{render_in, animation}
end

@doc """
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/breathing.ex
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ defmodule RGBMatrix.Animation.Breathing do
def new(leds, _config) do
color = HSV.new(40, 100, 100)
led_ids = Enum.map(leds, & &1.id)
{0, %State{color: color, tick: 0, speed: 100, led_ids: led_ids}}
%State{color: color, tick: 0, speed: 100, led_ids: led_ids}
end

@impl true
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/cycle_all.ex
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ defmodule RGBMatrix.Animation.CycleAll do
@impl true
def new(leds, _config) do
led_ids = Enum.map(leds, & &1.id)
{0, %State{tick: 0, speed: 100, led_ids: led_ids}}
%State{tick: 0, speed: 100, led_ids: led_ids}
end

@impl true
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/hue_wave.ex
Original file line number Diff line number Diff line change
@@ -51,7 +51,7 @@ defmodule RGBMatrix.Animation.HueWave do
@impl true
def new(leds, config) do
steps = 360 / config.width
{0, %State{tick: 0, leds: leds, steps: steps}}
%State{tick: 0, leds: leds, steps: steps}
end

@impl true
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/pinwheel.ex
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ defmodule RGBMatrix.Animation.Pinwheel do

@impl true
def new(leds, _config) do
{0, %State{tick: 0, speed: 100, leds: leds, center: determine_center(leds)}}
%State{tick: 0, speed: 100, leds: leds, center: determine_center(leds)}
end

defp determine_center(leds) do
11 changes: 5 additions & 6 deletions lib/rgb_matrix/animation/random_keypresses.ex
Original file line number Diff line number Diff line change
@@ -22,12 +22,11 @@ defmodule RGBMatrix.Animation.RandomKeypresses do
def new(leds, _config) do
led_ids = Enum.map(leds, & &1.id)

{0,
%State{
led_ids: led_ids,
# NOTE: as to not conflict with possible led ID of `:all`
dirty: {:all}
}}
%State{
led_ids: led_ids,
# NOTE: as to not conflict with possible led ID of `:all`
dirty: {:all}
}
end

@impl true
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/random_solid.ex
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ defmodule RGBMatrix.Animation.RandomSolid do

@impl true
def new(leds, _config) do
{0, %State{led_ids: Enum.map(leds, & &1.id)}}
%State{led_ids: Enum.map(leds, & &1.id)}
end

@impl true
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/solid_color.ex
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ defmodule RGBMatrix.Animation.SolidColor do
@impl true
def new(leds, _config) do
color = HSV.new(120, 100, 100)
{0, %State{color: color, led_ids: Enum.map(leds, & &1.id)}}
%State{color: color, led_ids: Enum.map(leds, & &1.id)}
end

@impl true
2 changes: 1 addition & 1 deletion lib/rgb_matrix/animation/solid_reactive.ex
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ defmodule RGBMatrix.Animation.SolidReactive do
@impl true
def new(leds, _config) do
color = HSV.new(190, 100, 100)
{0, %State{first_render: true, paused: false, tick: 0, color: color, leds: leds, hits: %{}}}
%State{first_render: true, paused: false, tick: 0, color: color, leds: leds, hits: %{}}
end

@impl true
91 changes: 26 additions & 65 deletions lib/rgb_matrix/engine.ex
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@ defmodule RGBMatrix.Engine do
alias Layout.LED
alias RGBMatrix.Animation

@type frame :: %{LED.t() => RGBMatrix.any_color_model()}

defmodule State do
@moduledoc false
defstruct [:leds, :animation, :paintables, :last_frame, :timer, :configurables]
@@ -22,34 +24,31 @@ defmodule RGBMatrix.Engine do
This module registers its process globally and is expected to be started by a
supervisor.
This function accepts the following arguments as a tuple:
This function accepts the following argument:
- `leds` - The list of LEDs to be painted on.
- `initial_animation` - The Animation type to initialize and play when the
engine starts.
"""
@spec start_link({leds :: [LED.t()], initial_animation_type :: Animation.type()}) ::
GenServer.on_start()
def start_link({leds, initial_animation_type}) do
GenServer.start_link(__MODULE__, {leds, initial_animation_type}, name: __MODULE__)
@spec start_link(leds :: [LED.t()]) :: GenServer.on_start()
def start_link(leds) do
GenServer.start_link(__MODULE__, leds, name: __MODULE__)
end

@doc """
Sets the given animation as the currently active animation.
"""
@spec set_animation(animation_type :: Animation.type()) :: :ok
def set_animation(animation_type) do
GenServer.cast(__MODULE__, {:set_animation, animation_type})
@spec set_animation(animation :: Animation.t()) :: :ok
def set_animation(animation) do
GenServer.cast(__MODULE__, {:set_animation, animation})
end

@doc """
Register a paint function for the engine to send frames to.
This function is idempotent.
"""
@spec register_paintable(paint_fn :: function) :: {:ok, function}
@spec register_paintable(paint_fn :: function) :: {:ok, function, frame}
def register_paintable(paint_fn) do
:ok = GenServer.call(__MODULE__, {:register_paintable, paint_fn})
{:ok, paint_fn}
{:ok, frame} = GenServer.call(__MODULE__, {:register_paintable, paint_fn})
{:ok, paint_fn, frame}
end

@doc """
@@ -71,22 +70,6 @@ defmodule RGBMatrix.Engine do
GenServer.cast(__MODULE__, {:interact, led})
end

@doc """
Retrieves the current animation's configuration and configuration schema.
"""
@spec get_animation_config() :: {config :: any, config_schema :: any}
def get_animation_config do
GenServer.call(__MODULE__, :get_animation_config)
end

@doc """
Updates the current animation's configuration.
"""
@spec update_animation_config(params :: map) :: :ok | :error
def update_animation_config(params) do
GenServer.call(__MODULE__, {:update_animation_config, params})
end

@doc """
Register a config function for the engine to send animation configuration to
when it changes.
@@ -113,18 +96,16 @@ defmodule RGBMatrix.Engine do
# Server

@impl GenServer
def init({leds, initial_animation_type}) do
def init(leds) do
black = Chameleon.HSV.new(0, 0, 0)
frame = Map.new(leds, &{&1.id, black})

state =
%State{
leds: leds,
last_frame: frame,
paintables: MapSet.new(),
configurables: MapSet.new()
}
|> init_and_set_animation(initial_animation_type)
state = %State{
leds: leds,
last_frame: frame,
paintables: MapSet.new(),
configurables: MapSet.new()
}

{:ok, state}
end
@@ -139,14 +120,6 @@ defmodule RGBMatrix.Engine do
%State{state | paintables: paintables}
end

defp init_and_set_animation(state, animation_type) do
{render_in, animation} = Animation.new(animation_type, state.leds)

%State{state | animation: animation}
|> schedule_next_render(render_in)
|> inform_configurables()
end

defp schedule_next_render(state, :ignore) do
state
end
@@ -202,8 +175,12 @@ defmodule RGBMatrix.Engine do
end

@impl GenServer
def handle_cast({:set_animation, animation_type}, state) do
state = init_and_set_animation(state, animation_type)
def handle_cast({:set_animation, animation}, state) do
state =
%State{state | animation: animation}
|> schedule_next_render(0)
|> inform_configurables()

{:noreply, state}
end

@@ -221,7 +198,7 @@ defmodule RGBMatrix.Engine do
@impl GenServer
def handle_call({:register_paintable, paint_fn}, _from, state) do
state = add_paintable(paint_fn, state)
{:reply, :ok, state}
{:reply, {:ok, state.last_frame}, state}
end

@impl GenServer
@@ -230,22 +207,6 @@ defmodule RGBMatrix.Engine do
{:reply, :ok, state}
end

@impl GenServer
def handle_call(:get_animation_config, _from, state) do
{:reply, Animation.get_config(state.animation), state}
end

@impl GenServer
def handle_call({:update_animation_config, params}, _from, state) do
animation = Animation.update_config(state.animation, params)

state =
%State{state | animation: animation}
|> inform_configurables()

{:reply, :ok, state}
end

@impl GenServer
def handle_call({:register_configurable, config_fn}, _from, state) do
state = add_configurable(config_fn, state)
141 changes: 140 additions & 1 deletion lib/xebow.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
defmodule Xebow do
@moduledoc false
@moduledoc """
Xebow is an Elixir-based firmware for keyboards. Currently, it is working on the Raspberry Pi0
Keybow kit.
"""

alias Layout.{Key, LED}
alias RGBMatrix.{Animation, Engine}

@leds [
LED.new(:l001, 0, 0),
@@ -37,4 +41,139 @@ defmodule Xebow do

@spec layout() :: Layout.t()
def layout, do: @layout

@type animations :: [Animation.t()]
@type animation_params :: %{String.t() => atom | number | String.t()}

defmodule State do
@moduledoc false
defstruct [:current_index, :active_animations, :count_of_active_animations]
end

use GenServer

# Client Implementations:

@doc """
Starts the Xebow application, which manages initialization of animations, as well
as switching between active animations. It also maintains animation config state
and persists it in memory between animation changes.
"""
@spec start_link([]) :: GenServer.on_start()
def start_link([]) do
GenServer.start_link(__MODULE__, Animation.types(), name: __MODULE__)
end

@doc """
Gets the animation configuration. This retrievs current values, which allows for
changes to be made with `update_animation_config/1`
"""
@spec get_animation_config() :: {Animation.Config.t(), keyword(Animation.Config.t())}
def get_animation_config do
GenServer.call(__MODULE__, :get_animation_config)
end

@doc """
Switches to the next active animation
"""
@spec next_animation() :: :ok
def next_animation do
GenServer.cast(__MODULE__, :next_animation)
end

@doc """
Switches to the previous active animation
"""
@spec previous_animation() :: :ok
def previous_animation do
GenServer.cast(__MODULE__, :previous_animation)
end

@doc """
Updates the configuration for the current animation
"""
@spec update_animation_config(animation_params) :: :ok
def update_animation_config(params) do
GenServer.cast(__MODULE__, {:update_animation_config, params})
end

# Server Implementations:

@impl GenServer
def init(types) do
count_of_active_animations = length(types)

active_animations =
types
|> Stream.map(&initialize_animation/1)
|> Stream.with_index()
|> Stream.map(fn {animation, index} -> {index, animation} end)
|> Enum.into(%{})

state = %State{
current_index: 0,
active_animations: active_animations,
count_of_active_animations: count_of_active_animations
}

Engine.set_animation(current_animation(state))

{:ok, state}
end

@impl GenServer
def handle_call(:get_animation_config, _from, state) do
config = Animation.get_config(current_animation(state))
{:reply, config, state}
end

@impl GenServer
def handle_cast(:next_animation, state) do
count_of_active_animations = state.count_of_active_animations

new_index =
case state.current_index + 1 do
i when i >= count_of_active_animations -> 0
i -> i
end

state = %State{state | current_index: new_index}
Engine.set_animation(current_animation(state))
{:noreply, state}
end

@impl GenServer
def handle_cast(:previous_animation, state) do
new_index =
case state.current_index - 1 do
i when i < 0 -> state.count_of_active_animations - 1
i -> i
end

state = %State{state | current_index: new_index}
Engine.set_animation(current_animation(state))
{:noreply, state}
end

@impl GenServer
def handle_cast({:update_animation_config, params}, state) do
updated_animation = Animation.update_config(current_animation(state), params)

state =
put_in(
state.active_animations[state.current_index],
updated_animation
)

Engine.set_animation(current_animation(state))
{:noreply, state}
end

defp current_animation(state) do
state.active_animations[state.current_index]
end

defp initialize_animation(animation_type) do
Animation.new(animation_type, @leds)
end
end
5 changes: 3 additions & 2 deletions lib/xebow/application.ex
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@ defmodule Xebow.Application do
use Application

@leds Xebow.layout() |> Layout.leds()
@animation_type RGBMatrix.Animation.types() |> List.first()

def start(_type, _args) do
# See https://hexdocs.pm/elixir/Supervisor.html
@@ -18,7 +17,9 @@ defmodule Xebow.Application do
# Children for all targets
# Starts a worker by calling: Xebow.Worker.start_link(arg)
# {Xebow.Worker, arg},
{RGBMatrix.Engine, {@leds, @animation_type}},
# Engine must be started before Xebow
{RGBMatrix.Engine, @leds},
Xebow,
# Phoenix:
XebowWeb.Telemetry,
{Phoenix.PubSub, name: Xebow.PubSub},
48 changes: 4 additions & 44 deletions lib/xebow/keyboard.ex
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ defmodule Xebow.Keyboard do
use GenServer

alias Circuits.GPIO
alias RGBMatrix.{Animation, Engine}
alias RGBMatrix.Engine

# maps the physical GPIO pins to key IDs
@gpio_pins %{
@@ -101,15 +101,15 @@ defmodule Xebow.Keyboard do
"""
@spec next_animation() :: :ok
def next_animation do
GenServer.cast(__MODULE__, :next_animation)
Xebow.next_animation()
end

@doc """
Cycle to the previous animation
"""
@spec previous_animation() :: :ok
def previous_animation do
GenServer.cast(__MODULE__, :previous_animation)
Xebow.previous_animation()
end

# Server
@@ -140,52 +140,12 @@ defmodule Xebow.Keyboard do
state = %{
pins: pins,
keyboard_state: keyboard_state,
hid: hid,
animation_types: Animation.types(),
current_animation_index: 0
hid: hid
}

{:ok, state}
end

@impl GenServer
def handle_cast(:next_animation, state) do
next_index = state.current_animation_index + 1

next_index =
case next_index < Enum.count(state.animation_types) do
true -> next_index
_ -> 0
end

animation_type = Enum.at(state.animation_types, next_index)

RGBMatrix.Engine.set_animation(animation_type)

state = %{state | current_animation_index: next_index}

{:noreply, state}
end

@impl GenServer
def handle_cast(:previous_animation, state) do
previous_index = state.current_animation_index - 1

previous_index =
case previous_index < 0 do
true -> Enum.count(state.animation_types) - 1
_ -> previous_index
end

animation_type = Enum.at(state.animation_types, previous_index)

RGBMatrix.Engine.set_animation(animation_type)

state = %{state | current_animation_index: previous_index}

{:noreply, state}
end

@impl GenServer
def handle_info({:hid_report, hid_report}, state) do
IO.binwrite(state.hid, hid_report)
2 changes: 1 addition & 1 deletion lib/xebow/leds.ex
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ defmodule Xebow.LEDs do
defp register_with_engine!(spidev) do
pid = self()

{:ok, paint_fn} =
{:ok, paint_fn, _frame} =
Engine.register_paintable(fn frame ->
if Process.alive?(pid) do
paint(spidev, frame)
60 changes: 16 additions & 44 deletions lib/xebow_web/live/matrix_live.ex
Original file line number Diff line number Diff line change
@@ -3,33 +3,27 @@ defmodule XebowWeb.MatrixLive do

use XebowWeb, :live_view

alias RGBMatrix.{Animation, Engine}
alias RGBMatrix.Engine

@layout Xebow.layout()
@black Chameleon.HSV.new(0, 0, 0)
@black_frame @layout
|> Layout.leds()
|> Map.new(fn led ->
{led.id, @black}
end)

@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{config, config_schema} = Engine.get_animation_config()
{config, config_schema} = Xebow.get_animation_config()
{paint_fn, config_fn, frame} = register_with_engine!()

initial_assigns = [
leds: make_view_leds(@black_frame),
leds: make_view_leds(frame),
config: config,
config_schema: config_schema,
animation_types: Animation.types(),
current_animation_index: 0
config_schema: config_schema
]

initial_assigns =
if connected?(socket) do
{paint_fn, config_fn} = register_with_engine!()

Keyword.merge(initial_assigns, paint_fn: paint_fn, config_fn: config_fn)
Keyword.merge(initial_assigns,
paint_fn: paint_fn,
config_fn: config_fn
)
else
initial_assigns
end
@@ -69,43 +63,21 @@ defmodule XebowWeb.MatrixLive do
def handle_event("update_config", %{"_target" => [field_str]} = params, socket) do
value = Map.fetch!(params, field_str)

Engine.update_animation_config(%{field_str => value})
Xebow.update_animation_config(%{field_str => value})

{:noreply, socket}
end

@impl Phoenix.LiveView
def handle_event("next_animation", %{}, socket) do
next_index = socket.assigns.current_animation_index + 1

next_index =
case next_index < Enum.count(socket.assigns.animation_types) do
true -> next_index
_ -> 0
end

animation_type = Enum.at(socket.assigns.animation_types, next_index)

RGBMatrix.Engine.set_animation(animation_type)

{:noreply, assign(socket, current_animation_index: next_index)}
Xebow.next_animation()
{:noreply, socket}
end

@impl Phoenix.LiveView
def handle_event("previous_animation", %{}, socket) do
previous_index = socket.assigns.current_animation_index - 1

previous_index =
case previous_index < 0 do
true -> Enum.count(socket.assigns.animation_types) - 1
_ -> previous_index
end

animation_type = Enum.at(socket.assigns.animation_types, previous_index)

RGBMatrix.Engine.set_animation(animation_type)

{:noreply, assign(socket, current_animation_index: previous_index)}
Xebow.previous_animation()
{:noreply, socket}
end

@impl Phoenix.LiveView
@@ -117,7 +89,7 @@ defmodule XebowWeb.MatrixLive do
defp register_with_engine! do
pid = self()

{:ok, paint_fn} =
{:ok, paint_fn, frame} =
Engine.register_paintable(fn frame ->
if Process.alive?(pid) do
send(pid, {:render, frame})
@@ -137,7 +109,7 @@ defmodule XebowWeb.MatrixLive do
end
end)

{paint_fn, config_fn}
{paint_fn, config_fn, frame}
end

defp make_view_leds(frame) do