The four NeoPixels on the add-on board.
The kernel drives them, so they are files rather than a protocol: each pixel is
a multicolour LED class device with multi_intensity for the three channels
and brightness as a master scale over them. Brightness has to be non-zero for
anything to light, whatever the intensities say.
Because colour and brightness are separate, a pulse can be driven by writing brightness alone and leaving the colour where it is.
indicator-0 is the rightmost pixel as the robot is mounted, so they run right
to left. That only matters if they are ever used as a bar rather than all
showing the same thing, but it is the sort of thing nobody wants to work out
twice.
Brightness is on a hot path and colour isn't
A breath rewrites brightness on every pixel twenty times a second, and on this
board a File.write/2 to sysfs costs 5.86 ms — so doing that the obvious way
spent about half a core on making some LEDs fade, permanently, whether or not
the robot was doing anything. It was worse than it looks: the write happened
through a helper that globbed the LED directory each time to find the pixels.
So open/1 resolves a pixel once — its path, its channel order, and a
descriptor held open on brightness — and set_brightness/2 is one
:file.pwrite/3 against that descriptor, skipped entirely when the value
hasn't changed. Colour goes through File.write/2 still, because it changes
only when the safety state does.
off/0 deliberately stays path-based and stateless. It runs from terminate,
when whatever held those descriptors may already be gone.
Summary
Functions
The order this device wants its channels in, as read from multi_index.
Every pixel, in the order the kernel names them.
The largest value brightness accepts, or nil if the device isn't there.
Turn every pixel off, and stop any pulse.
Resolve a pixel and hold its brightness open, ready to be written to often.
Every pixel, opened and ready. Empty when the hardware isn't there.
Take a pixel off whatever kernel trigger it was on, so that brightness is ours to set.
Set every pixel to the same colour.
Set a pixel's brightness, leaving its colour alone.
Set every pixel's brightness.
Set one pixel's colour, leaving its brightness alone.
Types
@type colour() :: {0..255, 0..255, 0..255}
Red, green and blue, each 0..255.
@type t() :: %BB.NSK.Leds{ brightness: non_neg_integer() | nil, file: File.io_device(), order: [:red | :green | :blue], path: Path.t() }
Functions
@spec channel_order(Path.t()) :: [:red | :green | :blue]
The order this device wants its channels in, as read from multi_index.
Almost always [:red, :green, :blue] — the driver deals with the wire order
these things actually use — but it is written down, so read it rather than
assume it.
@spec devices() :: [Path.t()]
Every pixel, in the order the kernel names them.
@spec max_brightness(Path.t()) :: pos_integer() | nil
The largest value brightness accepts, or nil if the device isn't there.
@spec off() :: :ok
Turn every pixel off, and stop any pulse.
Resolve a pixel and hold its brightness open, ready to be written to often.
@spec open_all() :: [t()]
Every pixel, opened and ready. Empty when the hardware isn't there.
Take a pixel off whatever kernel trigger it was on, so that brightness is ours to set.
Set every pixel to the same colour.
@spec set_brightness(t(), non_neg_integer()) :: t()
Set a pixel's brightness, leaving its colour alone.
Returns the pixel, which remembers what it wrote so that an unchanged value costs nothing.
@spec set_brightness_all([t()], non_neg_integer()) :: [t()]
Set every pixel's brightness.
Set one pixel's colour, leaving its brightness alone.
Separate from brightness so that a colour can change without interrupting a pulse, which drives brightness underneath it.