Initial commit — Amirine Cosplay Lights

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>
This commit is contained in:
2026-05-21 10:16:56 +02:00
commit 11eb2584ef
24 changed files with 1320 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
# 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):
1. Check the button for events; switch show if needed (`load_show()`).
2. Read the current step from `SHOW[].steps` in PROGMEM.
3. Compute progress `t = elapsed / duration_ms`, clamped to 0.01.0.
4. Blend `s_from_color` toward the target color by `t`.
5. Push the blended color to the LEDs.
6. 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:
1. **Debounce** — the raw pin state must be stable for `DEBOUNCE_MS` (50ms) before the debounced state updates. This filters contact bounce.
2. **Hold detection** — if the debounced state stays pressed for `HOLD_MS` (800ms), `BTN_HOLD` fires immediately (doesn't wait for release). Any pending tap count is discarded.
3. **Tap counting** — each clean release increments a tap counter and records the release time.
4. **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:
1. Reads every `.txt` in `converter/shows/`.
2. Converts `blue_pulse.txt` first (always index 0), then all others alphabetically.
3. Writes one `show_<name>.h` per file into `arduino/cosplay_lights/`.
4. Regenerates `shows.h` with updated `#include` lines and `SHOWS[]` 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`:
```cpp
#define PATTERN_SOLID 0
#define PATTERN_CHASE 1
#define ACTIVE_PATTERN PATTERN_CHASE
```
**Step 2** — Add the pattern logic in `led_controller.cpp`:
```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`:
```cpp
// 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:
```cpp
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`:
```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.