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

6. Fledex: DSL

livebooks/6_fledex_dsl.livemd

6. Fledex: DSL

# 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"}
])

Intro

We now define our animations with our DSL with the following steps:

  • First we have to use Fledex in order to import Fledex, to load our DSL macros (led_strip and animation), and to import Fledex.Leds. This means we won’t write Leds in fron of functions. By default this will also start the Fledex.Animation.Manager if it’s not already running which will take care of all the animation coordinations.
use Fledex

The clock

  • We define a new strip with a name (:clock), and configure it (we use the Kino default driver for display). This will internally create a new Fledex.LedStrip server (if it’s not already running) that will control the strip. Its server name will be identical to the led_strip name.
  • Then within this strip we define several animations that correspond to an animatio (and will use internally the Fledex.Animation.Animator). Each animation is identified by its own name (:help, :hour, :minute, :second).
  • Within each animation we define a function that will be executed repeatedly.
  • Inside that function we can define one or several Leds, either directly or through an led sequence (like a rainbow function) and animate those leds depending on some trigger data (data that comes into the animation function). An explanation of what we are doing in our example here will be given after the code.

It is also possble to define some other configurations (like the send_config_func as we have done in the “Fledex Clock Example“) but we will look at this a bit later.

led_strip :clock, Kino do
  static :help do
    leds(5) |> light(davy_s_grey()) |> repeat(12)
  end

  animation :hour do
    _triggers ->
      %{hour: hour, minute: _minute, second: _second} = Time.utc_now() |> Time.add(1, :hour)
      leds(24) |> light(blue(), hour + 1)
  end

  animation :minute do
    _triggers ->
      %{hour: _hour, minute: minute, second: _second} = Time.utc_now() |> Time.add(1, :hour)
      leds(60) |> light(:may_green, minute + 1)
  end

  animation :second do
    _triggers ->
      %{hour: _hour, minute: _minute, second: second} = Time.utc_now() |> Time.add(1, :hour)
      leds(60) |> light(:red, second + 1)
  end
end

The above code should result in an animation displaying the clock.Now it’s time to look at the different animation parts and how they work.

We first define a static animation (one that doesn’t animate). This animation does not get any triggers and will only be called once during the initalization (since it’s static). Notice that this function gets no argument (we don’t have the trailing _triggers -> part). It is used to make it easier to read the time by having a dash every 5 steps and those do not change over time. The markers are repeating in a regular pattern, so we only define the first one and then repeat it with the repeat/2 function 12 times.

For all other animations we make a call to the system time (Time.utc_now()) and therefore don’t require the _trigger information. We shift the result to the right timezone (we add 1 hour, for UTC+1).

We pattern match the result, depending on the animation into hour, minute, or second. Note: the code is a bit extra bloated to pattern match it every time into all 3 and to comment out the ones we don’t need, but that is just to highlight that we have all 3 elements but we are only interested in one of them.

Exapmle: For the second indicator the following would have been enough:

  animation :second do
    _triggers ->
      %{second: second} = Time.utc_now() |> Time.add(1, :hour)
      leds(60) |> light(:red, second + 1)
  end

We then define a sequence of Leds of the right length (24 for hour, and 60 for minute and second). We switch-on only a single light in some color at the correct position (with the offset corresponding to hour, minute or second)

Note: in the clock example we defined a static sequence of Leds and offsetted it. This requires the definition of a send_config_func. We will look on how to do it with the DSL as a next step.

Clock (with send function)

We are now implementing the above example, but demonstrating on how the :second, :minute, and :hour indicators can be implemented in exactly the same way as we did in chapter 4, i.e. with a send function that shifts the red marker depending on the time.

We also take the ocasion to demonstrate on how you can define a second strip, by giving it a new name (:clock2). To demonstrate that it’s really a new stip this clock here displays the time in UTC (we don’t add the extra hour of offset). Look carefully at the blue hour indicator.

Also, the above strip should continue to run and a new strip will appear below the code. Had we called the strip the same as above (i.e. :clock), then the above output would have stopped updating and a new one would appear here.

Try it out and modify any of the two animations. They should be fully independent.

led_strip :clock2, Kino do
  static :help do
    leds(5) |> light(davy_s_grey()) |> repeat(12)
  end

  animation :hour,
    send_config: fn _triggers ->
      %{hour: hour, minute: _minute, second: _second} = Time.utc_now()
      %{offset: hour, rotate_left: false}
    end do
    _triggers ->
      leds(24) |> light(blue())
  end

  animation :minute,
    send_config: fn _triggers ->
      %{hour: _hour, minute: minute, second: _second} = Time.utc_now()
      %{offset: minute, rotate_left: false}
    end do
    _triggers ->
      leds(60) |> light(:may_green)
  end

  animation :second,
    send_config: fn _triggers ->
      %{hour: _hour, minute: _minute, second: second} = Time.utc_now()
      %{offset: second, rotate_left: false}
    end do
    _triggers ->
      leds(60) |> light(:red)
  end
end

The weather app

We now implement the weather app (indicating the temperature) we implemented in chapter 5.

led_strip :weather, Kino do
  animation :temperature do
    triggers ->
      temp = triggers[:temperature] || 100
      temp = round(temp)

      temp =
        if temp == 100 do
          100
        else
          temp = min(temp, 40)
          max(temp, -20)
        end

      {leds, offset} =
        case temp do
          temp when temp == 100 ->
            {leds(61)
             |> light(:blue, 1, 20)
             |> light(:may_green)
             |> light(:red, 22, 40), 1}

          temp when temp < 0 ->
            {leds(1) |> light(:blue) |> repeat(temp), 21 + temp}

          temp when temp == 0 ->
            {leds(1) |> light(:may_green), 21}

          temp when temp > 0 ->
            {leds(1) |> light(:red) |> repeat(temp), 22}
        end

      leds(61) |> light(leds, offset)
  end

  static :help do
    # we allow temperatures down to -20C and up to +40C
    negative_scale =
      leds(5)
      |> light(davy_s_grey())
      |> repeat(4)

    positive_scale =
      leds(5)
      |> light(davy_s_grey(), 5)
      |> repeat(8)

    leds(61)
    |> light(negative_scale)
    |> light(davy_s_grey())
    |> light(positive_scale)
  end
end

This time we don’t implement the connection to the weather API, but we only fake it. Fledex imports Fledex.Utils.PubSub and therefore we can use the broadcast_trigger/1 function directly.

broadcast_trigger(%{temperature: 17.2})