BB.NSK.Leds (bb_nsk v0.1.2)

Copy Markdown View Source

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

Types

Red, green and blue, each 0..255.

t()

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

colour()

@type colour() :: {0..255, 0..255, 0..255}

Red, green and blue, each 0..255.

t()

@type t() :: %BB.NSK.Leds{
  brightness: non_neg_integer() | nil,
  file: File.io_device(),
  order: [:red | :green | :blue],
  path: Path.t()
}

Functions

channel_order(device)

@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.

devices()

@spec devices() :: [Path.t()]

Every pixel, in the order the kernel names them.

max_brightness(device)

@spec max_brightness(Path.t()) :: pos_integer() | nil

The largest value brightness accepts, or nil if the device isn't there.

off()

@spec off() :: :ok

Turn every pixel off, and stop any pulse.

open(device)

@spec open(Path.t()) :: {:ok, t()} | {:error, term()}

Resolve a pixel and hold its brightness open, ready to be written to often.

open_all()

@spec open_all() :: [t()]

Every pixel, opened and ready. Empty when the hardware isn't there.

release(device)

@spec release(Path.t()) :: :ok | {:error, term()}

Take a pixel off whatever kernel trigger it was on, so that brightness is ours to set.

set_all(pixels, colour)

@spec set_all([t()], colour()) :: :ok

Set every pixel to the same colour.

set_brightness(pixel, brightness)

@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.

set_brightness_all(pixels, brightness)

@spec set_brightness_all([t()], non_neg_integer()) :: [t()]

Set every pixel's brightness.

set_colour(pixel, colour)

@spec set_colour(t() | Path.t(), colour()) :: :ok | {:error, term()}

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.