Powered by AppSignal & Oban Pro

2. Fledex: Some simple tests

livebooks/2_fledex_first_steps.livemd

2. Fledex: Some simple tests

# We start to define our library that we want to use. If you are running on a laptop
# the libraries will be loaded, but on a nerves_livebook you can only use
# what has been compiled into your image
Mix.install([
  {:fledex, "~>0.6"}
])

Defining a strip

Before we can define our strip, we need to do a bit of preparation work. This won’t be necessary in the future (once we get to the DSL) because there everything will be taken care for us.

There is however no real magic in that setup. We define a couple of modules, so that we don’t have to type that much and then we’ll start our AnimationSystem which is required even if we don’t want to display any animation. It provides us with a stable infrastructure.

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

# then we start our system
AnimationSystem.start_link()

Fledex has several drivers which can also be used in parallel. It’s probably best to only use a single driver to start with. The Fledex.Driver.Impl.Kino driver does not have any hardware dependency and therefore is probably a good start to ensure that the library is loaded correctly.

At this point, we won’t go into the details what all the settings really mean, so just accept them for what they are. What is important to understand is the first parameter (:john) is the name of our strip through which we will refer to it. You can use any atom as a name.

This allows us to have several strips at the same time. Whenever we interact with the LedStrip we need to use that name to specify which strip we mean.

AnimationSystem.start_led_strip(
  :john,
  [
    {Kino, [update_freq: 1]}
  ]
)

Setting individual LEDs

Now we are ready to send some data to the led_strip. Before we send some color information, we first define a namspace (:default) to which we will do the drawing. Each led strip can have several namespaces that can operate independently and that get merged at the end to the same led strip (similar to a window in a window server). We’ll see later why this is useful.

For now we use a hex code to define colors. You can learn more about colors and how to define them in chapter 3b.

LedStrip.define_namespace(:john, :default)
LedStrip.set_leds(:john, :default, [0xFF0000, 0x00FF00, 0x0000FF], 3)

Note: if you don’t see your led strip displayed, then you might have to scroll up a bit. It should be just after the code block where you defined your led_strip.

Helper functions

We now define a couple of helper functions. The first 2 functions (red/1 and blue/1) are talking directly with the LED driver, the latter two (rainbow/1 and gradient/1) are making use of the Leds client module that helps to create nice effects like a rainbow or a gradient.

All functions are trying to create a simple animation by configuring the leds, sleeping for a while, and then reconfiguring the leds.

defmodule Helpers do
  def red(namespace) do
    Enum.each(1..10, fn _index ->
      LedStrip.set_leds(:john, namespace, [0xFF0000, 0x000000, 0x000000, 0x000000, 0x000000], 5)
      Process.sleep(600)
      LedStrip.set_leds(:john, namespace, [0x000000, 0xFF0000, 0x000000, 0x000000, 0x000000], 5)
      Process.sleep(600)
      LedStrip.set_leds(:john, namespace, [0x000000, 0x000000, 0xFF0000, 0x000000, 0x000000], 5)
      Process.sleep(600)
      LedStrip.set_leds(:john, namespace, [0x000000, 0x000000, 0x000000, 0xFF0000, 0x000000], 5)
      Process.sleep(600)
      LedStrip.set_leds(:john, namespace, [0x000000, 0x000000, 0x000000, 0x000000, 0xFF0000], 5)
      Process.sleep(600)
    end)
  end

  def blue(namespace) do
    Enum.each(1..10, fn _index ->
      LedStrip.set_leds(:john, namespace, [
        0x000000,
        0x000000,
        0x000000,
        0x000000,
        0x0000FF,
        0x00FF00
      ], 6)

      Process.sleep(600)

      LedStrip.set_leds(:john, namespace, [
        0x000000,
        0x000000,
        0x000000,
        0x0000FF,
        0x000000,
        0x000000
      ], 6)

      Process.sleep(600)

      LedStrip.set_leds(:john, namespace, [
        0x000000,
        0x000000,
        0x0000FF,
        0x000000,
        0x000000,
        0x00FF00
      ], 6)

      Process.sleep(600)

      LedStrip.set_leds(:john, namespace, [
        0x000000,
        0x0000FF,
        0x000000,
        0x000000,
        0x000000,
        0x000000
      ], 6)

      Process.sleep(600)

      LedStrip.set_leds(:john, namespace, [
        0x0000FF,
        0x000000,
        0x000000,
        0x000000,
        0x000000,
        0x00FF00
      ], 6)

      Process.sleep(600)
    end)
  end

  def rainbow(namespace) do
    Enum.each(0..10000, fn index ->
      config = [
        num_leds: 50,
        reversed: true
      ]

      Leds.leds(50)
      |> Leds.set_led_strip_info(:john, namespace)
      |> Leds.rainbow(config)
      |> Leds.send(offset: index)

      # before sending the next update we sleep a bit
      Process.sleep(100)
    end)
  end

  def gradient(namespace) do
    Enum.each(0..10000, fn index ->
      Leds.leds(50)
      |> Leds.set_led_strip_info(:john, namespace)
      |> Leds.gradient(0xFF0000, 0x0000FF, %{num_leds: 50})
      |> Leds.send(
        offset: index,
        rotate_left: true
      )

      # before sending the next update we sleep a bit
      Process.sleep(100)
    end)
  end
end

Take note, that our functions create sequences of different length.

Red and blue

Now we can try it out with one of the first functions. Let’s call our red/1 function. We spawn it in a separate process. The reason for that you’ll see in a second.

Note: you might have to scroll up to see the animation

LedStrip.define_namespace(:john, :red)

spawn(fn ->
  Helpers.red(:red)
end)

Similarly you can do the same with the blue/1 function. Where it gets interesting is, when you start first the red function and then start the blue function while the red function is still running.

Note: Don’t forget to use a differnt namespace name compared to the above function, otherwise one function would constantly overwrite what the other function has defined. We want them to live in parallel and get merged.

LedStrip.define_namespace(:john, :blue)

spawn(fn ->
  Helpers.blue(:blue)
end)

While both functions are running, they will

Rainbow

Now we can use the above functions with our trip. We can run the functions by spawning a new thread. This allows to also see what happens if another thread works in parallel. Here we create a rainbow effect that will be send with a changing offset so that the rainbow rotates through the LEDs

LedStrip.define_namespace(:john, :rainbow)
spawn(fn -> Helpers.rainbow(:rainbow) end)

Gradient

Here we create a gradient between red and blue. Also this one we let rotate through. Note, depending on whether you started the rainbow above or not, you might see different results. Try this section with and without running the Rainbow section.

LedStrip.define_namespace(:john, :gradient)
spawn(fn -> Helpers.gradient(:gradient) end)

We can drop the namespaces to see what happens

LedStrip.drop_namespace(:john, :gradient)

As we can see our animation stops to be visible and our calls to set_leds will fail (but we swallow those errors).

Several animations

This test is running a couple of different ligth definitions in parallel.

LedStrip.define_namespace(:john, :john)
LedStrip.define_namespace(:john, :jane)
LedStrip.define_namespace(:john, :default)

spawn(fn -> Helpers.blue(:jane) end)
spawn(fn -> Helpers.red(:john) end)
spawn(fn -> Helpers.rainbow(:default) end)

As you can see the animations overlap each other. We will work with this principle going forward.