VEX V5 Β· WS2812B Β· PROS 3.x / 4.x
LuxLib

Object-oriented addressable LED control for VEX V5 robots. 14 animations, frame-based playback, named zones β€” all non-blocking.

PROS 3.x / 4.x MIT License WS2812B 14 Animations Frame Playback Zone System ADI Expander

Overview

LuxLib wraps pros::ADILed with a clean API so you can do in one line what used to take fifty β€” without ever blocking your autonomous or opcontrol loops.

Every animation runs in its own background PROS task. Call strip.animate_rainbow() and walk away. The strip handles itself.

🎨
14 Procedural Animations
Rainbow, Fire, Meteor, Scanner, Breathe, Strobe, and more.
🎞️
Frame-Based Playback
Define pixel-perfect sequences as C arrays. Loop, cycle, or timed.
πŸ—ΊοΈ
Named Zones
Split one strip into independent sections: "Intake", "Drive", "Shooter".
⚑
Non-Blocking
Animations run in dedicated PROS tasks. Robot code never stalls.
🌈
HSV + RGB Math
Full color conversion, interpolation, scaling, and blending built-in.
πŸ”Œ
ADI Expander Support
First-class V5 expander support. Instantiate as many strips as you have ports.

Installation

Method 1 β€” PROS Template (Recommended)

Download luxlib@1.0.0.zip from the Releases page, then:

pros c fetch luxlib@1.0.0.zip
pros c apply luxlib

Done. luxlib/luxlib.hpp is now available in your project.

Method 2 β€” Manual

  1. Copy luxlib.hpp β†’ include/ and led_control.cpp β†’ src/
  2. Create include/luxlib/designs/ with an empty designs.h inside
  3. Add #include "luxlib/luxlib.hpp" where needed
πŸ’‘
Tip

Using the ADI Expander? Make sure main.h includes the expander headers before luxlib.hpp.

Quick Start

#include "luxlib/luxlib.hpp"
using namespace luxlib;

// Expander port 1, ADI port 'A', 60 LEDs
LedStrip drive(1, 'A', 60, "DriveLEDs");

void initialize() {
    drive.set_all(LED_BLUE);
}

void autonomous() {
    drive.animate_fire();          // Runs in background
}

void opcontrol() {
    drive.animate_rainbow();        // Loops forever in background
    while (true) {
        // Robot code here β€” LEDs don't block anything
        pros::delay(20);
    }
}
ℹ️
Direct Brain Wiring

No expander? Pass 0 as the expander port: LedStrip strip(0, 'A', 30);

The LedStrip Object

Everything in LuxLib centers on the LedStrip class. Each instance maps to one physical WS2812B strip on one ADI port. Multiple instances operate fully independently.

// LedStrip(expander_port, adi_port, num_leds, name = "LedStrip")
LedStrip intake(0, 'B', 20, "Intake");     // Direct brain, port B, 20 LEDs
LedStrip drive(1, 'A', 60, "Drive");       // Expander port 1, ADI A, 60 LEDs
LedStrip shooter(2, 'C', 12);              // Name defaults to "LedStrip"

Colors & Utilities

LuxLib ships 14 color constants as uint32_t in 0xRRGGBB format, plus global color utility functions β€” no object required.

LED_RED
LED_GREEN
LED_BLUE
LED_WHITE
LED_YELLOW
LED_ORANGE
LED_PURPLE
LED_CYAN
LED_PINK
LED_WARM
LED_TEAL
LED_LIME
LED_INDIGO
LED_OFF

You can also pass any hex literal or build your own:

// Pack / unpack
uint32_t rgb_to_hex(uint8_t r, uint8_t g, uint8_t b);
LuxRGB   hex_to_rgb(uint32_t color);

// Brightness scale (0.0 = off, 1.0 = full)
uint32_t scale_color(uint32_t color, float factor);

// HSV ↔ RGB  (LedHSV: h 0–360, s 0–1, v 0–1)
uint32_t hsv_to_rgb(LedHSV hsv);
LedHSV   rgb_to_hsv(uint32_t color);

// Interpolation β€” RGB (passes through grey) or HSV (vivid, shortest hue path)
uint32_t interpolate_rgb(uint32_t start, uint32_t end, float t);
uint32_t interpolate_rgb(uint32_t start, uint32_t end, int step, int width);
uint32_t interpolate_hsv(uint32_t start, uint32_t end, float t);
uint32_t interpolate_hsv(uint32_t start, uint32_t end, int step, int width);

Zone System

A zone is a named sub-range of a strip, letting you address physical sections independently. LuxLib rejects overlapping zones with a terminal error.

// add_zone(name, start, end) β€” inclusive on both ends
strip.add_zone("LeftDrive",  0,  19);
strip.add_zone("RightDrive", 20, 39);
strip.add_zone("Intake",     40, 59);

// Pass zone name to any animate_* or fill call. "" or omit = full strip.
strip.fill_zone("Intake", LED_GREEN);
strip.animate_breathe(LED_CYAN, 15, "LeftDrive");
strip.animate_rainbow(20, false, 1.0f, "RightDrive");

Brightness & Buffer

Each strip has a global brightness (0.0–1.0, default 1.0) applied at write-time. The internal buffer always stores full-color values. turn_off() does not stop an animation task β€” the task keeps running; turn_on() picks up where it left off.

// Brightness
strip.set_brightness(0.3f);   strip.get_brightness();
strip.turn_off();              strip.turn_on();
strip.set_enabled(false);     strip.set_enabled(true);

// Buffer writes
strip.set_pixel(5, LED_RED);
strip.set_all(LED_BLUE);
strip.fill_zone(10, 20, LED_GREEN);
strip.set_buffer(my_vector);
strip.clear();

// Snapshot / restore
strip.save_buffer();   strip.load_buffer();

// In-place manipulation
strip.color_shift(10, -20, 0);              // Shift RGB channels (clamped)
strip.rotate(3);  strip.rotate(-3, "LeftDrive");
strip.gradient(0, 59, LED_RED, LED_BLUE);    // RGB gradient
strip.gradient(0, 59, LED_RED, LED_BLUE, true); // HSV gradient (vivid)
strip.pulse(LED_WHITE, 15, 6);              // Soft pulse shape at pos 15
strip.blend_frames(frameA, frameB, 0.5f);   // Crossfade two frames

Procedural Animations

Calling any animate_* function immediately stops the current animation and starts the new one in a background task. Use timeout() or set_cycles() to auto-stop without blocking.

strip.stop();                   // Stop and clear
strip.animate_rainbow();
strip.timeout(5000);           // Auto-stop after 5s
strip.set_cycles(3);           // Auto-stop after 3 cycles
AnimationSignature (defaults)Description
animate_pulse(color, width=8, step_ms=30, zone="", reverse=false, bg=OFF)Band sweeps across, wrapping
animate_bounce(color, width=6, step_ms=20, zone="", bg=OFF)Band bounces end to end
animate_scanner(color, width=8, step_ms=20, zone="")KITT-style scan with fade tail
animate_breathe(color, step_ms=10, zone="")Quadratic fade in/out loop
animate_rainbow(step_ms=20, reverse=false, speed=1.0f, zone="")Full-spectrum scroll
animate_cycle(colors[], count, step_ms=30, reverse=false, zone="")Scroll a custom palette
animate_strobe(color, on_ms=50, off_ms=50, bg=OFF, zone="")Timed flash on/off
animate_theater_chase(color, bg=OFF, step_ms=50, spacing=3, reverse=false, zone="")Marquee spaced-pixel scroll
animate_meteor(color, size=6, tail_fade=0.7f, step_ms=20, reverse=false, zone="")Comet with fading tail
animate_wipe(color, step_ms=20, reverse=false, bg=OFF, zone="")Fill one LED at a time, then erase
animate_sparkle(color, density=3, step_ms=30, bg=OFF, zone="")Random flashing pixels
animate_confetti(colors[], count, density=3, step_ms=30, zone="")Sparkle from a custom palette
animate_fire(intensity=0.8f, step_ms=30, reverse=false, zone="")Cellular automaton fire sim

Frame-Based Animations

Define exact pixel layouts as C arrays and play them back at runtime β€” ideal for logo reveals, alliance intros, or match-start sequences.

Structs

struct LedFrame {
    const uint32_t* pixels;  // num_leds color values (0xRRGGBB)
    uint32_t delay_ms;
};
struct LedAnimation {
    const LedFrame* frames;
    int frame_count;
    bool loop;               // true = repeat forever
};

Playback

strip.show(frame);                 // Hold frame forever
strip.show(frame, 2000);           // Hold for 2s then clear
strip.play(anim, 5000);            // Play for 5 seconds
strip.play_cycles(anim, 3);        // Play 3 full loops
strip.play_timed(anim, 5000);     // Alias for play()

LED Animation Designer

LuxLib ships a browser-based pixel editor. Paint frames on a 60-LED model, build a timeline, and export ready-to-compile C code.

🎨
Workflow

Open led_designer.html β†’ paint LEDs β†’ Save Frame β†’ repeat β†’ Export Animation (.c) β†’ drop into project β†’ #include in designs.h.

/* Exported file looks like this */
#include "main.h"

static const LedFrame my_reveal_frames[] = {
    { { 0x000000, 0xFF0000, 0xFF0000 /* ... */ }, 300 },
    { { 0xFF0000, 0xFF0000, 0xFF0000 /* ... */ }, 500 },
};
const LedAnimation my_reveal = { my_reveal_frames, 2, true };
// designs.h
#include "my_reveal.c"

// usage
extern const LedAnimation my_reveal;
strip.play_cycles(my_reveal, 1);   // Play once at match start

API Quick Reference

Core Writes

MethodDescription
set_pixel(index, color)Single LED
set_all(color)Fill entire strip
fill_zone(start, end, color)Fill by index range
fill_zone(name, color)Fill by zone name
gradient(start, end, c1, c2, hsv=false)Paint gradient
clear()All off, buffer zeroed

Global Controls

MethodDescription
set_brightness(float)0.0–1.0
turn_off() / turn_on()Save/restore buffer + hardware toggle
set_enabled(bool)Combined toggle
save_buffer() / load_buffer()Snapshot / restore
stop()Kill task, clear strip
timeout(ms)Auto-stop after ms
set_cycles(n)Auto-stop after n cycles

Examples

Alliance Color + Zone State Feedback

strip.add_zone("Drive",   0,  39);
strip.add_zone("Intake",  40, 49);
strip.add_zone("Shooter", 50, 59);

void initialize() {
    strip.set_all(isRedAlliance ? LED_RED : LED_BLUE);
}
void opcontrol() {
    strip.animate_breathe(LED_BLUE, 15, "Drive");
    strip.fill_zone("Intake", LED_OFF);
}
void on_shooter_spinup() {
    strip.animate_strobe(LED_YELLOW, 80, 80, LED_OFF, "Shooter");
}
void on_shooter_ready() {
    strip.fill_zone("Shooter", LED_GREEN);
}

Multi-Strip Mirrored Drive

LedStrip left(1, 'A', 30, "Left");
LedStrip right(1, 'B', 30, "Right");
LedStrip intake(0, 'C', 15, "Intake");

void opcontrol() {
    left.animate_rainbow();
    right.animate_rainbow(20, true);  // Mirrored
    intake.animate_breathe(LED_GREEN);
    // All three run simultaneously, fully independent
}

FAQ

My LEDs flicker or show wrong colors.

WS2812B requires a 5V data line. If the V5 brain outputs 3.3V on ADI, you may need a level shifter. Also verify your strip's data direction β€” WS2812Bs are unidirectional.

Can I run two animations on two zones of the same strip simultaneously?

Not directly β€” each LedStrip object has one task slot. For true simultaneous zone animations, use two LedStrip objects, or write a custom PROS task that calls fill_zone for each zone in a loop.

What happens if I call animate_rainbow() while play_cycles() is running?

animate_rainbow() calls stop() internally before starting. Any running task is killed immediately and the new animation starts.

Does stop() block?

No. It calls m_task->remove() synchronously, clears the strip, and returns in microseconds.