Enigma LED outfit code
The code running all this isn’t terribly interesting.
I started with the Adafruit NeoPixel library but then modified it slightly. I didn’t really like the API of the original library and I eventually wanted to try swapping it out with a different implementation that didn’t take up so much CPU time. The most significant change was making some parts of the library compile time. For example, the pixel layout (RGB vs BGR vs with-white etc) was changed to a template parameter instead of a runtime variable.
Low Level Code
enum class NeoPixelColorLayout {
RGB, RBG, GRB, GBR, BRG, BGR,
WRGB, WRBG, WGRB, WGBR, WBRG, WBGR,
RWGB, RWBG, GWRB, GWBR, BWRG, BWGR,
RGWB, RBWG, GRWB, GBWR, BRWG, BGWR,
RGBW, RBGW, GRBW, GBRW, BRGW, BGRW,
};
template <NeoPixelColorLayout ColorLayout>
struct NeoPixelColorLayoutTraits
{
//static constexpr uint8_t red_offset;
//static constexpr uint8_t green_offset;
//static constexpr uint8_t blue_offset;
//static constexpr uint8_t white_offset;
//static constexpr bool has_white;
};
#define DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(color_layout, red_offset_, green_offset_, blue_offset_, white_offset_, has_white_) \
template <> \
struct NeoPixelColorLayoutTraits<NeoPixelColorLayout::color_layout> { \
static constexpr uint8_t red_offset = red_offset_;; \
static constexpr uint8_t green_offset = green_offset_; \
static constexpr uint8_t blue_offset = blue_offset_;; \
static constexpr uint8_t white_offset = white_offset_;; \
static constexpr bool has_white = has_white_; \
};
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RGB, 0, 1, 2, 0, false)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RBG, 0, 2, 1, 0, false)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GRB, 1, 0, 2, 0, false)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GBR, 2, 0, 1, 0, false)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BRG, 0, 2, 1, 0, false)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BGR, 2, 1, 0, 0, false)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(WRGB, 1, 2, 3, 0, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(WRBG, 1, 3, 2, 0, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(WGRB, 2, 1, 3, 0, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(WGBR, 3, 1, 2, 0, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(WBRG, 2, 3, 1, 0, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(WBGR, 3, 2, 1, 0, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RWGB, 0, 2, 3, 1, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RWBG, 0, 3, 2, 1, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GWRB, 2, 0, 3, 1, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GWBR, 3, 0, 2, 1, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BWRG, 2, 3, 0, 1, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BWGR, 3, 2, 0, 1, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RGWB, 0, 1, 3, 2, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RBWG, 0, 3, 1, 2, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GRWB, 1, 0, 3, 2, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GBWR, 3, 0, 1, 2, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BRWG, 1, 3, 0, 2, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BGWR, 3, 1, 0, 2, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RGBW, 0, 1, 2, 3, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(RBGW, 0, 2, 1, 3, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GRBW, 1, 0, 2, 3, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(GBRW, 2, 0, 1, 3, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BRGW, 1, 2, 0, 3, true)
DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT(BGRW, 2, 1, 0, 3, true)
#undef DEFINE_NEOPIXEL_COLOR_LAYOUT_TRAIT
This first block of code sets up “layout_traits”, so later we can use an enum to represent which pixel layout we want to use.
struct Color
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t w;
Color() : r(0), g(0), b(0), w(0) {};
Color(uint8_t r_, uint8_t g_, uint8_t b_) : r(r_), g(g_), b(b_), w(0) {};
Color(uint8_t r_, uint8_t g_, uint8_t b_, uint8_t w_) : r(r_), g(g_), b(b_), w(w_) {};
static Color HSV(uint16_t hue, uint8_t sat = 255, uint8_t val = 255)
{
// Remap 0-65535 to 0-1529. Pure red is CENTERED on the 64K rollover;
// 0 is not the start of pure red, but the midpoint...a few values above
// zero and a few below 65536 all yield pure red (similarly, 32768 is the
// midpoint, not start, of pure cyan). The 8-bit RGB hexcone (256 values
// each for red, green, blue) really only allows for 1530 distinct hues
// (not 1536, more on that below), but the full unsigned 16-bit type was
// chosen for hue so that one's code can easily handle a contiguous color
// wheel by allowing hue to roll over in either direction.
hue = (hue * 1530L + 32768) / 65536;
// Because red is centered on the rollover point (the +32768 above,
// essentially a fixed-point +0.5), the above actually yields 0 to 1530,
// where 0 and 1530 would yield the same thing. Rather than apply a
// costly modulo operator, 1530 is handled as a special case below.
// So you'd think that the color "hexcone" (the thing that ramps from
// pure red, to pure yellow, to pure green and so forth back to red,
// yielding six slices), and with each color component having 256
// possible values (0-255), might have 1536 possible items (6*256),
// but in reality there's 1530. This is because the last element in
// each 256-element slice is equal to the first element of the next
// slice, and keeping those in there this would create small
// discontinuities in the color wheel. So the last element of each
// slice is dropped...we regard only elements 0-254, with item 255
// being picked up as element 0 of the next slice. Like this:
// Red to not-quite-pure-yellow is: 255, 0, 0 to 255, 254, 0
// Pure yellow to not-quite-pure-green is: 255, 255, 0 to 1, 255, 0
// Pure green to not-quite-pure-cyan is: 0, 255, 0 to 0, 255, 254
// and so forth. Hence, 1530 distinct hues (0 to 1529), and hence why
// the constants below are not the multiples of 256 you might expect.
// Convert hue to R,G,B (nested ifs faster than divide+mod+switch):
uint8_t r;
uint8_t g;
uint8_t b;
if (hue < 510) { // Red to Green-1
b = 0;
if (hue < 255) { // Red to Yellow-1
r = 255;
g = hue; // g = 0 to 254
} else { // Yellow to Green-1
r = 510 - hue; // r = 255 to 1
g = 255;
}
}
else if (hue < 1020) { // Green to Blue-1
r = 0;
if (hue < 765) { // Green to Cyan-1
g = 255;
b = hue - 510; // b = 0 to 254
} else { // Cyan to Blue-1
g = 1020 - hue; // g = 255 to 1
b = 255;
}
}
else if(hue < 1530) { // Blue to Red-1
g = 0;
if (hue < 1275) { // Blue to Magenta-1
r = hue - 1020; // r = 0 to 254
b = 255;
} else { // Magenta to Red-1
r = 255;
b = 1530 - hue; // b = 255 to 1
}
} else { // Last 0.5 Red (quicker than % operator)
r = 255;
g = b = 0;
}
// Apply saturation and value to R,G,B, pack into 32-bit result:
uint32_t v1 = 1 + val; // 1 to 256; allows >>8 instead of /255
uint16_t s1 = 1 + sat; // 1 to 256; same reason
uint8_t s2 = 255 - sat; // 255 to 0
return Color{
((((r * s1) >> 8) + s2) * v1) >> 8,
((((g * s1) >> 8) + s2) * v1) >> 8,
((((b * s1) >> 8) + s2) * v1) >> 8
};
}
Color scale(uint8_t brightness) const
{
return {
uint8_t((this->r * brightness) >> 8),
uint8_t((this->g * brightness) >> 8),
uint8_t((this->b * brightness) >> 8),
uint8_t((this->w * brightness) >> 8),
};
}
};
The Color
class represents a color triplet (or quadruplet since there’s a white channel).
The HSV()
function generates a Color
object based on a Hue-Saturation-Value triplet (taken from Adafruit code).
The scale()
function scales a color by a scalar value (taken from the Adafruit code).
template <NeoPixelColorLayout neopixel_layout>
class NeoPixel final {
private:
using NeoPixelLayoutTrait = NeoPixelColorLayoutTraits<neopixel_layout>;
private:
uint16_t const num_leds;
uint8_t const pin_num;
uint8_t brightness;
std::vector<uint8_t> data;
uint32_t end_time; ///< Latch timing reference
public:
NeoPixel(uint8_t pin_num_, uint16_t num_leds_, uint8_t brightness_ = 0x20)
: num_leds(num_leds_)
, pin_num(pin_num_)
, brightness(brightness_)
, data(this->num_leds * (NeoPixelLayoutTrait::has_white ? 4 : 3), 0x00)
, end_time(micros())
{
pinMode(this->pin_num, OUTPUT);
digitalWrite(this->pin_num, LOW);
}
uint16_t getNumLeds() const
{
return this->num_leds;
}
void setRaw(uint8_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0)
{
size_t const base = n * (NeoPixelLayoutTrait::has_white ? 4 : 3);
this->data[base + NeoPixelLayoutTrait::red_offset ] = r;
this->data[base + NeoPixelLayoutTrait::green_offset] = g;
this->data[base + NeoPixelLayoutTrait::blue_offset ] = b;
this->data[base + NeoPixelLayoutTrait::white_offset] = w;
}
void set(uint8_t n, Color const c)
{
auto const sc = c.scale(this->brightness);
this->setRaw(n, sc.r, sc.g, sc.b, sc.w );
}
void setAll(Color const c)
{
for (auto n = 0 ; n < this->num_leds ; n++){
this->set(n, c);
}
}
bool canShow() const
{
return (micros() - this->end_time) >= 300L;
}
void show()
{
while(!this->canShow());
noInterrupts(); // Need 100% focus on instruction timing
uint8_t const portNum = g_APinDescription[this->pin_num].ulPort;
uint32_t const pinMask = 1ul << g_APinDescription[this->pin_num].ulPin;
uint8_t const * ptr = this->data.data();
uint8_t const * const end = ptr + this->data.size();
uint8_t p = *ptr++;
uint8_t bitMask = 0x80;
volatile uint32_t * set = &(PORT->Group[portNum].OUTSET.reg);
volatile uint32_t * clr = &(PORT->Group[portNum].OUTCLR.reg);
while (ptr < end){
*set = pinMask;
asm("nop; nop; nop; nop; nop; nop; nop; nop;");
if (p & bitMask) {
asm("nop; nop; nop; nop; nop; nop; nop; nop;nop; nop; nop; nop; nop; nop; nop; nop;nop; nop; nop; nop;");
*clr = pinMask;
} else {
*clr = pinMask;
asm("nop; nop; nop; nop; nop; nop; nop; nop;nop; nop; nop; nop; nop; nop; nop; nop;nop; nop; nop; nop;");
}
if (bitMask >>= 1) {
asm("nop; nop; nop; nop; nop; nop; nop; nop; nop;");
} else {
p = *ptr++;
bitMask = 0x80;
}
}
this->end_time = micros(); // Save EOD time for latch on next call
interrupts();
}
};
This is the meat of the NeoPixel class. Most of it (especially the show()
function) is based on the original Arduino code. The difference is the pixel layout from the traits class introduced before - this moves more of the “work” to compile time and lets the compiler more effectively optimize the get/set code (at least I think it does, I have not profiled).
At some point I should use a similar trait system to abstract the different ways to do the show()
action - it varies from chip to chip.
enum class TittyRing { Outer, Middle, Inner, Center };
static constexpr uint8_t NP_Outer = 24;
static constexpr uint8_t NP_Middle = 12;
static constexpr uint8_t NP_Inner = 6;
class NeoTitty final {
private:
NeoPixel<NeoPixelColorLayout::GRBW> inner;
NeoPixel<NeoPixelColorLayout::GRBW> outer;
uint8_t const inner_offset;
uint8_t const middle_offset;
uint8_t const outer_offset;
public:
NeoTitty(uint8_t inner_pin_num, uint8_t outer_pin_num, uint8_t inner_offset_ = 0, uint8_t middle_offset_ = 0, uint8_t outer_offset_ = 0)
: inner( inner_pin_num, NP_Middle + NP_Inner + 1 )
, outer( outer_pin_num, NP_Outer )
, inner_offset(inner_offset_)
, middle_offset(middle_offset_)
, outer_offset(outer_offset_)
{}
void set(TittyRing tr, uint8_t n, Color const c)
{
switch (tr){
case TittyRing::Outer:
n = (n + this->outer_offset) % NP_Outer;
this->outer.set(n, c);
return;
case TittyRing::Middle:
n = (n + this->middle_offset) % NP_Middle;
this->inner.set(n, c);
return;
case TittyRing::Inner:
n = (n + this->inner_offset) % NP_Inner;
this->inner.set(NP_Middle + 1 + n, c);
return;
case TittyRing::Center:
this->inner.set(NP_Middle + 0, c);
return;
}
}
void set(TittyRing tr, Color const c)
{
switch (tr){
case TittyRing::Outer: this->outer.setAll(c); return;
case TittyRing::Middle: for (auto i = 0 ; i < NP_Middle ; i++){ this->inner.set(i, c); }; return;
case TittyRing::Inner: for (auto i = 0 ; i < NP_Inner ; i++){ this->inner.set(NP_Middle + 1 + i, c); }; return;
case TittyRing::Center: this->inner.set(NP_Middle + 0, c); return;
}
}
void set(Color const c)
{
this->inner.setAll(c);
this->outer.setAll(c);
}
void show()
{
this->inner.show();
this->outer.show();
}
};
This is the class (named by Rosie) that encapsulates the specifics of the LED bra hardware: two rings plus a center board.
Instead of a simple string of 42 pixels, it’s an “outer ring” of 24, a “middle ring” of 12, an “inner ring” of 5 and finally a singular “center” pixel. This makes it easier to program for certain patterns without having to have “positioning” constants everywhere.
class NeoBra final {
private:
NeoTitty CR2;
NeoTitty right;
public:
NeoTitty(uint8_t left_inner_pin_num, uint8_t left_outer_pin_num,
uint8_t right_inner_pin_num, uint8_t right_outer_pin_num,
uint8_t left_inner_offset = 0, uint8_t left_middle_offset = 0, uint8_t left_outer_offset = 0,
uint8_t right_inner_offset = 0, uint8_t right_middle_offset = 0, uint8_t right_outer_offset = 0)
: left(left_inner_pin_num, left_outer_pin_num, left_inner_offset, left_middle_offset, left_outer_offset)
, right(right_inner_pin_num, right_outer_pin_num, right_inner_offset, right_middle_offset, right_outer_offset)
{}
void set(TittyRing tr, uint8_t n, Color const c)
{
this->left.set(tr, n, c);
this->right.set(tr, n, c);
}
void set(TittyRing tr, Color const c)
{
this->left.set(tr, c);
this->right.set(tr, c);
}
void set(Color const c)
{
this->left.set(c);
this->right.set(c);
}
void show()
{
this->left.show();
this->right.show();
}
};
This class simply wraps two NeoTitty
s: it has the same API but it forwards the calls to both instances.
At some point I may want to add more overloads that lets you select left vs right (instead of always doing both).
High Level Code
So now the top level can concern itself with lighting up the bra.
All of these classes follow this interface:
class Foo {
public:
Foo(NeoBra &nb, ...);
void step();
}
With the idea being that they can be constructed as global variables (or in the Arduino setup()
) and then their step()
function called inside the Arduino loop()
function.
Most of the classes end up busy-waiting in their step()
function to do timing, but it’s usually fast enough that it wouldn’t matter (if you’re doing other things in loop()
like switching between lighting modes).
The first class to look at is RainbowSpinner
. This class lights up the circles like a color wheel and then spins it over time.
The speed of rotation is configurable.
class RainbowSpinner
{
private:
NeoBra & nb;
uint32_t const stepsize;
uint16_t h;
public:
RainbowSpinner(NeoBra &nb_, uint32_t const stepsize_) : nb(nb_), stepsize(stepsize_), h(0) {};
RainbowSpinner(NeoBra &nb_) : RainbowSpinner(nb_, 20) {};
void step()
{
for (auto n = 0 ; n < NP_Inner ; n++){
nb.set(TittyRing::Inner, n, Color::HSV(this->h + (n * (65535 / NP_Inner)), 255, 150));
}
for (auto n = 0 ; n < NP_Middle ; n++){
nb.set(TittyRing::Middle, n, Color::HSV(this->h + (n * (65535 / NP_Middle)), 255, 200));
}
for (auto n = 0 ; n < NP_Outer ; n++){
nb.set(TittyRing::Outer, n, Color::HSV(this->h + (n * (65535 / NP_Outer))));
}
nb.set(TittyRing::Center, Color::HSV(this->h*2, 255, 100));
nb.show();
this->h += this->stepsize;
}
};
The next class, Quadrature
, sets up a 4-color pattern like the BMW logo and spins it over time.
The colors are configurable while the speed currently is not.
class Quadrature
{
private:
NeoBra & nb;
std::array<Color,4> colors;
uint8_t h;
public:
Quadrature(NeoBra &nb_, std::array<Color,4> colors_) : nb(nb_), colors(colors_), h(0) {};
void step()
{
for (auto n = 0 ; n < NP_Outer ; n++){
nb.set(TittyRing::Outer, n, this->colors[((n+h) / 6) % 4] );
}
for (auto n = 0 ; n < NP_Middle ; n++){
nb.set(TittyRing::Middle, n, this->colors[((n+(h/2)) / 3) % 4] );
}
for (auto n = 0 ; n < NP_Inner ; n++){
//nb.set(TittyRing::Inner, n, this->colors[((n+(h/4)) / 1) % 4] );
}
nb.show();
delay(80);
this->h += 1;
}
};
I have another class, Pulsar
, but it currently doesn’t work and I’ve forgotten what it’s supposed to do! 🤐
So there you have it, an LED cosplay bra from the ground up - and a framework to easily add more color patterns in the future.