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

8. Fledex: Component

livebooks/8_fledex_component.livemd

8. Fledex: Component

Mix.install([
  {:fledex, "~>0.5"}
])

Setup

use Fledex

The Weather animation (with a component)

Instead of defining the complicated logic of the termometer indicator, we use this time a component that has all this logic build in. The component defines our configuration and all we have to do is to specify the mandatory (and if desired optional) parameters.

alias Fledex.Component.Thermometer

led_strip :john, Kino do
  component(:thermo, Thermometer,
    range: -20..40,
    trigger: :temperature,
    negative: :blue,
    null: :may_green,
    positive: :red,
    marker: :davy_s_grey
  )
end
Fledex.Utils.PubSub.broadcast(:fledex, "trigger", {:trigger, %{temperature: 17.2}})

To test our animation we fake again the temperature by simply publishing the desired temperature.

What is a component

The question now is just “what is a component”?

The component is simply a module that implements the @behaviour Fledex.Component.Interface. Only a single configure/2 function is required that returns a Fledex.Animation.Manager.configs_t structure and thus can define anything you normaly would define through the animation, static, … macros.

To get the correct behaviour the component gets 2 parameters:

  1. The name of the component: Each animation gets it’s own name, so does the component. It should be noted that an component can create several animations and the component should make sure to name each of those individually. The termometer has two “animations” the scale and the markers on the scale. The former gets the name of the component and the latter gets the name with .helper attached.
  2. a keyword list of options: The list of parameters allows us to configure our component. Each component can define their own set of components. Probably a reoccuring parameter is :trigger_name wich allows us to configure runtime parameters, but this is by convention only.

As you have seen a component is nothing special but it allows to redefine a reusable structure. A component can be defined and distributed through an external library.

Netsted components

We now take a slightly different approach for creating our component. As we have seen above a component is simply a configuration and the dsl is simply an easier way to write a config. Thus, we can reuse one within the other.

alias Fledex.Component.Dot

led_strip :nested_components, Kino do
  component(:minute, Dot, color: :red, count: 60, trigger_name: :minute)
  component(:hour, Dot, color: :blue, count: 24, trigger_name: :hour)

  static :helper do
    leds(5) |> light(:davy_s_grey, 5) |> repeat(12)
  end
end
broadcast_trigger(%{hour: 17, minute: 34})

The interesting thing in the Fledex.Component.Dot component is that it actually defines the component by using the Fledex.animation/3 macro. The code looks like the following

  def configure(name, options) when is_atom(name) and is_list(options) do
    use Fledex
    # ... here comes some option extraction stuff
    animation name do
      triggers when is_map(triggers) and is_map_key(triggers, trigger_name) ->
        leds(count) |> light(color, triggers[trigger_name])
      _ -> leds()
    end
  end

The clock component

The Fledex.Component.Clock component is an interesting one, because it defines several animations and even a static pattern. For that it uses the special led_strip driver :config that simply returns the config instead of delegating to the Fledex.Animation.Manager.

Each animation requires its own unique name and this is accomplished with a small utility function that combines the strip name (the one we get in) and an additional fraction.

  def configure(name, options) do
    # ... here comes some option extraction stuff 
    use Fledex
    led_strip name, :config do
      component :minute, Dot, color: minute_color, count: 60, trigger_name: create_name(trigger_name, :minute)
      component :hour, Dot, color: hour_color, count: 24, trigger_name: create_name(trigger_name, :hour)
      static create_name(trigger_name, :helper) do
        leds(5) |> light(helper_color, 5) |> repeat(12)
      end
    end
  end

As you can see it defines, more or less, the same animations as our example above. Thus, instead of us defining the different animations, the component is doing this for us. To use the component, we now simply have to use the clock and pass in the necessary options, and that’s it!

alias Fledex.Component.Clock
import Crontab.CronExpression

led_strip :nested_components2, Kino do
  component(:myclock, Clock, trigger_name: :clock)
end

And to see the correct time, we only need to publish the appropriate time. When we do so we shouldn’t forget that the animations are reacting to two triggers that we passed in. We need to publish the triggers :clock_hour and :clock_minute as triggers (or if we like add even teh :clock_second too).

broadcast_trigger(%{clock_hour: 17, clock_minute: 34, clock_second: 12})

A job to drive the clock

Regular updates is something so common that we’ll look at the job macro that will allows to schedule udpates in regular intervals. We will take a closer look at this in another livebook but here already a sneak preview.

alias Fledex.Component.Clock

led_strip :nested_components3, Kino do
  component(:myclock2, Clock, trigger_name: :clock2)

  job :clock, ~e[* * * * * * *]e do
    %DateTime{hour: hour, minute: minute, second: second} = DateTime.utc_now()

    broadcast_trigger(%{
      clock2_hour: hour,
      clock2_minute: minute,
      clock2_second: second
    })
  end
end

Debugging tricks

When you develop components it can sometimes be difficult to debug them especially if you nest other components into your own. You easily will run into the question “Why does my component not show what I expect it to show?”. The issue is usually that the triggers are not properly configured. But figuring out where it goes wrong is not so easy.

There is a quite simply trick to debug a component by adding the following animation to your component:

  animation :logger do
    %{logger: logger} = trigger ->
      if rem(logger, 10) == 0 do
        IO.puts "logger: #{inspect trigger}"
      end
      {leds(), %{trigger | logger: logger+1}}
    trigger -> {leds(), Map.put_new(trigger, :logger, 0)}
  end

It will log the triggers in regular intervals so you see what the component receives and you can then compare it with what you would expect. As you can see this animation has no effect on your animation, because it always returns empty leds(). We only use the side effect (writing to IO.puts) of this animation.