defmodule Delux.Effects do @moduledoc """ Functions for creating a variety of LED patterns """ alias Delux.Program alias Delux.RGB @long_time 3_600_000 @typedoc """ Option effects (none yet) """ @type options() :: [] @doc """ All LEDs off """ @spec off() :: Program.t() def off() do %Program{ red: led_off(), green: led_off(), blue: led_off(), description: "off", duration: :infinity } end @doc """ Set an indicator to the specified color """ @spec on(RGB.t(), options()) :: Program.t() def on(c, _options \\ []) do {r, g, b} = RGB.new(c) %Program{ red: led_on(r), green: led_on(g), blue: led_on(b), description: RGB.to_ansidata(c, "solid "), duration: :infinity } end @doc """ Blink an indicator This returns a pattern that blinks the specified color at a 50% duty cycle. The pattern starts on and then goes off. """ @spec blink(RGB.t(), number(), options()) :: Program.t() def blink(c, frequency, _options \\ []) do {r, g, b} = RGB.new(c) %Program{ red: led_blink(r, frequency), green: led_blink(g, frequency), blue: led_blink(b, frequency), description: [RGB.to_ansidata(c, ""), " at #{frequency} Hz"], duration: :infinity } end @doc """ Create a transient two color sequence The first color is shown for 20 ms. 10 ms in, the second color is shown for 20 ms. The Effects is a quick flash of light that can be used to show feedback to a button. Total duration of the Effects is 40 ms. """ @spec blip(RGB.t(), RGB.t(), options()) :: Program.t() def blip(c1, c2, _options \\ []) do {r1, g1, b1} = RGB.new(c1) {r2, g2, b2} = RGB.new(c2) %Program{ red: led_blip(r1, r2), green: led_blip(g1, g2), blue: led_blip(b1, b2), description: [RGB.to_ansidata(c1), :reset, "->", RGB.to_ansidata(c2), " blip"], duration: 40 } end @doc """ Cycle between colors Colors are shown with equal duration determined from the specified frequency. """ @spec cycle([RGB.t()], number(), options()) :: Program.t() def cycle(colors, frequency, _options \\ []) when is_list(colors) and frequency > 0 and frequency < 20 do {reds, greens, blues} = colors |> Enum.map(&RGB.new/1) |> unzip3() duration = round(1000.0 / Enum.count(colors) / frequency) %Program{ red: led_cycle(reds, duration), green: led_cycle(greens, duration), blue: led_cycle(blues, duration), description: [ Enum.map(colors, &RGB.to_ansidata/1) |> Enum.intersperse([:reset, "-"]), :reset, " cycle at #{frequency} Hz" ], duration: :infinity } end @doc """ Create a program from an arbitrary function Pass in a function that takes times in milliseconds and returns colors. The returned pattern piecewise linearly interpolates the waveform. """ @spec waveform(fun(), non_neg_integer(), options()) :: Program.t() def waveform(fun, period, options \\ []) do time_step = 100 colors = for t <- 0..period//time_step, do: fun.(t, options) {reds, greens, blues} = unzip3(colors) %Program{ red: led_waveform(reds, time_step), green: led_waveform(greens, time_step), blue: led_waveform(blues, time_step), description: "waveform", duration: :infinity } end defp unzip3(tuples, acc \\ {[], [], []}) defp unzip3([], {a1, a2, a3}), do: {Enum.reverse(a1), Enum.reverse(a2), Enum.reverse(a3)} defp unzip3([{x, y, z} | rest], {a1, a2, a3}), do: unzip3(rest, {[x | a1], [y | a2], [z | a3]}) defp led_off(), do: [{0, @long_time}, {0, 0}] defp led_on(b), do: [{b, @long_time}, {b, 0}] defp led_blink(0, _frequency), do: led_off() defp led_blink(b, frequency) when frequency > 0 and frequency < 20 do on_time = round(500 / frequency) [{b, on_time}, {b, 0}, {0, on_time}, {0, 0}] end defp led_blink(b, frequency) when frequency >= 20, do: led_on(b) defp led_blip(0, 0), do: led_off() defp led_blip(0, b2), do: [{0, 20}, {0, 0}, {b2, 20}, {b2, 0} | led_off()] defp led_blip(b1, 0), do: [{0, 10}, {0, 0}, {b1, 20}, {b1, 0} | led_off()] defp led_blip(b1, b2), do: [{0, 10}, {0, 0}, {b1, 30}, {b2, 0} | led_off()] defp led_cycle(values, duration) do Enum.flat_map(values, fn b -> [{b, duration}, {b, 0}] end) end defp led_waveform(values, time_step) do Enum.map(values, fn v -> {v, time_step} end) end end