From 7be9c568f3d53639c05120b03f8b146b90529ee6 Mon Sep 17 00:00:00 2001 From: maze Date: Mon, 27 Nov 2023 23:13:48 +0100 Subject: [PATCH] Adding leds size function renaming conversion functions removing commented out code better type checking adding kino renderers for Leds and for colors fixing bug in name loader adding type to Names module adding guard for color names adding a color type adding more Leds tests updating TODO list creating a livebook to give an introduction to colors and leds --- TODO.md | 6 +- lib/fledex/color/kino_render.ex | 64 ++++++ lib/fledex/color/load_utils.ex | 9 +- lib/fledex/color/names.ex | 6 + lib/fledex/color/types.ex | 9 +- lib/fledex/color/utils.ex | 13 +- lib/fledex/led_strip_driver/spi_driver.ex | 2 +- lib/fledex/leds.ex | 81 ++++--- lib/fledex/leds_driver.ex | 2 +- livebooks/2b_fledex_how_to_define_leds.livemd | 201 ++++++++++++++++++ mix.exs | 1 + test/animation/led_animation_manager_test.exs | 2 +- test/color/color_test.exs | 43 ++-- test/color/color_utils_test.exs | 14 +- test/color/kino_render_test.exs | 56 +++++ test/leds_test.exs | 56 ++++- 16 files changed, 487 insertions(+), 78 deletions(-) create mode 100644 lib/fledex/color/kino_render.ex create mode 100644 livebooks/2b_fledex_how_to_define_leds.livemd create mode 100644 test/color/kino_render_test.exs diff --git a/TODO.md b/TODO.md index 06dd71e..6ddf244 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,7 @@ as really done. Here the outstanding tasks that I can think of. # Tasks - [ ] Documentation - [ ] Finish the dsl livebook example (mostly done, but the send_config part is not done yet) (v0.3) - - [ ] Create another livebook (2b) that looks at the different aspects of the Leds module and how to work with it + - [x] Create another livebook (2b) that looks at the different aspects of the Leds module and how to work with it (v0.3) - [ ] Add proper API/module documentation (at least for the most important modules) (v0.3) - [ ] Fledex - [ ] Fledex.Leds @@ -15,7 +15,9 @@ as really done. Here the outstanding tasks that I can think of. - [ ] Fledex.Animation.LedAnimator - [ ] Fledex.LedsDriver - [ ] Add type specs (at least for the most important modules) (v0.3) - - [ ] Improve hexdocs (add livebooks, create cheatsheet) (v0.3) + - [ ] Improve hexdocs + - [x] add livebooks (v0.3) + - [ ] create cheatsheet (v0.3) - [ ] Add documentation on how to connect the LED strip to a RaspberryPi Zero (with and without level shifter).This could be part of the first example (v0.3 & v0.4) - [ ] Add installation instructions (v0.4) - [ ] Create a dsl (domain specific language) to (finally) easily program strips diff --git a/lib/fledex/color/kino_render.ex b/lib/fledex/color/kino_render.ex new file mode 100644 index 0000000..d8f63b9 --- /dev/null +++ b/lib/fledex/color/kino_render.ex @@ -0,0 +1,64 @@ +defmodule Fledex.Color.KinoRender do + import Fledex.Color.Names, only: [is_color_name: 1] + alias Fledex.Color.Types + alias Fledex.Color.Utils + alias Fledex.Leds + + defguard is_byte(sub_pixel) when is_integer(sub_pixel) and sub_pixel >= 0 and sub_pixel <= 255 + + defstruct [:colors] + @type t :: %__MODULE__{ + colors: [Types.color] + } + + @spec new(Types.color | [Types.color]) :: t + def new(colors) when is_list(colors) do + %__MODULE__{colors: colors} + end + def new(color) when is_integer(color) do + new([color]) + end + def new({r, g, b} = color) when is_byte(r) and is_byte(g) and is_byte(b) do + new([color]) + end + def new(color) when is_atom(color) and is_color_name(color) do + new([color]) + end + + @spec to_leds(t) :: Leds.t + def to_leds(%__MODULE__{colors: colors}) do + colors = Enum.map(colors, fn color -> + Utils.to_colorint(Utils.to_rgb(color)) + end) + Leds.leds( + length(colors), + Map.new( + Enum.reduce(colors, [], fn value, acc -> + index = length(acc) + 1 + [{index, value} | acc] + end) + ), + %{} + ) + end + + @spec to_markdown(KinoRender.t) :: binary + def to_markdown(%__MODULE__{} = colors) do + Leds.to_markdown(to_leds(colors)) + end + + defimpl Kino.Render do + alias Fledex.Color.KinoRender + + @impl true + def to_livebook(%KinoRender{} = colors) do + md_kino = Kino.Markdown.new(KinoRender.to_markdown(colors)) + i_kino = Kino.Inspect.new(colors) + kino = Kino.Layout.tabs( + Leds: md_kino, + Raw: i_kino + ) + Kino.Render.to_livebook(kino) + end + end +end diff --git a/lib/fledex/color/load_utils.ex b/lib/fledex/color/load_utils.ex index 820f695..4543a48 100644 --- a/lib/fledex/color/load_utils.ex +++ b/lib/fledex/color/load_utils.ex @@ -28,17 +28,14 @@ defmodule Fledex.Color.LoadUtils do defp convert_to_atom(name) do name - |> String.trim() |> String.normalize(:nfd) - |> String.replace(~r/[^a-zA-Z0-9_]/, "_") - |> String.replace(~r/__+/, "_") + |> String.replace(~r/[^a-zA-Z0-9]/, " ") + |> String.trim() + |> String.replace(~r/\s+/, "_") |> String.downcase(:ascii) - |> remove_trailing_underscore() |> String.to_atom() end - # defp remove_trailing_underscore(<>), do: name - defp remove_trailing_underscore(name), do: name defp clean_and_convert(hex_string) do hex_string = String.replace(hex_string, "#", "") {hex_int, _} = Integer.parse(hex_string, 16) diff --git a/lib/fledex/color/names.ex b/lib/fledex/color/names.ex index 209bebc..a77d65e 100644 --- a/lib/fledex/color/names.ex +++ b/lib/fledex/color/names.ex @@ -59,6 +59,12 @@ defmodule Fledex.Color.Names do @colors colors @color_names Enum.map(@colors, fn %{name: name} = _colorinfo -> name end) + quote do + @type t :: unquote_splicing(@color_names) + end + + defguard is_color_name(atom) when atom in @color_names + @doc """ Get all the data about the predefined colors """ diff --git a/lib/fledex/color/types.ex b/lib/fledex/color/types.ex index 28c0ac7..7737bc0 100644 --- a/lib/fledex/color/types.ex +++ b/lib/fledex/color/types.ex @@ -1,6 +1,7 @@ defmodule Fledex.Color.Types do - @type rgb :: {red :: 0..255, green :: 0..255, blue :: 0..255} - @type hsv :: {hue :: 0..255, saturation :: 0..255, value :: 0..255} - @type hsl :: {hue :: 0..255, saturation :: 0..255, light :: 0..255} - @type colorint :: 0..0xFFFFFF + @type rgb :: {red :: 0..255, green :: 0..255, blue :: 0..255} + @type hsv :: {hue :: 0..255, saturation :: 0..255, value :: 0..255} + @type hsl :: {hue :: 0..255, saturation :: 0..255, light :: 0..255} + @type colorint :: 0..0xFFFFFF + @type color :: rgb | colorint | atom | Fledex.Color.Names.t end diff --git a/lib/fledex/color/utils.ex b/lib/fledex/color/utils.ex index 2605e46..914d015 100644 --- a/lib/fledex/color/utils.ex +++ b/lib/fledex/color/utils.ex @@ -51,7 +51,7 @@ defmodule Fledex.Color.Utils do def nscale8(color, rgb, video) do split_into_subpixels(color) |> nscale8(rgb, video) - |> combine_subpixels() + |> to_colorint() end @doc """ @@ -109,19 +109,20 @@ defmodule Fledex.Color.Utils do @doc """ This function combines the subpixels to a single (color) integer value """ - @spec combine_subpixels(Types.rgb) :: Types.colorint - def combine_subpixels({r, g, b}) do + # TODO: make more generic to accept more color types + @spec to_colorint(Types.rgb) :: Types.colorint + def to_colorint({r, g, b}) do (r<<<16) + (g<<<8) + b end @doc """ This function splits a single (color) integer value into it's rgb components """ - @spec convert_to_subpixels((Types.colorint | atom | Types.rgb | %{rgb: Types.rgb} | %{rgb: Types.colorint})) :: Types.rgb - def convert_to_subpixels(rgb) do + @spec to_rgb((Types.colorint | atom | Types.rgb | %{rgb: Types.rgb} | %{rgb: Types.colorint})) :: Types.rgb + def to_rgb(rgb) do case rgb do %{rgb: {r, g, b}} -> {r, g, b} - %{rgb: x} when is_integer(x) -> convert_to_subpixels(x) + %{rgb: x} when is_integer(x) -> to_rgb(x) x when is_atom(x) -> apply(Names, x, [:rgb]) x when is_integer(x) -> split_into_subpixels(x) x -> x diff --git a/lib/fledex/led_strip_driver/spi_driver.ex b/lib/fledex/led_strip_driver/spi_driver.ex index 7b90c6f..295c9db 100644 --- a/lib/fledex/led_strip_driver/spi_driver.ex +++ b/lib/fledex/led_strip_driver/spi_driver.ex @@ -48,7 +48,7 @@ defmodule Fledex.LedStripDriver.SpiDriver do binary = leds |> Correction.apply_rgb_correction(config.color_correction) |> Enum.reduce(<<>>, fn led, acc -> - {r, g, b} = Utils.convert_to_subpixels(led) + {r, g, b} = Utils.to_rgb(led) acc <> <> end) response = Circuits.SPI.transfer(config.ref, binary) diff --git a/lib/fledex/leds.ex b/lib/fledex/leds.ex index 016a446..acd8cf8 100644 --- a/lib/fledex/leds.ex +++ b/lib/fledex/leds.ex @@ -11,7 +11,7 @@ defmodule Fledex.Leds do alias Fledex.LedsDriver @enforce_keys [:count, :leds, :opts] - defstruct count: 0, leds: %{}, opts: %{}, meta: %{index: 1} #, fill: :none + defstruct count: 0, leds: %{}, opts: %{}, meta: %{index: 1} @type t :: %__MODULE__{ count: integer, leds: map, @@ -19,31 +19,6 @@ defmodule Fledex.Leds do meta: map } - # @spec new() :: t - # def new do - # new(0) - # end - # @spec new(integer) :: t - # def new(count) do - # new(count, %{server_name: nil, namespace: nil}) - # end - # @spec new(integer, map) :: t - # def new(count, opts) do - # new(count, %{}, opts) - # end - # @spec new(integer, map, map) :: t - # def new(count, leds, opts) do - # new(count, leds, opts, %{index: 1}) - # end - # @spec new(integer, map, map, map) :: t - # def new(count, leds, opts, meta) do - # %__MODULE__{ - # count: count, - # leds: leds, - # opts: opts, - # meta: meta - # } - # end @spec leds() :: t def leds do leds(0) @@ -70,6 +45,10 @@ defmodule Fledex.Leds do } end + @spec size(t) :: pos_integer + def size(%Leds{count: count} = _leds) do + count + end @spec set_driver_info(t, namespace :: atom, server_name :: atom) :: t def set_driver_info(%{opts: opts} = leds, namespace, server_name \\ Fledex.LedsDriver) do opts = %{opts | server_name: server_name, namespace: namespace} @@ -101,8 +80,8 @@ defmodule Fledex.Leds do num_leds = opts[:num_leds] || leds.count offset = opts[:offset] || 0 - start_color = Utils.convert_to_subpixels(start_color) - end_color = Utils.convert_to_subpixels(end_color) + start_color = Utils.to_rgb(start_color) + end_color = Utils.to_rgb(end_color) led_values = Functions.create_gradient_rgb(num_leds, start_color, end_color) |> convert_to_leds_structure(offset) @@ -145,15 +124,21 @@ defmodule Fledex.Leds do raise ArgumentError, message: "the offset needs to be > 0 (found: #{offset})" end @spec light(t, (Types.colorint | t | atom), pos_integer, pos_integer) :: t - def light(leds, led, offset, repeat) do + def light(leds, led, offset, repeat) when offset > 0 and repeat > 1 do + # convert led to a LEDs struct led = case led do led when is_integer(led) -> __MODULE__.leds(1) |> __MODULE__.light(led) led when is_atom(led) -> __MODULE__.leds(1) |> __MODULE__.light(led) - led when is_struct(led) -> led + %Leds{} = led -> led end + # repeat the sequence led = led |> __MODULE__.repeat(repeat) + # merge in the sequence at the coorect offset __MODULE__.light(leds, led, offset) end + def light(_leds, _led, offset, repeat) do + raise ArgumentError, message: "the offset needs to be > 0 (found: #{offset}) and repeat > 1 (found: #{repeat})" + end @spec do_update(t, (Types.colorint | Types.rgb | atom)) :: t defp do_update(%__MODULE__{meta: meta} = leds, rgb) do @@ -186,6 +171,11 @@ defmodule Fledex.Leds do color_int = apply(Names, atom, [:hex]) do_update(leds, color_int, offset) end + @spec do_update(t, Types.rgb, pos_integer) :: t + defp do_update(leds, {_r, _g, _b} = rgb, offset) do + color_int = Utils.to_colorint(rgb) + do_update(leds, color_int, offset) + end defp do_update(leds, led, offset) do raise ArgumentError, message: "unknown data #{inspect leds}, #{inspect led}, #{inspect offset}" end @@ -211,6 +201,21 @@ defmodule Fledex.Leds do end) end + @base16 16 + @block <<"\u2588">> + @spec to_markdown(Fledex.Leds.t, map) :: String.t + def to_markdown(leds, _config \\ %{}) do + leds + |> Fledex.Leds.to_list() + # |> Correction.apply_rgb_correction(config.color_correction) + |> Enum.reduce(<<>>, fn value, acc -> + hex = value + |> Integer.to_string(@base16) + |> String.pad_leading(6, "0") + acc <> "" <> @block <> "" + end) + end + @spec send(t, map) :: :ok | {:error, String} def send(leds, opts \\ %{}) do offset = opts[:offset] || 0 @@ -252,4 +257,20 @@ defmodule Fledex.Leds do offset = if rotate_left, do: offset, else: count-offset Enum.slide(vals, 0..rem(offset-1 + count, count), count) end + + defimpl Kino.Render, for: Fledex.Leds do + alias Fledex.Leds + + @impl true + @spec to_livebook(Fledex.Leds.t) :: map() + def to_livebook(leds) do + md_kino = Kino.Markdown.new(Leds.to_markdown(leds)) + i_kino = Kino.Inspect.new(leds) + kino = Kino.Layout.tabs( + Leds: md_kino, + Raw: i_kino + ) + Kino.Render.to_livebook(kino) + end + end end diff --git a/lib/fledex/leds_driver.ex b/lib/fledex/leds_driver.ex index 45ecc4b..d50f290 100644 --- a/lib/fledex/leds_driver.ex +++ b/lib/fledex/leds_driver.ex @@ -343,7 +343,7 @@ defmodule Fledex.LedsDriver do elems |> Enum.map(fn elem -> Utils.split_into_subpixels(elem) end) |> apply_merge_strategy(merge_strategy) - |> Utils.combine_subpixels() + |> Utils.to_colorint() end @spec apply_merge_strategy(list(Types.colorint()), atom) :: diff --git a/livebooks/2b_fledex_how_to_define_leds.livemd b/livebooks/2b_fledex_how_to_define_leds.livemd new file mode 100644 index 0000000..5380aff --- /dev/null +++ b/livebooks/2b_fledex_how_to_define_leds.livemd @@ -0,0 +1,201 @@ +# 2b Fledex: How to define LEDs + +```elixir +Mix.install( + [ + {:fledex, path: "/home/maze/Documents/code/fledex"} + ], + # necessary to enable the nicer rendering + consolidate_protocols: false +) +``` + +## Intro + +The `Fledex.Leds` module allows you to easily define a sequence of LEDs. In this livebook we will look at the following aspects: + +* the different color concepts +* defining individual LEDs (in sequence and out of sequence) +* merging LED sequences +* repeating an LED sequence +* defining LEDs though a function (gradient & rainbow) +* sending leds to a `Fledex.LedsDriver` + +## Color concepts + +Fledex allows to define colors in many different ways. + +One very common one is to work with an integer (as defined in `Types.colorint`) with a representation of the color similar to the [html notation](https://www.w3schools.com/colors/default.asp) It's best to write the color as hexadecimal, since each color has a range from 0 to 255 (i.e gets 2 digits), i.e 0xrrggbb (rr=red, gg=green, bb=blue). + +**Example:** `0x2cafe4` where red has a value of `0x2c` (44), green of `0xaf` (175), and blue of `0xe4` (228) +.This is equivalent to the decimal `2 928 612`. + +```elixir +# we define our own color +my_color1 = 0x2CAFE4 +my_color2 = 2_928_612 + +Fledex.Color.KinoRender.new([my_color1, my_color2]) +``` + +Fledex does also allow to specify an `{r, g, b}` triple (as defined in `Types.rgb`). Each color is again in the 0 to 255 range. + +```elixir +my_color3 = {0x2C, 0xAF, 0xE4} +my_color4 = {44, 175, 228} + +Fledex.Color.KinoRender.new([my_color3, my_color4]) +``` + +The next most convenient way is to defined a color through its name (see `Fledex.Color.Names`). +We will look at this in more detail in a [later chapter](./3b_more_about_colors.livemd) + +```elixir +alias Fledex.Color.Names +# note, this is a slightly different color compared to the above one, since the exact color +# does not exist. "Cerulean (Crayola): {28, 170, 214} is roughly the same color +my_color5 = :cerulean_crayola +my_color6 = Names.info(my_color5) +my_color7 = Names.cerulean_crayola() + +Fledex.Color.KinoRender.new([my_color5, my_color6, my_color7]) +``` + +From the color names you can also get more information, but as mentioned above we'll look at this in a later chapter. + +In addition, you might encounter, in some cases, other color encodings like `Type.hsv` or `Typee.hsl`. But they are in general not so important. + +## Defining individual Led + +In Fledex you define a sequence of LEDs (and their colors) through the `Leds` module. +You first define the size of the sequence. Let us define a sequence of 30 LEDs (by calling `Leds.leds`): + +```elixir +alias Fledex.Leds + +leds = Leds.leds(30) +``` + +Each LED in that sequence does not have any color assigned to it and therefore is (by definition) switched off (black). + +It is now possble to define individual LEDs in that sequence by simply specifying the color (through `Leds.light()` functions and chaining them together: + +```elixir +leds = + leds + |> Leds.light(0xFF0000) + |> Leds.light(:green) + |> Leds.light(255) +``` + +As can be seen by calling the `light` function we move an `index` to know which one we defined as last and we can thereby simply define the light one after the next. It should be noted that the leds are one-indexed. + +It is possible to define specific LEDs by specifying an offset. Let's extend our leds: + +```elixir +alias Fledex.Color.Names + +leds = + leds + |> Leds.light(:red, 10) + |> Leds.light(Names.green()) + |> Leds.light({0, 0, 0xFF}) +``` + +As seen, if we specify the offset of `10` and we continue to define colors without offset the next LED in the sequence will be defined (i.e `11`) + +At the `Raw` result you can see how the deta is encoded. + + + +```elixir +%Fledex.Leds{ + ... + leds: %{1 => 16711680, 2 => 65280, 3 => 255, 10 => 16711680, 11 => 65280, 12 => 255}, + ... +} +``` + +We have a `map` (`%{}`) with the `offset` as key and the color as value. + +It is possble to define a color with an invalid offset. i.e. which is outside the allowed range. This is not a problem. The information will be carried around, but will be ignored (take a look at the `Raw` data) + +```elixir +leds = + leds + |> Leds.light(:green, 40) +``` + +## Merging LED sequences + +Quite commonly there comes up the need to define an LED sequence as a combination of serveral sequences. This can easily be done by passing `Leds` as if it were a single LED + +```elixir +leds_sub = + Leds.leds(2) + |> Leds.light(0xFF0000) + |> Leds.light(0x00FF00) + +leds = + Leds.leds(30) + |> Leds.light(leds_sub) + |> Leds.light(leds_sub, 5) +``` + +## Repeating an LED sequence + +It is possble to define a sequence of LEDs by repeating it. If you want to have 5 red LEDs you define a single LED and insturct it to be repeated 5 times. This would look like this (note the first 5 is the offset which needs to be specified and the second specifies the repetition factor): + +```elixir +leds = + Leds.leds(30) + |> Leds.light(:red, 5, 5) +``` + +You can also use this to repeat a sub-sequence. This will will repeat the whole subsequence as many times in a row as specified. + +```elixir +leds_sub = + Leds.leds(2) + |> Leds.light(0xFF0000) + |> Leds.light(0x00FF00) + +leds = + Leds.leds(30) + |> Leds.light(leds_sub, 5, 3) +``` + +## Definining LEDs through a function + +When you want to create a color sequence that is more complex, then this can also be achieved through some definition functions. + +If, for example, you want to create a rainbow pattern over 10 LEDs, you can do so by using the `rainbow/2` function. + +```elixir +leds = + Leds.leds(30) + |> Leds.rainbow() +``` + +Another function is the `gradient/3` function that allows a transition from one color to another. + +```elixir +leds = + Leds.leds(30) + |> Leds.gradient(:red, :blue) +``` + +`repeat/2` is another function that will take the already defined sequence (and `Leds` size `x`) and repeats it `y` times. Hence, the returned strip size will be: `size = x * y`. + +```elixir +Leds.leds(10) +|> Leds.gradient(:red, :blue) +|> Leds.repeat(3) +``` + +## Sending LEDs to a led-strip + +Once we have defined our Leds, we can send them to our `LedsDriver`. This is not difficult, but it's such a common task that the Leds module provides the convenience function `send/2`. and some supporting functions (`set_driver_info/3`, `rotate/2`, `to_list/1`). + +The send function has a couple of checks that the `LedsDriver` is set up correctly. +We won't discuss the send function in detail, since we'll work a bit more in other chapters. diff --git a/mix.exs b/mix.exs index 5d54e81..d5bb604 100644 --- a/mix.exs +++ b/mix.exs @@ -91,6 +91,7 @@ defmodule Fledex.MixProject do "livebooks/README.md", "livebooks/1_first_steps_with_an_led_strip.livemd", "livebooks/2_fledex_first_steps.livemd", + "livebooks/2b_fledex_how_to_define_leds.livemd", "livebooks/3_fledex_animations.livemd", "livebooks/3b_fledex_more_about_colors.livemd", "livebooks/4_fledex_clock_example.livemd", diff --git a/test/animation/led_animation_manager_test.exs b/test/animation/led_animation_manager_test.exs index 2720b4e..113cf97 100644 --- a/test/animation/led_animation_manager_test.exs +++ b/test/animation/led_animation_manager_test.exs @@ -1,5 +1,5 @@ defmodule Fledex.Animation.LedAnimationManagerTest do - use ExUnit.Case + use ExUnit.Case, async: false alias Fledex.Animation.LedAnimationManager diff --git a/test/color/color_test.exs b/test/color/color_test.exs index b5cc104..c883ff5 100644 --- a/test/color/color_test.exs +++ b/test/color/color_test.exs @@ -118,6 +118,11 @@ defmodule Fledex.Color.ColorTest do import Fledex.Color.Names assert 14_235_678 == vermilion2() end + test "color name guard" do + import Fledex.Color.Names, only: [is_color_name: 1] + assert is_color_name(:vermilion2) == true + assert is_color_name(:non_existing) == false + end end describe "test color corrections" do test "color" do @@ -181,25 +186,25 @@ defmodule Fledex.Color.ColorTest do # #19CB97 0.099 0.795 0.591 162.4° 163.4° 0.696 0.620 0.795 0.447 0.495 0.564 0.875 0.779 0.800 # #362698 0.211 0.149 0.597 248.3° 247.3° 0.448 0.420 0.597 0.373 0.319 0.219 0.750 0.601 0.533 # #7E7EB8 0.495 0.493 0.721 240.5° 240.4° 0.228 0.227 0.721 0.607 0.570 0.520 0.316 0.290 0.135 - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xffffff)) == {0, 0, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x808080)) == {0, 0, 181} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x000000)) == {0, 0, 0} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xff0000)) == {0, 255, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xbfbf00)) == {63, 255, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x008000)) == {96, 255, 181} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x80ffff)) == {195, 74, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x8080ff)) == {195, 74, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xbf40bf)) == {0, 127, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xa0a424)) == {71, 159, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x411bea)) == {182, 172, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x1eac41)) == {116, 168, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xf0c80e)) == {43, 196, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xb430e5)) == {248, 145, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xed7651)) == {32, 111, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0xfef888)) == {55, 69, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x19c897)) == {147, 175, 255} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x362698)) == {172, 157, 252} - assert Approximate.rgb2hsv(Utils.convert_to_subpixels(0x7e7eb8)) == {160, 76, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xffffff)) == {0, 0, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x808080)) == {0, 0, 181} + assert Approximate.rgb2hsv(Utils.to_rgb(0x000000)) == {0, 0, 0} + assert Approximate.rgb2hsv(Utils.to_rgb(0xff0000)) == {0, 255, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xbfbf00)) == {63, 255, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x008000)) == {96, 255, 181} + assert Approximate.rgb2hsv(Utils.to_rgb(0x80ffff)) == {195, 74, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x8080ff)) == {195, 74, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xbf40bf)) == {0, 127, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xa0a424)) == {71, 159, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x411bea)) == {182, 172, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x1eac41)) == {116, 168, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xf0c80e)) == {43, 196, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xb430e5)) == {248, 145, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xed7651)) == {32, 111, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0xfef888)) == {55, 69, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x19c897)) == {147, 175, 255} + assert Approximate.rgb2hsv(Utils.to_rgb(0x362698)) == {172, 157, 252} + assert Approximate.rgb2hsv(Utils.to_rgb(0x7e7eb8)) == {160, 76, 255} end end end diff --git a/test/color/color_utils_test.exs b/test/color/color_utils_test.exs index 7bd7b73..7a62469 100644 --- a/test/color/color_utils_test.exs +++ b/test/color/color_utils_test.exs @@ -15,7 +15,7 @@ defmodule Fledex.Color.UtilsTest do end test "combine" do led = {0x11, 0x64, 0xC8} - assert Utils.combine_subpixels(led) == 0x1164C8 + assert Utils.to_colorint(led) == 0x1164C8 end test "split into subpixels" do pixel = 0xFF7722 @@ -35,13 +35,13 @@ defmodule Fledex.Color.UtilsTest do assert Utils.nscale8({128, 128, 128}, Utils.frac8(32, 85)) == {49, 49, 49} assert Utils.nscale8({128, 128, 128}, Utils.frac8(32, 85), false) == {48, 48, 48} - assert Utils.nscale8(Utils.combine_subpixels({128, 128, 128}), Utils.frac8(32, 85)) == 3_223_857 + assert Utils.nscale8(Utils.to_colorint({128, 128, 128}), Utils.frac8(32, 85)) == 3_223_857 end test "convert to subpixels" do - assert Utils.convert_to_subpixels(%{rgb: 0x123456}) == {0x12, 0x34, 0x56} - assert Utils.convert_to_subpixels(%{rgb: {0x12, 0x34, 0x56}}) == {0x12, 0x34, 0x56} - assert Utils.convert_to_subpixels(:red) == {0xff, 0x00, 0x00} - assert Utils.convert_to_subpixels(0x123456) == {0x12, 0x34, 0x56} - assert Utils.convert_to_subpixels({0x12, 0x34, 0x56}) == {0x12, 0x34, 0x56} + assert Utils.to_rgb(%{rgb: 0x123456}) == {0x12, 0x34, 0x56} + assert Utils.to_rgb(%{rgb: {0x12, 0x34, 0x56}}) == {0x12, 0x34, 0x56} + assert Utils.to_rgb(:red) == {0xff, 0x00, 0x00} + assert Utils.to_rgb(0x123456) == {0x12, 0x34, 0x56} + assert Utils.to_rgb({0x12, 0x34, 0x56}) == {0x12, 0x34, 0x56} end end diff --git a/test/color/kino_render_test.exs b/test/color/kino_render_test.exs new file mode 100644 index 0000000..829f7c8 --- /dev/null +++ b/test/color/kino_render_test.exs @@ -0,0 +1,56 @@ +defmodule Fledex.Color.KinoRenderTest do + use Kino.LivebookCase, async: true + + alias Fledex.Color.KinoRender + alias Fledex.Leds + + describe "new" do + test "single color" do + assert %KinoRender{colors: [255]} = KinoRender.new(0x0000ff) + assert %KinoRender{colors: [{0, 0, 255}]} = KinoRender.new({0, 0, 255}) + assert %KinoRender{colors: [:red]} = KinoRender.new(:red) + end + test "color array" do + assert %KinoRender{colors: [255, {0, 0, 255}]} = KinoRender.new([0x0000ff, {0, 0, 255}]) + end + test "guard" do + import Fledex.Color.KinoRender, only: [is_byte: 1] + assert is_byte(0) == true + assert is_byte(128) == true + assert is_byte(255) == true + assert is_byte(-1) == false + assert is_byte(256) == false + end + end + describe "conversion functions" do + test "to_leds" do + kino = KinoRender.new([0x0000ff, {0, 0, 255}]) + assert %Leds{count: 2, leds: %{1 => 255, 2 => 255}} = KinoRender.to_leds(kino) + end + test "to_markdown" do + kino = KinoRender.new([0x0000ff, {0, 0, 255}]) + assert ~s() == KinoRender.to_markdown(kino) + end + end + describe "livebook" do + test "render" do + KinoRender.new([0xff0000, 0x00ff00, 0x0000ff]) + |> Kino.render() + + assert_output(%{ + labels: ["Leds", "Raw"], + outputs: [%{ + type: :markdown, + text: ~s(), + chunk: false + }, + %{ + type: :terminal_text, + text: ~s(%Fledex.Color.KinoRender{\e[34mcolors:\e[0m [\e[34m16711680\e[0m, \e[34m65280\e[0m, \e[34m255\e[0m]}), + chunk: false + }], + type: :tabs + }) + end + end +end diff --git a/test/leds_test.exs b/test/leds_test.exs index 208f777..1fa493a 100644 --- a/test/leds_test.exs +++ b/test/leds_test.exs @@ -60,6 +60,17 @@ defmodule Fledex.LedsTest do end test "setting leds in sequence" do + leds = Leds.leds(10) + |> Leds.light(:red) + |> Leds.light({0, 0xff, 0}) + |> Leds.light(0xff) + + assert Leds.get_light(leds, 1) == 0xff0000 + assert Leds.get_light(leds, 2) == 0x00ff00 + assert Leds.get_light(leds, 3) == 0x0000ff + + end + test "setting leds with a function" do leds = Leds.leds(10) |> Leds.rainbow() @@ -143,7 +154,7 @@ defmodule Fledex.LedsTest do |> Leds.light(:light_salmon) |> Leds.light(:red) |> Leds.light(:green) - |> Leds.light(:lime_web_x11_green_) + |> Leds.light(:lime_web_x11_green) |> Leds.light(:blue) assert Leds.get_light(leds, 1) == 0xFFA07A @@ -273,6 +284,19 @@ defmodule Fledex.LedsTest do assert Leds.get_light(leds, 9) == 0xff0000 assert leds.meta.index == 10 end + test "light with repeat" do + leds = Leds.leds(3) |> Leds.light(:red, 2, 3) + assert Leds.size(leds) == 3 + assert Leds.get_light(leds, 1) == 0x000000 + assert Leds.get_light(leds, 2) == 0xff0000 + assert Leds.get_light(leds, 3) == 0xff0000 + assert_raise ArgumentError, fn -> + Leds.leds(3) |> Leds.light(:red, 1, 1) + end + assert_raise ArgumentError, fn -> + Leds.leds(3) |> Leds.light(:red, -1, 2) + end + end test "repeat with different input types" do leds = Leds.leds(10, %{ 1 => 0xff0000, @@ -361,3 +385,33 @@ defmodule Fledex.LedsTestSync do end end end + +defmodule Fledex.LedsTestKino do + use Kino.LivebookCase, async: true + alias Fledex.Leds + + describe "render test" do + test "output" do + Leds.leds(3) + |> Leds.light(0xff0000) + |> Leds.light(0x00ff00) + |> Leds.light(0x0000ff) + |> Kino.render() + + assert_output(%{ + labels: ["Leds", "Raw"], + outputs: [%{ + type: :markdown, + text: ~s(), + chunk: false + }, + %{ + type: :terminal_text, + text: ~s(%Fledex.Leds{\n \e[34mcount:\e[0m \e[34m3\e[0m,\n \e[34mleds:\e[0m %{\e[34m1\e[0m => \e[34m16711680\e[0m, \e[34m2\e[0m => \e[34m65280\e[0m, \e[34m3\e[0m => \e[34m255\e[0m},\n \e[34mopts:\e[0m %{\e[34mnamespace:\e[0m \e[35mnil\e[0m, \e[34mserver_name:\e[0m \e[35mnil\e[0m},\n \e[34mmeta:\e[0m %{\e[34mindex:\e[0m \e[34m4\e[0m}\n}), + chunk: false + }], + type: :tabs + }) + end + end +end