Arduino Uno + WS2812B LED strip controller with a text-based lightshow system. Shows are defined as .txt files (hex color + fade duration per step), converted to PROGMEM headers by convert_all.py, and navigated at runtime via a debounced button (tap/double-tap/hold). BSD 2-Clause license. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.4 KiB
Architecture Details — Amirine Cosplay Lights
For anyone who wants to understand or modify how the project works.
SPDX-License-Identifier: BSD-2-Clause
How show playback works
The Arduino sketch maintains a small amount of playback state:
| Variable | Type | What it stores |
|---|---|---|
s_show_index |
uint8_t |
Which show is active (index into SHOWS[]) |
s_show |
ShowDef |
Active show descriptor (read from PROGMEM once on load) |
s_step_index |
uint16_t |
Current step within the active show |
s_step_start |
uint32_t |
millis() when the current step began |
s_from_color |
CRGB |
Color the strip was showing when this step started |
On every loop() call (~60 times per second):
- Check the button for events; switch show if needed (
load_show()). - Read the current step from
SHOW[].stepsin PROGMEM. - Compute progress
t = elapsed / duration_ms, clamped to 0.0–1.0. - Blend
s_from_colortoward the target color byt. - Push the blended color to the LEDs.
- If
t >= 1.0, advance to the next step (loops at end of show).
To hold a color, add two steps with the same color — the second transition is from the color to itself, which is invisible.
Why PROGMEM?
The Arduino Uno has only 2KB of RAM. Storing all show data in normal arrays would exhaust it quickly. PROGMEM stores data in flash memory (32KB), which the Uno has much more of. Both the Step arrays and the ShowDef index (SHOWS[]) live in PROGMEM; the read_step() and read_show_def() helpers in lightshow_format.h extract values safely.
Each Step is 5 bytes (3 bytes RGB + 2 bytes duration). A ShowDef is 4 bytes (2-byte pointer + 2-byte length). The sketch currently uses 21% of flash — there is room for many more shows.
How the button works
button.h/.cpp implements a state machine with software debouncing:
-
Debounce — the raw pin state must be stable for
DEBOUNCE_MS(50ms) before the debounced state updates. This filters contact bounce. -
Hold detection — if the debounced state stays pressed for
HOLD_MS(800ms),BTN_HOLDfires immediately (doesn't wait for release). Any pending tap count is discarded. -
Tap counting — each clean release increments a tap counter and records the release time.
-
Tap resolution — after
DOUBLE_TAP_MS(400ms) of silence, the accumulated tap count is resolved: 1 tap →BTN_TAP, 2+ taps →BTN_DOUBLE_TAP. This introduces a 400ms delay on single taps (acceptable for show navigation).
Timing constants are at the top of button.cpp and can be adjusted.
File generation
shows.h and show_*.h are generated files — they are the output of converter/convert_all.py. The source of truth is the .txt files in converter/shows/.
The generation pipeline:
- Reads every
.txtinconverter/shows/. - Converts
blue_pulse.txtfirst (always index 0), then all others alphabetically. - Writes one
show_<name>.hper file intoarduino/cosplay_lights/. - Regenerates
shows.hwith updated#includelines andSHOWS[]table.
Running make shows triggers this. make upload runs make shows first automatically.
Adding a new LED pattern
The current ACTIVE_PATTERN is PATTERN_SOLID — all LEDs show the same blended color.
To add, for example, a chase pattern:
Step 1 — Add a #define in config.h:
#define PATTERN_SOLID 0
#define PATTERN_CHASE 1
#define ACTIVE_PATTERN PATTERN_CHASE
Step 2 — Add the pattern logic in led_controller.cpp:
#elif ACTIVE_PATTERN == PATTERN_CHASE
// One lit LED chases along the strip using the blended show color.
static uint16_t head = 0;
fill_solid(leds, NUM_LEDS, CRGB::Black);
leds[head] = color;
head = (head + 1) % NUM_LEDS;
The color parameter is always the show's current blended color, so patterns automatically follow lightshow transitions.
Changing the LED strip type
Edit config.h:
// WS2812B (default)
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
#define LED_DATA_PIN 6
// APA102 / Dotstar (two-wire)
// #define LED_TYPE APA102
// #define COLOR_ORDER BGR
// #define LED_DATA_PIN 6
// #define LED_CLOCK_PIN 7
For APA102, also change the FastLED.addLeds call in led_controller.cpp to the two-pin form:
FastLED.addLeds<LED_TYPE, LED_DATA_PIN, LED_CLOCK_PIN, COLOR_ORDER>(leds, NUM_LEDS);
Multiple strips
To drive two strips with the same show, add a second array and register it in led_controller.cpp:
static CRGB leds_a[NUM_LEDS];
static CRGB leds_b[NUM_LEDS];
void leds_begin() {
FastLED.addLeds<LED_TYPE, 6, COLOR_ORDER>(leds_a, NUM_LEDS);
FastLED.addLeds<LED_TYPE, 5, COLOR_ORDER>(leds_b, NUM_LEDS);
FastLED.setBrightness(MAX_BRIGHTNESS);
FastLED.clear(true);
}
void leds_apply_color(CRGB color) {
fill_solid(leds_a, NUM_LEDS, color);
fill_solid(leds_b, NUM_LEDS, color);
}
FastLED.show() pushes all registered strips at once — leds_show() needs no changes.
Power budget reference
| Scenario | Current draw (approx.) |
|---|---|
| 60 LEDs, full white, brightness 255 | ~3.6A |
| 60 LEDs, full white, brightness 150 | ~2.1A |
| 60 LEDs, single color, brightness 150 | ~0.7A |
| 60 LEDs, all off | ~20mA (Arduino only) |
Always use a 5V supply rated for at least 1A more than your calculated draw, and add a 1000µF capacitor on the supply lines near the strip connector.