Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

3. Fledex: Animations

livebooks/3_fledex_animations.livemd

3. Fledex: Animations

# In this example we'll use the simple setup (experiments are inteded to not run on real
# hardware but on a normal livebook, This makes it easier to stay up-to-date and experiment
# We start to define our fledex library, directly linking to the latest version on github
Mix.install([
  {:fledex, "~>0.5"}
])

# we define a couple of aliases to not have to type that much
alias Fledex.Animation.Manager
alias Fledex.Leds
alias Fledex.LedStrip
alias Fledex.Driver.Impl.Kino
alias Fledex.Color.Correction

{:ok, pid} = Manager.start_link()

:ok

Preparation

To start with, we define a couple of aliases to make our life easier and more importantly, we start our Fledex.Animation.Manager. We will use it to create animations. The animations are actually maintained by the Fledex.Animation.Animator, but we will interact with it only through the Fledex.Animation.Manager.

Helper functions

We define a bit of helper functions that we will use when we define our animations. Those helper functions are the actual definitions of how the ledstrip will look.

Note that the functions get in a map (here called triggers) that can contain serveral triggers. It will definitely contain one trigger (caused by the Fledex.LedStrip) with the name of the led strip. The structure does not need to be used, but can be used in order to change the animation based on the trigger. Here we use the trigger in the send configuration to specify an offset to the leds and thereby create a movement.

defmodule HelperFunctions do
  @rainbow Leds.leds(50)
           |> Leds.rainbow(%{
             num_leds: 50,
             reversed: true
           })
  def rainbow(_triggers) do
    @rainbow
  end

  def send_config(triggers) do
    %{offset: triggers[:john]}
  end
end

Creating a first animation

Now it’s time to define our first animation by using the Fledex.Animation.Manager. Before we can define our animations we first have to define (and configure) our led strip. The Fledex.Animation.Manager can manage serveral strips at the same time.

We call our led strip :john (it has to be an atom). In this example we will only use the simple kino driver (and configure it with defaults) and thereby display the result within our livebook.

We then define an animation configuration that consists of a map with they key being the name of the animation and the value being the configuration of that animation. Each animation configuration needs to specify the :type as :animation, because (as we will see later) other types exist, and a :def_func that defines our animation (potentially with an additional :send_config_func as shown here. We’ll look at this in more detail in a later section).

Manager.register_strip(:john, [{Kino, []}], [])

Manager.register_config(:john, %{
  caine: %{
    type: :animation,
    def_func: &HelperFunctions.rainbow/1,
    send_config_func: &HelperFunctions.send_config/1
  }
})

Replacing our animation

We can now redefine our animation by registering different functions. Here we define a 3 led wide red dot that moves slowly from right to left.

Note: we are starting to use color names instead of defining it through an integer. If you want to learn more, then take a small detour to the Color livebook

import Bitwise

defmodule HelperFunctions2 do
  @red_leds Leds.leds(50) |> Leds.light(:red) |> Leds.light(:red) |> Leds.light(:red)
  def red_leds(_triggers) do
    @red_leds
  end

  def send_config(triggers) do
    slow_offset = rem(triggers[:john] || 0 >>> 5, 50)
    %{offset: slow_offset}
  end
end

Manager.register_config(:john, %{
  caine: %{
    type: :animation,
    def_func: &HelperFunctions2.red_leds/1,
    send_config_func: &HelperFunctions2.send_config/1
  }
})

Defining serveral animations

It’s also possible to define serveral animations. Here we continue with our previous animation (we do have to explicitly specify it again, otherwise it would be removed) and add a new animation with 3 leds that move faster from right to left.

import Bitwise

defmodule HelperFunctions3 do
  @rgb_leds Leds.leds(50) |> Leds.light(:red) |> Leds.light(:green) |> Leds.light(:blue)
  def rgb_leds(_triggers) do
    @rgb_leds
  end

  def send_config(triggers) do
    faster_offset = 50 - rem(triggers[:john] || 0 >>> 2, 50)
    %{offset: faster_offset}
  end
end

Manager.register_config(:john, %{
  caine: %{
    type: :animation,
    def_func: &HelperFunctions2.red_leds/1,
    send_config_func: &HelperFunctions2.send_config/1
  },
  doe: %{
    type: :animation,
    def_func: &HelperFunctions3.rgb_leds/1,
    send_config_func: &HelperFunctions3.send_config/1
  }
})

Use of anonymous functions

It is also possible to use an anonymous function during the definition which avoids to create a separate module. Here we redefine our above definition, but this time we define :doe with an anonymous function. To really see that we have changed the definition we define the leds as all blue. In addition we change the send_config_func to animate the dots even faster.

The advantage of using the anonymous functions is that you don’t get any compilation error, that occures if you recompile the module. Try it out by reevaluating the below definition several times and then doing the same thing with the previous definition.

This allows for a smooth replacement of definitions.

Manager.register_config(:john, %{
  caine: %{
    type: :animation,
    def_func: &HelperFunctions2.red_leds/1,
    send_config_func: &HelperFunctions2.send_config/1
  },
  doe: %{
    type: :animation,
    def_func: fn _triggers ->
      Leds.leds(50) |> Leds.light(:blue) |> Leds.light(:blue) |> Leds.light(:blue)
    end,
    send_config_func: fn triggers ->
      faster_offset = 50 - rem(triggers[:john] || 0 >>> 1, 50)
      %{offset: faster_offset}
    end
  }
})

From now on we will only define our definitions through the use of anonymous functions.