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:
+154
@@ -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.0–1.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.
|
||||
Reference in New Issue
Block a user