From f7cd56b06b66273c1d921cf2180e844e97016ce2 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Tue, 28 Jul 2020 20:35:59 -0700 Subject: [PATCH 1/8] add animation manager This moves responsibility for tracking animations and switching to next/previous animations out of the various paintables and into a single location --- lib/rgb_matrix/animation.ex | 4 +- lib/rgb_matrix/engine.ex | 50 ++++++++++----------- lib/xebow.ex | 74 +++++++++++++++++++++++++++++++ lib/xebow/application.ex | 5 ++- lib/xebow/keyboard.ex | 46 ++----------------- lib/xebow_web/live/matrix_live.ex | 46 ++++++++++--------- 6 files changed, 130 insertions(+), 95 deletions(-) diff --git a/lib/rgb_matrix/animation.ex b/lib/rgb_matrix/animation.ex index 93ebfd1..49f6f70 100644 --- a/lib/rgb_matrix/animation.ex +++ b/lib/rgb_matrix/animation.ex @@ -63,7 +63,7 @@ defmodule RGBMatrix.Animation do 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) + {0, animation_state} = animation_type.new(leds, animation_config) animation = %__MODULE__{ type: animation_type, @@ -71,7 +71,7 @@ defmodule RGBMatrix.Animation do state: animation_state } - {render_in, animation} + animation end @doc """ diff --git a/lib/rgb_matrix/engine.ex b/lib/rgb_matrix/engine.ex index 9f7c5f8..063ee42 100644 --- a/lib/rgb_matrix/engine.ex +++ b/lib/rgb_matrix/engine.ex @@ -27,10 +27,10 @@ defmodule RGBMatrix.Engine do - `initial_animation` - The Animation type to initialize and play when the engine starts. """ - @spec start_link({leds :: [LED.t()], initial_animation_type :: Animation.type()}) :: + @spec start_link(leds :: [LED.t()]) :: GenServer.on_start() - def start_link({leds, initial_animation_type}) do - GenServer.start_link(__MODULE__, {leds, initial_animation_type}, name: __MODULE__) + def start_link(leds) do + GenServer.start_link(__MODULE__, leds, name: __MODULE__) end @doc """ @@ -113,18 +113,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,13 +137,13 @@ 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 set_animation(state, animation) do + # # {render_in, animation} = Animation.new(animation_type, state.leds) + # + # %State{state | animation: animation} + # |> schedule_next_render(0) + # |> inform_configurables() + # end defp schedule_next_render(state, :ignore) do state @@ -202,8 +200,13 @@ 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 = set_animation(state, animation_type) + state = + %State{state | animation: animation} + |> schedule_next_render(0) + |> inform_configurables() + {:noreply, state} end @@ -230,11 +233,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) diff --git a/lib/xebow.ex b/lib/xebow.ex index 79affcd..82e5d84 100644 --- a/lib/xebow.ex +++ b/lib/xebow.ex @@ -37,4 +37,78 @@ defmodule Xebow do @spec layout() :: Layout.t() def layout, do: @layout + + use GenServer + + # Client Implementations: + + @spec start_link([]) :: GenServer.on_start() + def start_link([]) do + GenServer.start_link(__MODULE__, RGBMatrix.Animation.types(), name: __MODULE__) + end + + def next_animation do + GenServer.cast(__MODULE__, :next_animation) + end + + def previous_animation do + GenServer.cast(__MODULE__, :previous_animation) + end + + def get_animation_config do + GenServer.call(__MODULE__, :get_animation_config) + end + + # Server Implementations: + + @impl GenServer + def init(types) do + active_animations = + types + |> Enum.map(&initialize_animation/1) + + [current | _] = active_animations + RGBMatrix.Engine.set_animation(current) + state = {active_animations, []} + + {:ok, state} + end + + @impl GenServer + def handle_call(:get_animation_config, _caller, state) do + {[current | _rest], _previous} = state + {:reply, RGBMatrix.Animation.get_config(current), state} + end + + @impl GenServer + def handle_cast(:next_animation, state) do + case state do + {[current | []], previous} -> + remaining_next = Enum.reverse([current | previous]) + RGBMatrix.Engine.set_animation(hd(remaining_next)) + {:noreply, {remaining_next, []}} + + {[current | remaining_next], previous} -> + RGBMatrix.Engine.set_animation(hd(remaining_next)) + {:noreply, {remaining_next, [current | previous]}} + end + end + + @impl GenServer + def handle_cast(:previous_animation, state) do + case state do + {remaining_next, []} -> + [next | remaining_previous] = Enum.reverse(remaining_next) + RGBMatrix.Engine.set_animation(next) + {:noreply, {[next], remaining_previous}} + + {remaining_next, [next | remaining_previous]} -> + RGBMatrix.Engine.set_animation(next) + {:noreply, {[next | remaining_next], remaining_previous}} + end + end + + defp initialize_animation(animation_type) do + RGBMatrix.Animation.new(animation_type, @leds) + end end diff --git a/lib/xebow/application.ex b/lib/xebow/application.ex index c7a48bf..629ade0 100644 --- a/lib/xebow/application.ex +++ b/lib/xebow/application.ex @@ -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}, diff --git a/lib/xebow/keyboard.ex b/lib/xebow/keyboard.ex index 932e972..05adf29 100644 --- a/lib/xebow/keyboard.ex +++ b/lib/xebow/keyboard.ex @@ -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,7 +101,7 @@ defmodule Xebow.Keyboard do """ @spec next_animation() :: :ok def next_animation do - GenServer.cast(__MODULE__, :next_animation) + Xebow.next_animation() end @doc """ @@ -109,7 +109,7 @@ defmodule Xebow.Keyboard do """ @spec previous_animation() :: :ok def previous_animation do - GenServer.cast(__MODULE__, :previous_animation) + Xebow.previous_animation() end # Server @@ -141,51 +141,11 @@ defmodule Xebow.Keyboard do pins: pins, keyboard_state: keyboard_state, hid: hid, - animation_types: Animation.types(), - current_animation_index: 0 } {: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) diff --git a/lib/xebow_web/live/matrix_live.ex b/lib/xebow_web/live/matrix_live.ex index 83e8766..2ebe9ca 100644 --- a/lib/xebow_web/live/matrix_live.ex +++ b/lib/xebow_web/live/matrix_live.ex @@ -3,7 +3,7 @@ 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) @@ -15,14 +15,12 @@ defmodule XebowWeb.MatrixLive do @impl Phoenix.LiveView def mount(_params, _session, socket) do - {config, config_schema} = Engine.get_animation_config() + {config, config_schema} = Xebow.get_animation_config() initial_assigns = [ leds: make_view_leds(@black_frame), config: config, config_schema: config_schema, - animation_types: Animation.types(), - current_animation_index: 0 ] initial_assigns = @@ -76,36 +74,40 @@ defmodule XebowWeb.MatrixLive do @impl Phoenix.LiveView def handle_event("next_animation", %{}, socket) do - next_index = socket.assigns.current_animation_index + 1 + Xebow.next_animation() + {:noreply, socket} + # 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 + # 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) + # animation_type = Enum.at(socket.assigns.animation_types, next_index) - RGBMatrix.Engine.set_animation(animation_type) + # RGBMatrix.Engine.set_animation(animation_type) - {:noreply, assign(socket, current_animation_index: next_index)} + # {:noreply, assign(socket, current_animation_index: next_index)} end @impl Phoenix.LiveView def handle_event("previous_animation", %{}, socket) do - previous_index = socket.assigns.current_animation_index - 1 + Xebow.previous_animation() + {:noreply, socket} + # 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 + # 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) + # animation_type = Enum.at(socket.assigns.animation_types, previous_index) - RGBMatrix.Engine.set_animation(animation_type) + # RGBMatrix.Engine.set_animation(animation_type) - {:noreply, assign(socket, current_animation_index: previous_index)} + # {:noreply, assign(socket, current_animation_index: previous_index)} end @impl Phoenix.LiveView From 6b8ff683c4e5b07bef7a50a438fcbafbbcdbf814 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Tue, 28 Jul 2020 21:44:20 -0700 Subject: [PATCH 2/8] make Animation.new only return state All animations have an initial render delay of 0ms for their initial frame. --- lib/rgb_matrix/animation.ex | 6 +++--- lib/rgb_matrix/animation/breathing.ex | 2 +- lib/rgb_matrix/animation/cycle_all.ex | 2 +- lib/rgb_matrix/animation/hue_wave.ex | 2 +- lib/rgb_matrix/animation/pinwheel.ex | 2 +- lib/rgb_matrix/animation/random_keypresses.ex | 3 +-- lib/rgb_matrix/animation/random_solid.ex | 2 +- lib/rgb_matrix/animation/solid_color.ex | 2 +- lib/rgb_matrix/animation/solid_reactive.ex | 2 +- lib/rgb_matrix/engine.ex | 8 -------- 10 files changed, 11 insertions(+), 20 deletions(-) diff --git a/lib/rgb_matrix/animation.ex b/lib/rgb_matrix/animation.ex index 49f6f70..872632c 100644 --- a/lib/rgb_matrix/animation.ex +++ b/lib/rgb_matrix/animation.ex @@ -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,11 +59,11 @@ 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() - {0, animation_state} = animation_type.new(leds, animation_config) + animation_state = animation_type.new(leds, animation_config) animation = %__MODULE__{ type: animation_type, diff --git a/lib/rgb_matrix/animation/breathing.ex b/lib/rgb_matrix/animation/breathing.ex index a55206c..67cf9be 100644 --- a/lib/rgb_matrix/animation/breathing.ex +++ b/lib/rgb_matrix/animation/breathing.ex @@ -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 diff --git a/lib/rgb_matrix/animation/cycle_all.ex b/lib/rgb_matrix/animation/cycle_all.ex index 55942f4..5b2ebb6 100644 --- a/lib/rgb_matrix/animation/cycle_all.ex +++ b/lib/rgb_matrix/animation/cycle_all.ex @@ -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 diff --git a/lib/rgb_matrix/animation/hue_wave.ex b/lib/rgb_matrix/animation/hue_wave.ex index 47b8047..665c89d 100644 --- a/lib/rgb_matrix/animation/hue_wave.ex +++ b/lib/rgb_matrix/animation/hue_wave.ex @@ -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 diff --git a/lib/rgb_matrix/animation/pinwheel.ex b/lib/rgb_matrix/animation/pinwheel.ex index c5813bb..aa7d7d1 100644 --- a/lib/rgb_matrix/animation/pinwheel.ex +++ b/lib/rgb_matrix/animation/pinwheel.ex @@ -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 diff --git a/lib/rgb_matrix/animation/random_keypresses.ex b/lib/rgb_matrix/animation/random_keypresses.ex index 380e640..33679b6 100644 --- a/lib/rgb_matrix/animation/random_keypresses.ex +++ b/lib/rgb_matrix/animation/random_keypresses.ex @@ -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} - }} + } end @impl true diff --git a/lib/rgb_matrix/animation/random_solid.ex b/lib/rgb_matrix/animation/random_solid.ex index a604489..6b05805 100644 --- a/lib/rgb_matrix/animation/random_solid.ex +++ b/lib/rgb_matrix/animation/random_solid.ex @@ -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 diff --git a/lib/rgb_matrix/animation/solid_color.ex b/lib/rgb_matrix/animation/solid_color.ex index 855f1e3..311e016 100644 --- a/lib/rgb_matrix/animation/solid_color.ex +++ b/lib/rgb_matrix/animation/solid_color.ex @@ -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 diff --git a/lib/rgb_matrix/animation/solid_reactive.ex b/lib/rgb_matrix/animation/solid_reactive.ex index 633fce7..e583218 100644 --- a/lib/rgb_matrix/animation/solid_reactive.ex +++ b/lib/rgb_matrix/animation/solid_reactive.ex @@ -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 diff --git a/lib/rgb_matrix/engine.ex b/lib/rgb_matrix/engine.ex index 063ee42..02f1f4b 100644 --- a/lib/rgb_matrix/engine.ex +++ b/lib/rgb_matrix/engine.ex @@ -71,14 +71,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. """ From 2b70005d0a6cf29d8436210d638107a534df8c41 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Tue, 28 Jul 2020 22:35:23 -0700 Subject: [PATCH 3/8] persist config changes in memory --- lib/rgb_matrix/animation.ex | 4 +++- lib/rgb_matrix/engine.ex | 8 -------- lib/xebow.ex | 36 +++++++++++++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/rgb_matrix/animation.ex b/lib/rgb_matrix/animation.ex index 872632c..4418793 100644 --- a/lib/rgb_matrix/animation.ex +++ b/lib/rgb_matrix/animation.ex @@ -114,6 +114,8 @@ defmodule RGBMatrix.Animation do config = config_module.update(config, params) - %{animation | config: config} + animation = %{animation | config: config} + :ok = Xebow.update_animation_config(animation) + animation end end diff --git a/lib/rgb_matrix/engine.ex b/lib/rgb_matrix/engine.ex index 02f1f4b..2312bfd 100644 --- a/lib/rgb_matrix/engine.ex +++ b/lib/rgb_matrix/engine.ex @@ -129,14 +129,6 @@ defmodule RGBMatrix.Engine do %State{state | paintables: paintables} end - # defp set_animation(state, animation) do - # # {render_in, animation} = Animation.new(animation_type, state.leds) - # - # %State{state | animation: animation} - # |> schedule_next_render(0) - # |> inform_configurables() - # end - defp schedule_next_render(state, :ignore) do state end diff --git a/lib/xebow.ex b/lib/xebow.ex index 82e5d84..16750b5 100644 --- a/lib/xebow.ex +++ b/lib/xebow.ex @@ -1,5 +1,8 @@ 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} @@ -47,16 +50,37 @@ defmodule Xebow do GenServer.start_link(__MODULE__, RGBMatrix.Animation.types(), name: __MODULE__) end + @doc """ + Gets the current animation configuration. This retrievs current values, which + allows for changes to be made with `update_animation_config/2` + """ + @spec get_animation_config() :: RGBMatrix.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 - def get_animation_config do - GenServer.call(__MODULE__, :get_animation_config) + @doc """ + Updates the animation configuration for the current animation + """ + @spec update_animation_config(RGBMatrix.Animation.type()) :: :ok | :error + def update_animation_config(animation_with_config) do + GenServer.call(__MODULE__, {:update_animation_config, animation_with_config}) end # Server Implementations: @@ -80,6 +104,12 @@ defmodule Xebow do {:reply, RGBMatrix.Animation.get_config(current), state} end + @impl GenServer + def handle_call({:update_animation_config, animation_with_config}, _caller, state) do + {[_current | rest], previous} = state + {:reply, :ok, {[animation_with_config | rest], previous}} + end + @impl GenServer def handle_cast(:next_animation, state) do case state do From 73dfd4aa43d1a8f98c8a2fc4ef33320236e3fb43 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Wed, 29 Jul 2020 00:29:19 -0700 Subject: [PATCH 4/8] provide current frame to live view mount --- lib/rgb_matrix/animation.ex | 8 +--- lib/rgb_matrix/animation/random_keypresses.ex | 10 ++--- lib/rgb_matrix/engine.ex | 13 +++--- lib/xebow.ex | 21 +++++----- lib/xebow/keyboard.ex | 2 +- lib/xebow/leds.ex | 2 +- lib/xebow_web/live/matrix_live.ex | 40 +++++-------------- 7 files changed, 36 insertions(+), 60 deletions(-) diff --git a/lib/rgb_matrix/animation.ex b/lib/rgb_matrix/animation.ex index 4418793..2245619 100644 --- a/lib/rgb_matrix/animation.ex +++ b/lib/rgb_matrix/animation.ex @@ -65,13 +65,11 @@ defmodule RGBMatrix.Animation do animation_config = config_module.new() animation_state = animation_type.new(leds, animation_config) - animation = %__MODULE__{ + %__MODULE__{ type: animation_type, config: animation_config, state: animation_state } - - animation end @doc """ @@ -114,8 +112,6 @@ defmodule RGBMatrix.Animation do config = config_module.update(config, params) - animation = %{animation | config: config} - :ok = Xebow.update_animation_config(animation) - animation + %{animation | config: config} end end diff --git a/lib/rgb_matrix/animation/random_keypresses.ex b/lib/rgb_matrix/animation/random_keypresses.ex index 33679b6..5823b84 100644 --- a/lib/rgb_matrix/animation/random_keypresses.ex +++ b/lib/rgb_matrix/animation/random_keypresses.ex @@ -22,11 +22,11 @@ defmodule RGBMatrix.Animation.RandomKeypresses do def new(leds, _config) do led_ids = Enum.map(leds, & &1.id) - %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 diff --git a/lib/rgb_matrix/engine.ex b/lib/rgb_matrix/engine.ex index 2312bfd..961bafe 100644 --- a/lib/rgb_matrix/engine.ex +++ b/lib/rgb_matrix/engine.ex @@ -41,15 +41,17 @@ defmodule RGBMatrix.Engine do GenServer.cast(__MODULE__, {:set_animation, animation_type}) end + @typep frame :: %{LED.t() => RGBMatrix.any_color_model()} + @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 """ @@ -185,7 +187,6 @@ defmodule RGBMatrix.Engine do @impl GenServer def handle_cast({:set_animation, animation}, state) do - # state = set_animation(state, animation_type) state = %State{state | animation: animation} |> schedule_next_render(0) @@ -208,7 +209,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 @@ -225,7 +226,7 @@ defmodule RGBMatrix.Engine do %State{state | animation: animation} |> inform_configurables() - {:reply, :ok, state} + {:reply, Xebow.update_animation_config(animation), state} end @impl GenServer diff --git a/lib/xebow.ex b/lib/xebow.ex index 16750b5..6e514a3 100644 --- a/lib/xebow.ex +++ b/lib/xebow.ex @@ -5,6 +5,7 @@ defmodule Xebow do """ alias Layout.{Key, LED} + alias RGBMatrix.{Animation, Engine} @leds [ LED.new(:l001, 0, 0), @@ -47,14 +48,14 @@ defmodule Xebow do @spec start_link([]) :: GenServer.on_start() def start_link([]) do - GenServer.start_link(__MODULE__, RGBMatrix.Animation.types(), name: __MODULE__) + GenServer.start_link(__MODULE__, Animation.types(), name: __MODULE__) end @doc """ Gets the current animation configuration. This retrievs current values, which allows for changes to be made with `update_animation_config/2` """ - @spec get_animation_config() :: RGBMatrix.Animation.Config.t() + @spec get_animation_config() :: {Animation.Config.t(), keyword(Animation.Config.t())} def get_animation_config do GenServer.call(__MODULE__, :get_animation_config) end @@ -78,7 +79,7 @@ defmodule Xebow do @doc """ Updates the animation configuration for the current animation """ - @spec update_animation_config(RGBMatrix.Animation.type()) :: :ok | :error + @spec update_animation_config(Animation.t()) :: :ok | :error def update_animation_config(animation_with_config) do GenServer.call(__MODULE__, {:update_animation_config, animation_with_config}) end @@ -92,7 +93,7 @@ defmodule Xebow do |> Enum.map(&initialize_animation/1) [current | _] = active_animations - RGBMatrix.Engine.set_animation(current) + Engine.set_animation(current) state = {active_animations, []} {:ok, state} @@ -101,7 +102,7 @@ defmodule Xebow do @impl GenServer def handle_call(:get_animation_config, _caller, state) do {[current | _rest], _previous} = state - {:reply, RGBMatrix.Animation.get_config(current), state} + {:reply, Animation.get_config(current), state} end @impl GenServer @@ -115,11 +116,11 @@ defmodule Xebow do case state do {[current | []], previous} -> remaining_next = Enum.reverse([current | previous]) - RGBMatrix.Engine.set_animation(hd(remaining_next)) + Engine.set_animation(hd(remaining_next)) {:noreply, {remaining_next, []}} {[current | remaining_next], previous} -> - RGBMatrix.Engine.set_animation(hd(remaining_next)) + Engine.set_animation(hd(remaining_next)) {:noreply, {remaining_next, [current | previous]}} end end @@ -129,16 +130,16 @@ defmodule Xebow do case state do {remaining_next, []} -> [next | remaining_previous] = Enum.reverse(remaining_next) - RGBMatrix.Engine.set_animation(next) + Engine.set_animation(next) {:noreply, {[next], remaining_previous}} {remaining_next, [next | remaining_previous]} -> - RGBMatrix.Engine.set_animation(next) + Engine.set_animation(next) {:noreply, {[next | remaining_next], remaining_previous}} end end defp initialize_animation(animation_type) do - RGBMatrix.Animation.new(animation_type, @leds) + Animation.new(animation_type, @leds) end end diff --git a/lib/xebow/keyboard.ex b/lib/xebow/keyboard.ex index 05adf29..fa93aa1 100644 --- a/lib/xebow/keyboard.ex +++ b/lib/xebow/keyboard.ex @@ -140,7 +140,7 @@ defmodule Xebow.Keyboard do state = %{ pins: pins, keyboard_state: keyboard_state, - hid: hid, + hid: hid } {:ok, state} diff --git a/lib/xebow/leds.ex b/lib/xebow/leds.ex index 79de259..1c39abc 100644 --- a/lib/xebow/leds.ex +++ b/lib/xebow/leds.ex @@ -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) diff --git a/lib/xebow_web/live/matrix_live.ex b/lib/xebow_web/live/matrix_live.ex index 2ebe9ca..d299c12 100644 --- a/lib/xebow_web/live/matrix_live.ex +++ b/lib/xebow_web/live/matrix_live.ex @@ -20,14 +20,18 @@ defmodule XebowWeb.MatrixLive do initial_assigns = [ leds: make_view_leds(@black_frame), config: config, - config_schema: config_schema, + config_schema: config_schema ] initial_assigns = if connected?(socket) do - {paint_fn, config_fn} = register_with_engine!() + {paint_fn, config_fn, frame} = 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, + leds: make_view_leds(frame) + ) else initial_assigns end @@ -76,38 +80,12 @@ defmodule XebowWeb.MatrixLive do def handle_event("next_animation", %{}, socket) do Xebow.next_animation() {:noreply, socket} - # 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)} end @impl Phoenix.LiveView def handle_event("previous_animation", %{}, socket) do Xebow.previous_animation() {:noreply, socket} - # 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)} end @impl Phoenix.LiveView @@ -119,7 +97,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}) @@ -139,7 +117,7 @@ defmodule XebowWeb.MatrixLive do end end) - {paint_fn, config_fn} + {paint_fn, config_fn, frame} end defp make_view_leds(frame) do From 7f5d16c10e4a4649b3708a129af5db39c1565cc6 Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Wed, 29 Jul 2020 00:36:55 -0700 Subject: [PATCH 5/8] always use some non-black frame on live view mount --- lib/xebow_web/live/matrix_live.ex | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/xebow_web/live/matrix_live.ex b/lib/xebow_web/live/matrix_live.ex index d299c12..bd92070 100644 --- a/lib/xebow_web/live/matrix_live.ex +++ b/lib/xebow_web/live/matrix_live.ex @@ -6,31 +6,23 @@ defmodule XebowWeb.MatrixLive do 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} = 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 ] initial_assigns = if connected?(socket) do - {paint_fn, config_fn, frame} = register_with_engine!() - Keyword.merge(initial_assigns, paint_fn: paint_fn, - config_fn: config_fn, - leds: make_view_leds(frame) + config_fn: config_fn ) else initial_assigns From de998ce0cdbc1122aa9ab5d50518edbaffe4495e Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Wed, 29 Jul 2020 22:20:29 -0700 Subject: [PATCH 6/8] code review changes - engine.ex Completely removes config functions from Engine --- lib/rgb_matrix/engine.ex | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/lib/rgb_matrix/engine.ex b/lib/rgb_matrix/engine.ex index 961bafe..772eaef 100644 --- a/lib/rgb_matrix/engine.ex +++ b/lib/rgb_matrix/engine.ex @@ -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,13 +24,10 @@ 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()]) :: - GenServer.on_start() + @spec start_link(leds :: [LED.t()]) :: GenServer.on_start() def start_link(leds) do GenServer.start_link(__MODULE__, leds, name: __MODULE__) end @@ -36,13 +35,11 @@ defmodule RGBMatrix.Engine do @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 - @typep frame :: %{LED.t() => RGBMatrix.any_color_model()} - @doc """ Register a paint function for the engine to send frames to. @@ -73,14 +70,6 @@ defmodule RGBMatrix.Engine do GenServer.cast(__MODULE__, {:interact, led}) 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. @@ -218,17 +207,6 @@ defmodule RGBMatrix.Engine do {:reply, :ok, 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, Xebow.update_animation_config(animation), state} - end - @impl GenServer def handle_call({:register_configurable, config_fn}, _from, state) do state = add_configurable(config_fn, state) From c887e52faf8bbe50d81ef3f34e287b7ee4c616ec Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Wed, 29 Jul 2020 22:21:42 -0700 Subject: [PATCH 7/8] code review changes - xebow.ex Changes state into a struct and improves the config update function --- lib/xebow.ex | 124 +++++++++++++++++++++--------- lib/xebow_web/live/matrix_live.ex | 2 +- 2 files changed, 90 insertions(+), 36 deletions(-) diff --git a/lib/xebow.ex b/lib/xebow.ex index 6e514a3..3613535 100644 --- a/lib/xebow.ex +++ b/lib/xebow.ex @@ -42,18 +42,31 @@ 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_animation, :next_list, :previous_list] + 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 current animation configuration. This retrievs current values, which - allows for changes to be made with `update_animation_config/2` + 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 @@ -77,11 +90,12 @@ defmodule Xebow do end @doc """ - Updates the animation configuration for the current animation + Updates the configuration for the current animation """ - @spec update_animation_config(Animation.t()) :: :ok | :error - def update_animation_config(animation_with_config) do - GenServer.call(__MODULE__, {:update_animation_config, animation_with_config}) + @spec update_animation_config(animation_params) :: + :ok + def update_animation_config(params) do + GenServer.cast(__MODULE__, {:update_animation_config, params}) end # Server Implementations: @@ -92,53 +106,93 @@ defmodule Xebow do types |> Enum.map(&initialize_animation/1) - [current | _] = active_animations + current = hd(active_animations) Engine.set_animation(current) - state = {active_animations, []} - {:ok, state} - end + state = %State{ + current_animation: current, + next_list: active_animations, + previous_list: [] + } - @impl GenServer - def handle_call(:get_animation_config, _caller, state) do - {[current | _rest], _previous} = state - {:reply, Animation.get_config(current), state} + {:ok, state} end @impl GenServer - def handle_call({:update_animation_config, animation_with_config}, _caller, state) do - {[_current | rest], previous} = state - {:reply, :ok, {[animation_with_config | rest], previous}} + def handle_call(:get_animation_config, _from, state) do + {:reply, Animation.get_config(state.current_animation), state} end @impl GenServer def handle_cast(:next_animation, state) do - case state do - {[current | []], previous} -> - remaining_next = Enum.reverse([current | previous]) - Engine.set_animation(hd(remaining_next)) - {:noreply, {remaining_next, []}} - - {[current | remaining_next], previous} -> - Engine.set_animation(hd(remaining_next)) - {:noreply, {remaining_next, [current | previous]}} + case state.next_list do + [] -> + [new_current | next_list] = Enum.reverse([state.current_animation | state.previous_list]) + Engine.set_animation(new_current) + + state = %{ + state + | current_animation: new_current, + next_list: next_list, + previous_list: [] + } + + {:noreply, state} + + [new_current | next_list] -> + Engine.set_animation(new_current) + + state = %{ + state + | current_animation: new_current, + next_list: next_list, + previous_list: [state.current_animation | state.previous_list] + } + + {:noreply, state} end end @impl GenServer def handle_cast(:previous_animation, state) do - case state do - {remaining_next, []} -> - [next | remaining_previous] = Enum.reverse(remaining_next) - Engine.set_animation(next) - {:noreply, {[next], remaining_previous}} - - {remaining_next, [next | remaining_previous]} -> - Engine.set_animation(next) - {:noreply, {[next | remaining_next], remaining_previous}} + case state.previous_list do + [] -> + [new_current | previous_list] = Enum.reverse([state.current_animation | state.next_list]) + Engine.set_animation(new_current) + + state = %{ + state + | current_animation: new_current, + next_list: [], + previous_list: previous_list + } + + {:noreply, state} + + [new_current | previous_list] -> + Engine.set_animation(new_current) + + state = %{ + state + | current_animation: new_current, + next_list: [state.current_animation | state.next_list], + previous_list: previous_list + } + + {:noreply, state} end end + @impl GenServer + def handle_cast({:update_animation_config, params}, state) do + updated_animation = Animation.update_config(state.current_animation, params) + Engine.set_animation(updated_animation) + + state = %{state | current_animation: updated_animation} + + {:noreply, state} + end + defp initialize_animation(animation_type) do Animation.new(animation_type, @leds) end diff --git a/lib/xebow_web/live/matrix_live.ex b/lib/xebow_web/live/matrix_live.ex index bd92070..0e31ca1 100644 --- a/lib/xebow_web/live/matrix_live.ex +++ b/lib/xebow_web/live/matrix_live.ex @@ -63,7 +63,7 @@ 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 From ea6f45fd1f4ae0dc827b0b6d5a725829c0f13a8c Mon Sep 17 00:00:00 2001 From: Jesse Van Volkinburg Date: Thu, 30 Jul 2020 10:18:40 -0700 Subject: [PATCH 8/8] use map to hold animations Allows instant access to a given index without modifying or traversing lists --- lib/xebow.ex | 112 +++++++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/lib/xebow.ex b/lib/xebow.ex index 3613535..aef60ac 100644 --- a/lib/xebow.ex +++ b/lib/xebow.ex @@ -47,7 +47,7 @@ defmodule Xebow do defmodule State do @moduledoc false - defstruct [:current_animation, :next_list, :previous_list] + defstruct [:current_index, :active_animations, :count_of_active_animations] end use GenServer @@ -92,8 +92,7 @@ defmodule Xebow do @doc """ Updates the configuration for the current animation """ - @spec update_animation_config(animation_params) :: - :ok + @spec update_animation_config(animation_params) :: :ok def update_animation_config(params) do GenServer.cast(__MODULE__, {:update_animation_config, params}) end @@ -102,97 +101,78 @@ defmodule Xebow do @impl GenServer def init(types) do + count_of_active_animations = length(types) + active_animations = types - |> Enum.map(&initialize_animation/1) - - current = hd(active_animations) - Engine.set_animation(current) + |> Stream.map(&initialize_animation/1) + |> Stream.with_index() + |> Stream.map(fn {animation, index} -> {index, animation} end) + |> Enum.into(%{}) state = %State{ - current_animation: current, - next_list: active_animations, - previous_list: [] + 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 - {:reply, Animation.get_config(state.current_animation), state} + config = Animation.get_config(current_animation(state)) + {:reply, config, state} end @impl GenServer def handle_cast(:next_animation, state) do - case state.next_list do - [] -> - [new_current | next_list] = Enum.reverse([state.current_animation | state.previous_list]) - Engine.set_animation(new_current) - - state = %{ - state - | current_animation: new_current, - next_list: next_list, - previous_list: [] - } - - {:noreply, state} - - [new_current | next_list] -> - Engine.set_animation(new_current) - - state = %{ - state - | current_animation: new_current, - next_list: next_list, - previous_list: [state.current_animation | state.previous_list] - } - - {:noreply, state} - end + 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 - case state.previous_list do - [] -> - [new_current | previous_list] = Enum.reverse([state.current_animation | state.next_list]) - Engine.set_animation(new_current) - - state = %{ - state - | current_animation: new_current, - next_list: [], - previous_list: previous_list - } - - {:noreply, state} - - [new_current | previous_list] -> - Engine.set_animation(new_current) - - state = %{ - state - | current_animation: new_current, - next_list: [state.current_animation | state.next_list], - previous_list: previous_list - } - - {:noreply, state} - end + 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(state.current_animation, params) - Engine.set_animation(updated_animation) + updated_animation = Animation.update_config(current_animation(state), params) - state = %{state | current_animation: updated_animation} + 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