defmodule PiGlow do
@leds PiGlow.LED.leds() |> Enum.sort_by(& &1.index)
@led_count 18 = Enum.count(@leds)
@enable_chunk_size 6
@enable_bytes 3 = div(@led_count, @enable_chunk_size)
@moduledoc """
A module for changing the power and brightness of the LEDs on a PiGlow device.
Each PiGlow device contains #{@led_count} LEDs. (See `PiGlow.LED` for a
list.) At any given time, each LED has two hardware properties:
* **`enable`:** Whether power is being supplied to the LED, expressed as `1`/`true` or `0`/`false`.
* **`power`:** The amount of power (via [PWM](https://en.wikipedia.org/wiki/Pulse-width_modulation)) being supplied to the LED, expressed as an integer between `0` (no power) and `255` (max power).
Note that **these properties are independent of each other.** An LED with
`enabled` set to `true` may still be turned off if `power` is set to `0`.
Similarly, an LED with `power` set to `255` will also still be turned off if
`enable` is set to `false`.
In general, the easiest way to use this library is to set all LEDs to
`enable` = `true` when your application starts, and then simply adjust the
`power` values (using `0` to turn them off). (This is how most other PiGlow
libraries work.) However, having access to both properties allows for useful
tricks, such as using `set_enable/1` to flash the lights on and off — without
changing their brightness, and while allowing some lights to remain off
(`power` = `0`).
To set these properties, three levels of API are available:
* **low:** Calling `set_*` with binary arguments. This will send raw bytes to the device.
* **medium:** Calling `set_*` with a list of values for each LED (in order). This will convert those values to the appropriate binaries, and send them using the "low" method above.
* **high:** Calling `map_*` with a function that maps each `PiGlow.LED` structure to a value. This will create a list of values, which will be sent via the "medium" method above.
Thus, you can choose whichever approach works best for your application — for
example, based on efficiency versus complexity.
## Asynchronous API
With the exception of `start_link/1`, `wait/1` and `stop/1`, all functions in
this module are asynchronous — they cast messages to the running `PiGlow`
instance without waiting for a reply, and they always return `:ok`.
Aside from generally improving performance, this also allows rapidly queuing
up multiple instructions, e.g. iterating through the full brigtness range
(from 0 to 255 and back) to "pulse" the LEDs, allowing the PiGlow instance to
run at full speed without waits.
In situations where you need to ensure that all your prior instructions have
completed execution, you can call `wait/1` to send a synchronous request to
the instance. This will block until the PiGlow instance has finished
processing its current message queue.
## Process lifecycle
You generally won't need to start a PiGlow instance manually, as starting
the application will (by default) automatically start a named instance,
unless you set `config :pi_glow, start: false` in your application config.
If you do choose to manually launch a PiGlow instance, all functions in
this module (besides `start_link`) accept an optional `pid` argument that can
be used to specify either the PID or registered name of your launched
instance.
Note that **launching or shutting down a PiGlow instance does not change the
state of the LEDs**. (This is in contrast with other PiGlow libraries, which
generally apply power to all LEDs on startup, and may also remove power on
exit.) Unless you know the LEDs are already enabled for some other reason,
you'll want to use one of the `*_enable` functions before any `*_power`
changes will be visible.
If you are using this library in a script (rather than a daemon), note also
that **when a script finishes and exits, all in-flight messages are
discarded**. If your script makes changes to the LEDs and then immediately
exits, there's a good chance that most or all of your changes will never get
processed. If you want to run LED events just before exiting — for example,
to turn them off — be sure to use `wait/1` or `stop/1` before exiting.
"""
use GenServer, restart: :transient
use PiGlow.AliasI2C
@default_name __MODULE__
@default_bus 1
@bus_addr 0x54
@cmd_enable_output 0x00
@cmd_set_pwm_values 0x01
@cmd_enable_leds 0x13
@cmd_update 0x16
@doc """
Starts a process that will update the PiGlow device LEDs when it receives messages.
## Options
* `:bus` - I2C bus device number (default: `#{@default_bus}`)
* `:name` - Registered process name to use (default: `#{inspect(@default_name)}`)
* Use value `nil` to prevent registration.
This function also accepts all the options accepted by `GenServer.start_link/3`.
## Return values
Same as `GenServer.start_link/3`.
"""
@spec start_link(GenServer.options()) :: GenServer.on_start()
def start_link(opts \\ []) do
{bus_id, opts} = Keyword.pop(opts, :bus, @default_bus)
opts = Keyword.put_new(opts, :name, @default_name)
GenServer.start_link(__MODULE__, bus_id, opts)
end
@async_return_value """
Always returns `:ok` immediately (even if there is no `PiGlow` process
running). Use `wait/1` if you need to ensure your changes have been sent to
the device.
"""
@doc """
Enables or disables power to each LED.
The `values` argument can be specified one of two ways:
* As a list of #{@led_count} booleans, with `true` or `false` indicating
whether each LED should be enabled or disabled.
* As a #{@enable_bytes}-byte binary, where each byte is a
#{@enable_chunk_size}-bit integer indicating which LEDs should be enabled.
Note that LEDs require both `enabled = true` **and** `power > 0` to light up.
Use `set_power/1` to adjust LED power, or `set_enabled_and_power/1` to set
both in a single operation.
#{@async_return_value}
## Examples
# Enable LEDs at indices 1, 2, 3, 5, 8, 13:
iex> 1..18 |> Enum.map(&(&1 in [1, 2, 3, 5, 8, 13])) |> PiGlow.set_enable()
:ok
# Equivalent to:
iex> PiGlow.set_enable(<<0b111010, 0b010000, 0b100000>>)
:ok
"""
@type enable :: [boolean] | binary
@spec set_enable(enable, pid) :: :ok
def set_enable(values, pid \\ @default_name) do
GenServer.cast(pid, {:set_enable, to_enable_binary(values)})
end
defp to_enable_binary(<<binary::binary-size(@enable_bytes)>>), do: binary
defp to_enable_binary(list) when is_list(list) do
list
|> Enum.chunk_every(@enable_chunk_size)
|> Enum.map(&bools_to_bits/1)
|> :erlang.list_to_binary()
end
@doc """
Sets the amount of power being delivered to each LED.
The `values` argument can be specified one of two ways:
* As a list of #{@led_count} integers, ranging from `0` (off) to `255` (full power).
* As an #{@led_count}-byte binary, where each byte is an integer, as above.
Note that LEDs require both `enabled = true` **and** `power > 0` to light up.
Use `set_enable/1` to turn LEDs on, or `set_enabled_and_power/1` to set both
in a single operation.
#{@async_return_value}
## Examples
# Set all LEDs to minimum brightness:
iex> 1..18 |> Enum.map(fn _ -> 1 end) |> PiGlow.set_power()
:ok
# Equivalent to:
iex> PiGlow.set_power(<<1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1>>)
:ok
"""
@type power :: [integer] | binary
@spec set_power(power, pid) :: :ok
def set_power(values, pid \\ @default_name) do
GenServer.cast(pid, {:set_power, to_power_binary(values)})
end
defp to_power_binary(<<binary::binary-size(@led_count)>>), do: binary
defp to_power_binary(list) when is_list(list), do: list |> :erlang.list_to_binary()
@doc """
Enables or disables power to all LEDs, and sets the amount of power, in a single operation.
The `values` argument can be specified one of two ways:
* As a list of #{@led_count} two-element tuples, each in the format `{enable, power}`, where enable is a boolean and power is an integer.
* As a two-element tuple, in the format `{enable_values, power_values}`.
* `enable_values` can be in either format (binary or list) accepted by `set_enable/1`.
* `power_values` can be in either format (binary or list) accepted by `set_power/1`.
When using this function, both instructions are sent to the I2C controller,
one immediately after the other, with no "update" operation sent inbetween.
This should avoid any unexpected LED flickering caused by setting each value
independently.
#{@async_return_value}
## Examples
# Set all LEDs to minimum brightness:
iex> 1..18 |> Enum.map(fn _ -> 1 end) |> PiGlow.set_power()
:ok
# Equivalent to:
iex> PiGlow.set_power(<<1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1>>)
:ok
"""
@spec set_enable_and_power([{boolean, integer}] | {enable, power}, pid) :: :ok
def set_enable_and_power(values, pid \\ @default_name)
def set_enable_and_power({enable, power}, pid) do
GenServer.cast(pid, {:set_enable_and_power, to_enable_binary(enable), to_power_binary(power)})
end
def set_enable_and_power([{_, _} | _] = list, pid) do
Enum.unzip(list)
|> set_enable_and_power(pid)
end
@doc """
Run a function to determine which LEDs should have power enabled or disabled.
The `fun` argument must be a function that takes one argument (a `PiGlow.LED`
structure) and returns a boolean. The resulting list of booleans will then
be sent to `set_enable/1`. See that function for more info.
Returns `:ok` immediately.
## Examples
# Turn on the green and blue LEDs, turn off the rest:
iex> PiGlow.map_enable(fn led -> led.colour in [:green, :blue] end)
:ok
# Turn on five LEDs at random:
iex> leds = PiGlow.LED.leds() |> Enum.take_random(5)
iex> PiGlow.map_enable(&(&1 in leds))
:ok
"""
@spec map_enable((PiGlow.LED.t() -> boolean), pid) :: :ok
def map_enable(fun, pid \\ @default_name) when is_function(fun) do
@leds
|> Enum.map(fun)
|> set_enable(pid)
end
@doc """
Run a function to determine how much power to send to each LED.
The `fun` argument must be a function that takes one argument (a `PiGlow.LED`
structure) and returns an integer in the range of `0..255`. The resulting
list of integers will then be sent to `set_power/1`. See that function for
more info.
Returns `:ok` immediately.
## Examples
# Set all LEDs to random brightness:
iex> PiGlow.map_power(fn _ -> Enum.random(1..255) end)
:ok
# Set the first arm to max brightness, the second to medium, the third to minimum.
iex> PiGlow.map_power(fn
...> %{arm: 1} -> 255
...> %{arm: 2} -> 125
...> %{arm: 3} -> 1
...> end)
:ok
"""
@spec map_power((PiGlow.LED.t() -> integer), pid) :: :ok
def map_power(fun, pid \\ @default_name) when is_function(fun) do
@leds
|> Enum.map(fun)
|> set_power(pid)
end
@doc """
Run a function to determine the enable status and power to send to each LED.
The `fun` argument must be a function that takes one argument (a `PiGlow.LED`
structure) and returns a 2-element tuple `{enable, power}`, where `enable` is
a boolean and `power` is an integer in the range of `0..255`.
The resulting list of boolean-integer pairs will be unzipped and sent to
`set_enable_and_power/1`. See that function for more info.
Returns `:ok` immediately.
## Examples
# Enable red LEDs at max brightness, amber at min brightness, disable the rest.
iex> PiGlow.map_enable_and_power(fn
...> %{colour: :red} -> {true, 255}
...> %{colour: :amber} -> {true, 1}
...> _ -> {false, 0}
...> end)
:ok
"""
@spec map_enable_and_power((PiGlow.LED.t() -> {boolean, integer}), pid) :: :ok
def map_enable_and_power(fun, pid \\ @default_name) when is_function(fun) do
@leds
|> Enum.map(fun)
|> set_enable_and_power(pid)
end
@doc """
Waits for all prior messages to be received and processed.
Returns `:ok` once all pending messages are processed. (Note that this does
not guarantee that the PiGlow is idle, only that messages sent prior to the
start of this `wait` call have been processed.)
If no message is received within `timeout` milliseconds (default: 60
seconds), the caller will exit, as per standard `GenServer.call/2` semantics.
## Examples
# Pulse all lights once:
iex> [0..255, 255..0] |>
...> Enum.flat_map(&Enum.to_list/1) |>
...> Enum.map(&PiGlow.LED.gamma_correct/1) |>
...> Enum.each(fn value ->
...> PiGlow.map_power(fn _ -> value end)
...> end)
:ok
# Wait for those 512 events to all be processed:
iex> PiGlow.wait()
:ok
"""
@spec wait(timeout, pid) :: :ok
def wait(timeout \\ 60_000, pid \\ @default_name) do
GenServer.call(pid, :wait, timeout)
end
@doc """
Stops a running instance and releases the I2C device.
Returns `:ok` once all pending messages are processed and the instance has
been cleanly stopped.
Note that `PiGlow` uses a default restart policy of `:transient`, meaning
that it will not be automatically restarted if stopped via this function.
If no message is received within `timeout` milliseconds (default: 60
seconds), the caller will exit.
## Examples
# Turn off all LEDs, then shut it down:
iex> PiGlow.map_enable_and_power(fn _ -> {false, 0} end)
:ok
iex> PiGlow.stop()
:ok
"""
@spec stop(timeout, pid) :: :ok
def stop(timeout \\ 60_000, pid \\ @default_name) do
GenServer.stop(pid, :normal, timeout)
end
# --- Internal functions ---
@impl true
def init(bus_id) when bus_id in 1..10 do
{:ok, bus} = I2C.open("i2c-#{bus_id}")
spawn_cleanup(bus)
:ok = I2C.write(bus, @bus_addr, <<@cmd_enable_output, 1>>)
{:ok, bus}
end
@impl true
def handle_cast({:set_enable, <<bytes::binary-size(@enable_bytes)>>}, bus) do
:ok = I2C.write(bus, @bus_addr, <<@cmd_enable_leds>> <> bytes)
:ok = update(bus)
{:noreply, bus}
end
@impl true
def handle_cast({:set_power, <<bytes::binary-size(@led_count)>>}, bus) do
:ok = I2C.write(bus, @bus_addr, <<@cmd_set_pwm_values>> <> bytes)
:ok = update(bus)
{:noreply, bus}
end
@impl true
def handle_cast(
{:set_enable_and_power, <<enable::binary-size(@enable_bytes)>>,
<<power::binary-size(@led_count)>>},
bus
) do
:ok = I2C.write(bus, @bus_addr, <<@cmd_enable_leds>> <> enable)
:ok = I2C.write(bus, @bus_addr, <<@cmd_set_pwm_values>> <> power)
:ok = update(bus)
{:noreply, bus}
end
@impl true
def handle_call(:wait, _from, bus) do
{:reply, :ok, bus}
end
defp spawn_cleanup(bus) do
me = self()
spawn(fn ->
ref = Process.monitor(me)
receive do
{:DOWN, ^ref, :process, ^me, _} -> I2C.close(bus)
end
end)
end
defp update(bus), do: I2C.write(bus, @bus_addr, <<@cmd_update, 0xFF>>)
defp bools_to_bits(list, acc \\ 0)
defp bools_to_bits([], acc), do: acc
defp bools_to_bits([true | rest], acc), do: bools_to_bits(rest, 2 * acc + 1)
defp bools_to_bits([false | rest], acc), do: bools_to_bits(rest, 2 * acc)
end