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:
+19
@@ -0,0 +1,19 @@
|
||||
# Arduino IDE build output
|
||||
build/
|
||||
*.elf
|
||||
*.hex
|
||||
*.map
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
|
||||
# Generated file — recreate with convert.py
|
||||
# Commented out so the example is tracked in version control:
|
||||
# arduino/cosplay_lights/lightshow.h
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+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.
|
||||
@@ -0,0 +1,24 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2026, Amirine Cosplay Lights Contributors
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Amirine Cosplay Lights — Build & Upload
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Usage:
|
||||
# make shows — convert all .txt shows and regenerate shows.h
|
||||
# make build — compile the sketch
|
||||
# make upload — convert shows, compile, and upload (auto-detects port)
|
||||
# make upload PORT=/dev/ttyUSB1 — upload to a specific port
|
||||
# make port — list connected Arduino-compatible devices
|
||||
# make clean — remove build artifacts
|
||||
|
||||
SKETCH := arduino/cosplay_lights
|
||||
FQBN := arduino:avr:uno
|
||||
BUILD_DIR := build
|
||||
|
||||
# Auto-detect the first connected Arduino Uno port.
|
||||
# Override on the command line: make upload PORT=/dev/ttyUSB1
|
||||
PORT ?= $(shell arduino-cli board list 2>/dev/null | awk '/arduino:avr:uno/{print $$1}' | head -1)
|
||||
|
||||
# ---- Targets -----------------------------------------------------------
|
||||
|
||||
.PHONY: all shows build upload port clean
|
||||
|
||||
all: build
|
||||
|
||||
## Convert all .txt show files and regenerate shows.h.
|
||||
shows:
|
||||
python converter/convert_all.py
|
||||
|
||||
## Compile the sketch.
|
||||
build:
|
||||
arduino-cli compile --fqbn $(FQBN) --build-path $(BUILD_DIR) $(SKETCH)
|
||||
|
||||
## Convert shows, compile, and upload to the Arduino.
|
||||
upload: shows build
|
||||
@if [ -z "$(PORT)" ]; then \
|
||||
echo ""; \
|
||||
echo " No Arduino found. Plug in the USB cable and try again,"; \
|
||||
echo " or specify the port manually: make upload PORT=/dev/ttyUSB0"; \
|
||||
echo ""; \
|
||||
exit 1; \
|
||||
fi
|
||||
arduino-cli upload --fqbn $(FQBN) --port $(PORT) $(SKETCH)
|
||||
@echo ""
|
||||
@echo " Uploaded to $(PORT). The show starts immediately."
|
||||
|
||||
## List connected Arduino-compatible devices.
|
||||
port:
|
||||
@arduino-cli board list
|
||||
|
||||
## Remove build artifacts.
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
@@ -0,0 +1,65 @@
|
||||
# Project Plan — Amirine Cosplay Lights
|
||||
|
||||
This file records the original design brief and decisions so future contributors can understand why things are built the way they are.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Light show player** — load `.txt` files that define a sequence of colors with transition durations. Steps fade from the previous color to the next over the specified time. Setting duration to `0ms` gives an instant switch; high values (e.g. `5000ms`) give a slow fade.
|
||||
|
||||
2. **Multi-show navigation** — all shows are compiled into the firmware. A single button lets you cycle through them:
|
||||
- 1 tap — next show
|
||||
- 2 taps — previous show
|
||||
- Hold — reset to show 0 (slow blue pulse)
|
||||
|
||||
3. **Arduino + WS2812B** — runs on an Arduino Uno with a WS2812B (NeoPixel) LED strip.
|
||||
|
||||
4. **Open source** — BSD 2-Clause license. Free for any use, personal or commercial.
|
||||
|
||||
5. **Designed for expansion** — the LED controller layer (`led_controller.h/.cpp`) is structured so that new patterns (chase, sparkle, gradient) can be added later without touching show playback logic.
|
||||
|
||||
---
|
||||
|
||||
## Hardware confirmed
|
||||
|
||||
- **Board:** Arduino Uno
|
||||
- **LED strip:** WS2812B (NeoPixel), data pin 6, ~60 LEDs, GRB color order
|
||||
- **Button:** momentary push button on pin 2, wired to GND (INPUT_PULLUP)
|
||||
- **Color format:** hex codes (`#RRGGBB`)
|
||||
- **Show loading:** compiled into firmware via Arduino IDE / arduino-cli
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
Write/edit .txt files in converter/shows/
|
||||
↓
|
||||
make upload
|
||||
(runs make shows → make build → arduino-cli upload)
|
||||
↓
|
||||
Show starts immediately, button navigates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Small, clearly named functions** — each function does one thing.
|
||||
- **Comments explain the why** — not the what.
|
||||
- **PROGMEM for all show data** — Arduino Uno has only 2KB RAM; all `Step` arrays and the `SHOWS[]` index live in flash.
|
||||
- **Single config file** — all hardware values are in `config.h`; porting to a different board or strip type requires edits in one place.
|
||||
- **generate-don't-maintain** — `shows.h` and `show_*.h` are generated files; the source of truth is the `.txt` files in `converter/shows/`.
|
||||
|
||||
---
|
||||
|
||||
## Future ideas (not yet implemented)
|
||||
|
||||
- Per-LED patterns (chase, sparkle, gradient segments)
|
||||
- Multiple strip support
|
||||
- Battery power optimization (auto-dim, sleep)
|
||||
- SD card loading (no recompile needed for new shows)
|
||||
- WiFi loading via ESP32 (browser-based show upload)
|
||||
- Multiple buttons for direct show selection
|
||||
- Show name displayed on a small OLED
|
||||
@@ -0,0 +1,161 @@
|
||||
# Amirine Cosplay Lights
|
||||
|
||||
Arduino-powered LED strip controller for cosplay costumes.
|
||||
Write simple `.txt` files to define light shows — fades, instant switches, pulses — then upload them to the Arduino. A single button lets you cycle through all loaded shows.
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
---
|
||||
|
||||
## What you need
|
||||
|
||||
| Item | Notes |
|
||||
|------|-------|
|
||||
| Arduino Uno or Nano | Any clone works |
|
||||
| WS2812B LED strip | NeoPixel-compatible |
|
||||
| 5V power supply | See power warning below |
|
||||
| Micro USB cable | For uploading code |
|
||||
| Momentary push button | Any normally-open tactile button |
|
||||
| 470Ω resistor | On the LED data line — protects the first LED |
|
||||
| 1000µF capacitor | Across LED power lines — prevents voltage spikes |
|
||||
|
||||
---
|
||||
|
||||
## Wiring
|
||||
|
||||
### LED strip
|
||||
|
||||
```
|
||||
Arduino WS2812B strip
|
||||
─────── ─────────────
|
||||
GND ──────────────────── GND
|
||||
D6 ──── [470Ω] ──────── DIN (data in)
|
||||
```
|
||||
|
||||
Power the strip separately — see the power warning below.
|
||||
|
||||
### Button
|
||||
|
||||
```
|
||||
Arduino Button
|
||||
─────── ──────
|
||||
D2 ──────────────────── leg 1
|
||||
GND ──────────────────── leg 2
|
||||
```
|
||||
|
||||
Uses the internal pull-up resistor. No external resistor needed. Pin 2 can be changed in `config.h`.
|
||||
|
||||
### Button behaviour
|
||||
|
||||
| Press | Action |
|
||||
|-------|--------|
|
||||
| 1 tap | Next show |
|
||||
| 2 taps | Previous show |
|
||||
| Hold (~0.8s) | Reset to show 0 (slow blue pulse) |
|
||||
|
||||
### Power warning — read this
|
||||
|
||||
At full brightness, each WS2812B LED draws up to 60mA.
|
||||
A 60-LED strip at full white = **3.6A** — far more than the Arduino's 5V pin can supply.
|
||||
|
||||
**Power the strip directly from a 5V supply** (phone charger, power bank, or battery pack):
|
||||
- Connect the supply's **5V** and **GND** to the strip's **VCC** and **GND**.
|
||||
- Also connect the supply's **GND** to the Arduino's **GND** (shared ground).
|
||||
- Do **not** connect the supply's 5V to the Arduino's 5V pin.
|
||||
- Place a **1000µF capacitor** between VCC and GND near the strip connector.
|
||||
|
||||
`MAX_BRIGHTNESS` in `config.h` is set to 150 (out of 255) to limit current draw. Lower it further for battery builds.
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1. Install the FastLED library
|
||||
|
||||
Arduino IDE: **Sketch → Include Library → Manage Libraries** → search `FastLED` → Install.
|
||||
|
||||
### 2. Configure your hardware
|
||||
|
||||
Edit `arduino/cosplay_lights/config.h`:
|
||||
- `LED_DATA_PIN` — pin connected to the strip's DIN (default: 6)
|
||||
- `BUTTON_PIN` — pin the button is wired to (default: 2)
|
||||
- `NUM_LEDS` — total LED count on your strip (default: 60)
|
||||
- `MAX_BRIGHTNESS` — global brightness cap (default: 150)
|
||||
|
||||
### 3. Write or edit show files
|
||||
|
||||
Show files live in `converter/shows/`. Four are included:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `blue_pulse.txt` | Slow blue breathing — always show 0 (home/reset) |
|
||||
| `example_fade.txt` | Slow color cycle |
|
||||
| `example_party.txt` | Fast rainbow flashes |
|
||||
| `example_pulse.txt` | Red heartbeat |
|
||||
|
||||
**Show file format:**
|
||||
|
||||
```
|
||||
// Lines starting with // are comments.
|
||||
|
||||
#FF0000, 0 // snap to red instantly (0ms = no fade)
|
||||
#00FF00, 3000 // 3-second fade to green
|
||||
#0000FF, 0 // snap to blue
|
||||
```
|
||||
|
||||
Each line: a hex color and how long (in ms) to fade from the previous color.
|
||||
|
||||
**Tip — holding a color:** add a second step with the same color, duration = hold time:
|
||||
```
|
||||
#FF0000, 0 // snap to red
|
||||
#FF0000, 2000 // hold red for 2 seconds
|
||||
#0000FF, 1500 // fade to blue
|
||||
```
|
||||
|
||||
### 4. Build and upload
|
||||
|
||||
```bash
|
||||
make upload
|
||||
```
|
||||
|
||||
This converts all `.txt` shows, compiles the sketch, and uploads it. The show starts immediately and loops forever. Use the button to switch shows.
|
||||
|
||||
If you have multiple USB devices and the wrong port is detected:
|
||||
```bash
|
||||
make port # see what's connected
|
||||
make upload PORT=/dev/ttyUSB1 # target a specific port
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding your own shows
|
||||
|
||||
1. Create a `.txt` file in `converter/shows/` using the format above.
|
||||
2. Run `make upload` — your new show is automatically included.
|
||||
|
||||
The order in which shows appear when cycling through with the button is: `blue_pulse` first, then all others sorted alphabetically by filename.
|
||||
|
||||
---
|
||||
|
||||
## File overview
|
||||
|
||||
```
|
||||
Amirine_Cosplay_Lights/
|
||||
├── arduino/cosplay_lights/
|
||||
│ ├── cosplay_lights.ino — main sketch (upload this)
|
||||
│ ├── config.h — hardware settings
|
||||
│ ├── button.h / .cpp — button debounce and event detection
|
||||
│ ├── led_controller.h / .cpp — LED abstraction layer
|
||||
│ ├── lightshow_format.h — Step and ShowDef structs
|
||||
│ ├── shows.h — master show index (regenerated by make shows)
|
||||
│ └── show_*.h — individual show data (regenerated by make shows)
|
||||
├── converter/
|
||||
│ ├── convert_all.py — converts all shows and regenerates shows.h
|
||||
│ ├── convert.py — converts a single show file (for testing)
|
||||
│ └── shows/ — .txt show files (edit these)
|
||||
├── Makefile — build, upload, and show conversion targets
|
||||
├── README.md — this file
|
||||
├── DETAILS.md — architecture and modification guide
|
||||
├── PLAN.md — original project brief
|
||||
└── LICENSE — BSD 2-Clause
|
||||
```
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* button.cpp — Debounced button with single-tap, double-tap, and hold detection.
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "button.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
// ---- Timing constants --------------------------------------------------
|
||||
|
||||
static const uint16_t DEBOUNCE_MS = 50; // minimum stable time before accepting a state change
|
||||
static const uint16_t HOLD_MS = 800; // hold this long to emit BTN_HOLD
|
||||
static const uint16_t DOUBLE_TAP_MS = 400; // second tap must arrive within this window
|
||||
|
||||
// ---- State -------------------------------------------------------------
|
||||
|
||||
static uint8_t s_pin;
|
||||
|
||||
// Debounce state
|
||||
static bool s_raw_state; // last raw digitalRead result
|
||||
static bool s_debounced; // stable debounced state (true = pressed)
|
||||
static uint32_t s_debounce_time; // millis() when raw state last changed
|
||||
|
||||
// Tap counting
|
||||
static uint8_t s_tap_count; // taps accumulated in the current sequence
|
||||
static uint32_t s_last_release_ms; // millis() of the most recent release
|
||||
|
||||
// Hold detection
|
||||
static uint32_t s_press_start_ms; // millis() when current press began
|
||||
static bool s_hold_fired; // true after BTN_HOLD has been emitted for this press
|
||||
|
||||
// ---- Helpers -----------------------------------------------------------
|
||||
|
||||
// Returns true when the button is physically pressed.
|
||||
// INPUT_PULLUP means LOW = pressed.
|
||||
static inline bool pin_is_pressed() {
|
||||
return digitalRead(s_pin) == LOW;
|
||||
}
|
||||
|
||||
// ---- Public API --------------------------------------------------------
|
||||
|
||||
void button_begin(uint8_t pin) {
|
||||
s_pin = pin;
|
||||
s_raw_state = false;
|
||||
s_debounced = false;
|
||||
s_debounce_time = 0;
|
||||
s_tap_count = 0;
|
||||
s_last_release_ms = 0;
|
||||
s_press_start_ms = 0;
|
||||
s_hold_fired = false;
|
||||
pinMode(pin, INPUT_PULLUP);
|
||||
}
|
||||
|
||||
ButtonEvent button_update() {
|
||||
uint32_t now = millis();
|
||||
bool raw = pin_is_pressed();
|
||||
|
||||
// ---- Debounce ----
|
||||
// Reset the debounce timer whenever the raw reading changes.
|
||||
if (raw != s_raw_state) {
|
||||
s_raw_state = raw;
|
||||
s_debounce_time = now;
|
||||
}
|
||||
|
||||
// Only update the debounced state after the signal has been stable.
|
||||
bool prev = s_debounced;
|
||||
if ((now - s_debounce_time) >= DEBOUNCE_MS) {
|
||||
s_debounced = raw;
|
||||
}
|
||||
|
||||
bool just_pressed = ( s_debounced && !prev);
|
||||
bool just_released = (!s_debounced && prev);
|
||||
|
||||
// ---- Press start ----
|
||||
if (just_pressed) {
|
||||
s_press_start_ms = now;
|
||||
s_hold_fired = false;
|
||||
}
|
||||
|
||||
// ---- Hold detection (fires once while the button is held) ----
|
||||
if (s_debounced && !s_hold_fired && (now - s_press_start_ms) >= HOLD_MS) {
|
||||
s_hold_fired = true;
|
||||
s_tap_count = 0; // discard any pending taps so hold doesn't also trigger a tap
|
||||
return BTN_HOLD;
|
||||
}
|
||||
|
||||
// ---- Count taps on each release (only if hold didn't fire) ----
|
||||
if (just_released && !s_hold_fired) {
|
||||
s_tap_count++;
|
||||
s_last_release_ms = now;
|
||||
}
|
||||
|
||||
// ---- Resolve tap count after the double-tap window expires ----
|
||||
// Wait until the button is up AND the window has passed before deciding.
|
||||
if (s_tap_count > 0 && !s_debounced && (now - s_last_release_ms) >= DOUBLE_TAP_MS) {
|
||||
uint8_t taps = s_tap_count;
|
||||
s_tap_count = 0;
|
||||
if (taps == 1) return BTN_TAP;
|
||||
if (taps >= 2) return BTN_DOUBLE_TAP;
|
||||
}
|
||||
|
||||
return BTN_NONE;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* button.h — Debounced button with single-tap, double-tap, and hold detection.
|
||||
*
|
||||
* Wiring: connect one leg of the button to the configured pin, the other to GND.
|
||||
* Uses INPUT_PULLUP — no external resistor needed.
|
||||
*
|
||||
* Timing (adjust in button.cpp if needed):
|
||||
* DEBOUNCE_MS — ignores bounces shorter than this (default 50ms)
|
||||
* HOLD_MS — how long to hold before BTN_HOLD fires (default 800ms)
|
||||
* DOUBLE_TAP_MS — window to catch a second tap (default 400ms)
|
||||
*
|
||||
* Note: single taps are confirmed after DOUBLE_TAP_MS of silence so the
|
||||
* code can tell them apart from the start of a double-tap.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
enum ButtonEvent : uint8_t {
|
||||
BTN_NONE, // nothing happened
|
||||
BTN_TAP, // one short press, released, no second press within window
|
||||
BTN_DOUBLE_TAP, // two presses within DOUBLE_TAP_MS of each other
|
||||
BTN_HOLD, // button held longer than HOLD_MS
|
||||
};
|
||||
|
||||
// Initialize the button. Call once in setup().
|
||||
// pin: the Arduino pin the button is wired to (other end to GND).
|
||||
void button_begin(uint8_t pin);
|
||||
|
||||
// Poll for button events. Call once per loop().
|
||||
// Returns BTN_NONE most of the time; returns an event exactly once when detected.
|
||||
ButtonEvent button_update();
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// ============================================================
|
||||
// Hardware Configuration — Amirine Cosplay Lights
|
||||
// Adjust these values to match your physical setup.
|
||||
// ============================================================
|
||||
|
||||
// -- LED strip type ------------------------------------------
|
||||
// Uncomment the line that matches your strip.
|
||||
// Full list: https://github.com/FastLED/FastLED/wiki/Chipset-reference
|
||||
#define LED_TYPE WS2812B // NeoPixel — most common, single data wire
|
||||
// #define LED_TYPE APA102 // Dotstar — requires data + clock wire
|
||||
|
||||
// -- Color order ---------------------------------------------
|
||||
// WS2812B is almost always GRB. If your colors look wrong, try RGB.
|
||||
#define COLOR_ORDER GRB
|
||||
|
||||
// -- Wiring --------------------------------------------------
|
||||
// Pin on the Arduino connected to the strip's DIN (data-in).
|
||||
#define LED_DATA_PIN 6
|
||||
|
||||
// Clock pin — only needed for APA102/SK9822 strips.
|
||||
// #define LED_CLOCK_PIN 7
|
||||
|
||||
// -- Strip size ----------------------------------------------
|
||||
// Total number of LEDs in your strip.
|
||||
#define NUM_LEDS 60
|
||||
|
||||
// -- Brightness ----------------------------------------------
|
||||
// 0 (off) to 255 (full). Keep at or below 180 to limit heat and
|
||||
// current draw, especially when powered from USB.
|
||||
#define MAX_BRIGHTNESS 150
|
||||
|
||||
// -- Button --------------------------------------------------
|
||||
// Pin connected to one leg of the navigation button (other leg to GND).
|
||||
// Uses INPUT_PULLUP — no external resistor needed.
|
||||
// 1 tap → next show
|
||||
// 2 taps → previous show
|
||||
// hold → reset to show 0 (blue pulse)
|
||||
#define BUTTON_PIN 2
|
||||
|
||||
// -- Active pattern ------------------------------------------
|
||||
// Controls how the current color is mapped to the LEDs.
|
||||
// Only PATTERN_SOLID is included; see DETAILS.md for adding more.
|
||||
#define PATTERN_SOLID 0
|
||||
#define ACTIVE_PATTERN PATTERN_SOLID
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Amirine Cosplay Lights
|
||||
*
|
||||
* Plays lightshows defined in shows.h on a WS2812B LED strip.
|
||||
* A single button navigates between shows:
|
||||
* 1 tap — next show
|
||||
* 2 taps — previous show
|
||||
* hold — reset to show 0 (slow blue pulse)
|
||||
*
|
||||
* To add or update shows:
|
||||
* 1. Add or edit a .txt file in converter/shows/
|
||||
* 2. Run: make shows
|
||||
* 3. Run: make upload
|
||||
*
|
||||
* Required library: FastLED (Sketch > Include Library > Manage Libraries)
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "led_controller.h"
|
||||
#include "button.h"
|
||||
#include "shows.h"
|
||||
|
||||
// Milliseconds between LED updates. 16ms ≈ 60 refreshes/second.
|
||||
#define UPDATE_INTERVAL_MS 16
|
||||
|
||||
// ---- Color math --------------------------------------------------------
|
||||
|
||||
// Interpolate a single 8-bit channel from a to b by ratio t (0.0 – 1.0).
|
||||
static uint8_t lerp_channel(uint8_t a, uint8_t b, float t) {
|
||||
return (uint8_t)(a + (b - a) * t);
|
||||
}
|
||||
|
||||
// Blend two colors: t=0.0 returns 'from', t=1.0 returns 'to'.
|
||||
static CRGB blend_colors(CRGB from, CRGB to, float t) {
|
||||
return CRGB(
|
||||
lerp_channel(from.r, to.r, t),
|
||||
lerp_channel(from.g, to.g, t),
|
||||
lerp_channel(from.b, to.b, t)
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Playback state ----------------------------------------------------
|
||||
|
||||
static uint8_t s_show_index = 0; // which show is active (index into SHOWS[])
|
||||
static ShowDef s_show; // active show, read from PROGMEM once on load
|
||||
static uint16_t s_step_index = 0; // current step within the active show
|
||||
static uint32_t s_step_start = 0; // millis() when this step began
|
||||
static CRGB s_from_color = CRGB::Black; // color at the start of this transition
|
||||
|
||||
// Load show at 'index', resetting playback to the first step.
|
||||
static void load_show(uint8_t index) {
|
||||
s_show_index = index;
|
||||
s_show = read_show_def(&SHOWS[index]);
|
||||
s_step_index = 0;
|
||||
s_step_start = millis();
|
||||
s_from_color = CRGB::Black;
|
||||
}
|
||||
|
||||
// How far through the current step we are (0.0 – 1.0).
|
||||
// Returns 1.0 immediately for instant steps (duration_ms == 0).
|
||||
static float step_progress(const Step& step) {
|
||||
if (step.duration_ms == 0) return 1.0f;
|
||||
uint32_t elapsed = millis() - s_step_start;
|
||||
float t = (float)elapsed / step.duration_ms;
|
||||
return (t < 1.0f) ? t : 1.0f;
|
||||
}
|
||||
|
||||
// Complete the current step and advance to the next (loops at end of show).
|
||||
static void advance_step(CRGB reached_color) {
|
||||
s_from_color = reached_color;
|
||||
s_step_index = (s_step_index + 1) % s_show.length;
|
||||
s_step_start = millis();
|
||||
}
|
||||
|
||||
// ---- Arduino entry points ----------------------------------------------
|
||||
|
||||
void setup() {
|
||||
leds_begin();
|
||||
button_begin(BUTTON_PIN);
|
||||
load_show(0); // start on show 0 — slow blue pulse
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// ---- Button navigation ----
|
||||
ButtonEvent evt = button_update();
|
||||
if (evt == BTN_TAP) {
|
||||
// Next show, wrapping around to 0 after the last one.
|
||||
load_show((s_show_index + 1) % SHOW_COUNT);
|
||||
}
|
||||
if (evt == BTN_DOUBLE_TAP) {
|
||||
// Previous show, wrapping around to the last one from show 0.
|
||||
load_show((s_show_index + SHOW_COUNT - 1) % SHOW_COUNT);
|
||||
}
|
||||
if (evt == BTN_HOLD) {
|
||||
// Reset to show 0 (blue pulse) from any position.
|
||||
load_show(0);
|
||||
}
|
||||
|
||||
// ---- Playback ----
|
||||
Step step = read_step(&s_show.steps[s_step_index]);
|
||||
CRGB to = CRGB(step.r, step.g, step.b);
|
||||
float t = step_progress(step);
|
||||
|
||||
leds_apply_color(blend_colors(s_from_color, to, t));
|
||||
leds_show();
|
||||
|
||||
if (t >= 1.0f) {
|
||||
advance_step(to);
|
||||
}
|
||||
|
||||
delay(UPDATE_INTERVAL_MS);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
#include "led_controller.h"
|
||||
|
||||
// Internal LED buffer — FastLED writes to this array, then pushes it to the strip.
|
||||
static CRGB leds[NUM_LEDS];
|
||||
|
||||
void leds_begin() {
|
||||
FastLED.addLeds<LED_TYPE, LED_DATA_PIN, COLOR_ORDER>(leds, NUM_LEDS);
|
||||
FastLED.setBrightness(MAX_BRIGHTNESS);
|
||||
FastLED.clear(true); // clear + show: strip starts fully off
|
||||
}
|
||||
|
||||
void leds_apply_color(CRGB color) {
|
||||
#if ACTIVE_PATTERN == PATTERN_SOLID
|
||||
// All LEDs show the same color.
|
||||
fill_solid(leds, NUM_LEDS, color);
|
||||
|
||||
// To add a new pattern, add a new #define in config.h and a new branch here.
|
||||
// The pattern receives 'leds', 'NUM_LEDS', and the blended 'color' for this frame.
|
||||
#endif
|
||||
}
|
||||
|
||||
void leds_show() {
|
||||
FastLED.show();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
#pragma once
|
||||
#include <FastLED.h>
|
||||
#include "config.h"
|
||||
|
||||
// Initialize the LED strip. Call once in setup().
|
||||
void leds_begin();
|
||||
|
||||
// Apply 'color' to all LEDs using the pattern defined in config.h (ACTIVE_PATTERN).
|
||||
// Call leds_show() afterwards to push the update to the physical strip.
|
||||
void leds_apply_color(CRGB color);
|
||||
|
||||
// Flush the LED buffer to the physical strip. Call once per frame after leds_apply_color().
|
||||
void leds_show();
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* lightshow_format.h — Core data structures for show playback.
|
||||
*
|
||||
* All show data lives in PROGMEM (flash memory) to preserve the
|
||||
* Arduino Uno's 2KB of RAM. The read_* helpers copy values out of
|
||||
* PROGMEM so the rest of the code can work with them normally.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <avr/pgmspace.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// One step in a lightshow: a target color and the time to reach it.
|
||||
struct Step {
|
||||
uint8_t r, g, b; // Target RGB color (0–255 each)
|
||||
uint16_t duration_ms; // Milliseconds to fade from the previous color (0 = instant)
|
||||
};
|
||||
|
||||
// Descriptor for one complete show: a pointer to its PROGMEM Step array and its length.
|
||||
struct ShowDef {
|
||||
const Step* steps; // Pointer to a PROGMEM Step array
|
||||
uint16_t length; // Number of steps in the array
|
||||
};
|
||||
|
||||
// Read one Step from PROGMEM into a regular struct.
|
||||
// Direct pointer access does not work for PROGMEM on AVR — use this helper.
|
||||
inline Step read_step(const Step* ptr) {
|
||||
Step s;
|
||||
s.r = pgm_read_byte(&ptr->r);
|
||||
s.g = pgm_read_byte(&ptr->g);
|
||||
s.b = pgm_read_byte(&ptr->b);
|
||||
s.duration_ms = pgm_read_word(&ptr->duration_ms);
|
||||
return s;
|
||||
}
|
||||
|
||||
// Read one ShowDef from a PROGMEM ShowDef array.
|
||||
// On AVR, pointers are 2 bytes wide, so pgm_read_word works for both fields.
|
||||
inline ShowDef read_show_def(const ShowDef* ptr) {
|
||||
ShowDef sd;
|
||||
sd.steps = (const Step*)pgm_read_word(&ptr->steps);
|
||||
sd.length = pgm_read_word(&ptr->length);
|
||||
return sd;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Generated by convert_all.py from: blue_pulse.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_BLUE_PULSE[] PROGMEM = {
|
||||
{ 5, 0, 37, 0}, // #050025, 0ms
|
||||
{ 40, 40, 255, 3000}, // #2828FF, 3000ms
|
||||
{ 5, 0, 37, 3000}, // #050025, 3000ms
|
||||
};
|
||||
const uint16_t SHOW_BLUE_PULSE_LENGTH = 3;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Generated by convert_all.py from: example_fade.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_EXAMPLE_FADE[] PROGMEM = {
|
||||
{255, 0, 0, 0}, // #FF0000, 0ms
|
||||
{ 0, 255, 0, 3000}, // #00FF00, 3000ms
|
||||
{ 0, 0, 255, 0}, // #0000FF, 0ms
|
||||
{255, 0, 255, 2000}, // #FF00FF, 2000ms
|
||||
{255, 128, 0, 1000}, // #FF8000, 1000ms
|
||||
{ 0, 0, 0, 5000}, // #000000, 5000ms
|
||||
};
|
||||
const uint16_t SHOW_EXAMPLE_FADE_LENGTH = 6;
|
||||
@@ -0,0 +1,27 @@
|
||||
// Generated by convert_all.py from: example_party.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_EXAMPLE_PARTY[] PROGMEM = {
|
||||
{255, 0, 0, 0}, // #FF0000, 0ms
|
||||
{255, 0, 0, 150}, // #FF0000, 150ms
|
||||
{255, 136, 0, 0}, // #FF8800, 0ms
|
||||
{255, 136, 0, 150}, // #FF8800, 150ms
|
||||
{255, 255, 0, 0}, // #FFFF00, 0ms
|
||||
{255, 255, 0, 150}, // #FFFF00, 150ms
|
||||
{ 0, 255, 0, 0}, // #00FF00, 0ms
|
||||
{ 0, 255, 0, 150}, // #00FF00, 150ms
|
||||
{ 0, 255, 255, 0}, // #00FFFF, 0ms
|
||||
{ 0, 255, 255, 150}, // #00FFFF, 150ms
|
||||
{ 0, 0, 255, 0}, // #0000FF, 0ms
|
||||
{ 0, 0, 255, 150}, // #0000FF, 150ms
|
||||
{255, 0, 255, 0}, // #FF00FF, 0ms
|
||||
{255, 0, 255, 150}, // #FF00FF, 150ms
|
||||
{255, 255, 255, 0}, // #FFFFFF, 0ms
|
||||
{255, 255, 255, 150}, // #FFFFFF, 150ms
|
||||
{ 0, 0, 0, 500}, // #000000, 500ms
|
||||
};
|
||||
const uint16_t SHOW_EXAMPLE_PARTY_LENGTH = 17;
|
||||
@@ -0,0 +1,13 @@
|
||||
// Generated by convert_all.py from: example_pulse.txt
|
||||
// Do not edit manually — edit the .txt file and run: make shows
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
const Step SHOW_EXAMPLE_PULSE[] PROGMEM = {
|
||||
{ 32, 0, 0, 0}, // #200000, 0ms
|
||||
{255, 0, 0, 1200}, // #FF0000, 1200ms
|
||||
{ 32, 0, 0, 1200}, // #200000, 1200ms
|
||||
};
|
||||
const uint16_t SHOW_EXAMPLE_PULSE_LENGTH = 3;
|
||||
@@ -0,0 +1,29 @@
|
||||
// =====================================================================
|
||||
// shows.h — Master show index.
|
||||
// Generated by: make shows (converter/convert_all.py)
|
||||
// Do not edit manually — add .txt files to converter/shows/ instead.
|
||||
// =====================================================================
|
||||
//
|
||||
// Show 0 is always the home/reset show (slow blue pulse).
|
||||
// Holding the button resets back to show 0.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
// ---- Individual show data ----------------------------------------------
|
||||
#include "show_blue_pulse.h"
|
||||
#include "show_example_fade.h"
|
||||
#include "show_example_party.h"
|
||||
#include "show_example_pulse.h"
|
||||
|
||||
// ---- Show index (PROGMEM) ----------------------------------------------
|
||||
const ShowDef SHOWS[] PROGMEM = {
|
||||
{SHOW_BLUE_PULSE, SHOW_BLUE_PULSE_LENGTH}, // 0 — home show
|
||||
{SHOW_EXAMPLE_FADE, SHOW_EXAMPLE_FADE_LENGTH}, // 1
|
||||
{SHOW_EXAMPLE_PARTY, SHOW_EXAMPLE_PARTY_LENGTH}, // 2
|
||||
{SHOW_EXAMPLE_PULSE, SHOW_EXAMPLE_PULSE_LENGTH}, // 3
|
||||
};
|
||||
|
||||
const uint8_t SHOW_COUNT = 4;
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
convert.py — Convert a single .txt show file to an Arduino header.
|
||||
|
||||
Use this to preview or test a single show.
|
||||
For the full workflow (all shows + regenerate shows.h), use convert_all.py.
|
||||
|
||||
Usage:
|
||||
python converter/convert.py path/to/show.txt
|
||||
|
||||
Output:
|
||||
show_<name>.h written to the current directory.
|
||||
Move it to arduino/cosplay_lights/ then run: make upload
|
||||
|
||||
Show file format:
|
||||
Each step is one line: #RRGGBB, duration_ms
|
||||
Lines starting with // are comments and are ignored.
|
||||
Blank lines are ignored.
|
||||
|
||||
duration_ms controls how long the fade from the PREVIOUS color takes:
|
||||
0 = instant switch (no fade)
|
||||
500 = half-second fade
|
||||
5000 = five-second slow fade
|
||||
|
||||
To hold a color, add a second step with the same color and the hold time as duration:
|
||||
#FF0000, 0 // snap to red
|
||||
#FF0000, 2000 // hold red for 2 seconds
|
||||
#0000FF, 3000 // fade to blue over 3 seconds
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
STEP_PATTERN = re.compile(r'^\s*#([0-9A-Fa-f]{6})\s*,\s*(\d+)')
|
||||
|
||||
|
||||
def filename_to_symbol(stem: str) -> str:
|
||||
"""Convert a filename stem to an uppercase C array symbol."""
|
||||
clean = re.sub(r'[^a-zA-Z0-9]', '_', stem)
|
||||
return "SHOW_" + clean.upper()
|
||||
|
||||
|
||||
def hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
|
||||
return int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16)
|
||||
|
||||
|
||||
def parse_show(filepath: Path) -> list[tuple[int, int, int, int]]:
|
||||
"""
|
||||
Parse a .txt show file. Returns list of (r, g, b, duration_ms) tuples.
|
||||
Raises ValueError on malformed lines.
|
||||
"""
|
||||
steps = []
|
||||
with open(filepath) as f:
|
||||
for lineno, raw_line in enumerate(f, 1):
|
||||
line = raw_line.split("//")[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
match = STEP_PATTERN.match(line)
|
||||
if not match:
|
||||
raise ValueError(
|
||||
f"Line {lineno}: expected '#RRGGBB, duration_ms' "
|
||||
f"but got: {raw_line.rstrip()!r}"
|
||||
)
|
||||
r, g, b = hex_to_rgb(match.group(1))
|
||||
duration = int(match.group(2))
|
||||
steps.append((r, g, b, duration))
|
||||
if not steps:
|
||||
raise ValueError("Show file contains no steps.")
|
||||
return steps
|
||||
|
||||
|
||||
def render_header(steps: list, source_name: str, symbol: str) -> str:
|
||||
"""Render parsed steps as a C++ header file string."""
|
||||
lines = [
|
||||
f"// Generated by convert.py from: {source_name}",
|
||||
"// Do not edit manually — edit the .txt file instead.",
|
||||
"// SPDX-License-Identifier: BSD-2-Clause",
|
||||
"",
|
||||
"#pragma once",
|
||||
'#include "lightshow_format.h"',
|
||||
"",
|
||||
f"const Step {symbol}[] PROGMEM = {{",
|
||||
]
|
||||
for r, g, b, dur in steps:
|
||||
hex_color = f"#{r:02X}{g:02X}{b:02X}"
|
||||
lines.append(f" {{{r:3d}, {g:3d}, {b:3d}, {dur:5d}}}, // {hex_color}, {dur}ms")
|
||||
lines += [
|
||||
"};",
|
||||
f"const uint16_t {symbol}_LENGTH = {len(steps)};",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
source_path = Path(sys.argv[1])
|
||||
if not source_path.exists():
|
||||
print(f"Error: file not found: {source_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
steps = parse_show(source_path)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
symbol = filename_to_symbol(source_path.stem)
|
||||
header_text = render_header(steps, source_path.name, symbol)
|
||||
output_path = Path(f"show_{source_path.stem}.h")
|
||||
output_path.write_text(header_text, encoding="utf-8")
|
||||
|
||||
print(f"OK {len(steps)} steps → {output_path}")
|
||||
print(f" Move to 'arduino/cosplay_lights/' then run 'make upload'.")
|
||||
print(f" Or run 'make shows' to convert all shows at once.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
convert_all.py — Convert all .txt show files and regenerate shows.h.
|
||||
|
||||
Reads every .txt file in converter/shows/, converts each one to a
|
||||
show_<name>.h header, and regenerates arduino/cosplay_lights/shows.h
|
||||
with the updated SHOWS[] index.
|
||||
|
||||
Usage:
|
||||
python converter/convert_all.py
|
||||
(or via Makefile: make shows)
|
||||
|
||||
Show 0 is always 'blue_pulse' — the home/reset show.
|
||||
All other shows are sorted alphabetically and follow after it.
|
||||
|
||||
SPDX-License-Identifier: BSD-2-Clause
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Paths relative to this script's location
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
ROOT_DIR = SCRIPT_DIR.parent
|
||||
SHOWS_DIR = SCRIPT_DIR / "shows"
|
||||
SKETCH_DIR = ROOT_DIR / "arduino" / "cosplay_lights"
|
||||
|
||||
# The special first show — always placed at index 0.
|
||||
HOME_SHOW = "blue_pulse"
|
||||
|
||||
# Regex to match a valid step line: #RRGGBB, duration_ms
|
||||
STEP_PATTERN = re.compile(r'^\s*#([0-9A-Fa-f]{6})\s*,\s*(\d+)')
|
||||
|
||||
|
||||
# ---- Helpers -----------------------------------------------------------
|
||||
|
||||
def filename_to_symbol(stem: str) -> str:
|
||||
"""Convert a filename stem to an uppercase C array symbol. e.g. 'example_fade' → 'SHOW_EXAMPLE_FADE'"""
|
||||
clean = re.sub(r'[^a-zA-Z0-9]', '_', stem)
|
||||
return "SHOW_" + clean.upper()
|
||||
|
||||
|
||||
def hex_to_rgb(hex_str: str) -> tuple[int, int, int]:
|
||||
return int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16)
|
||||
|
||||
|
||||
def parse_show(filepath: Path) -> list[tuple[int, int, int, int]]:
|
||||
"""
|
||||
Parse a .txt show file. Returns list of (r, g, b, duration_ms) tuples.
|
||||
Raises ValueError on malformed lines.
|
||||
"""
|
||||
steps = []
|
||||
with open(filepath) as f:
|
||||
for lineno, raw_line in enumerate(f, 1):
|
||||
line = raw_line.split("//")[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
match = STEP_PATTERN.match(line)
|
||||
if not match:
|
||||
raise ValueError(
|
||||
f"{filepath.name} line {lineno}: "
|
||||
f"expected '#RRGGBB, duration_ms' but got: {raw_line.rstrip()!r}"
|
||||
)
|
||||
r, g, b = hex_to_rgb(match.group(1))
|
||||
duration = int(match.group(2))
|
||||
steps.append((r, g, b, duration))
|
||||
if not steps:
|
||||
raise ValueError(f"{filepath.name}: file contains no steps.")
|
||||
return steps
|
||||
|
||||
|
||||
def render_show_header(steps: list, source_name: str, symbol: str) -> str:
|
||||
"""Render a show as a C++ header file."""
|
||||
lines = [
|
||||
f"// Generated by convert_all.py from: {source_name}",
|
||||
"// Do not edit manually — edit the .txt file and run: make shows",
|
||||
"// SPDX-License-Identifier: BSD-2-Clause",
|
||||
"",
|
||||
"#pragma once",
|
||||
'#include "lightshow_format.h"',
|
||||
"",
|
||||
f"const Step {symbol}[] PROGMEM = {{",
|
||||
]
|
||||
for r, g, b, dur in steps:
|
||||
hex_color = f"#{r:02X}{g:02X}{b:02X}"
|
||||
lines.append(f" {{{r:3d}, {g:3d}, {b:3d}, {dur:5d}}}, // {hex_color}, {dur}ms")
|
||||
lines += [
|
||||
"};",
|
||||
f"const uint16_t {symbol}_LENGTH = {len(steps)};",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def render_shows_index(ordered_stems: list[str]) -> str:
|
||||
"""Render the master shows.h index file."""
|
||||
includes = "\n".join(f'#include "show_{stem}.h"' for stem in ordered_stems)
|
||||
entries = "\n".join(
|
||||
f" {{{filename_to_symbol(s)}, {filename_to_symbol(s)}_LENGTH}},"
|
||||
+ (" // 0 — home show" if i == 0 else f" // {i}")
|
||||
for i, s in enumerate(ordered_stems)
|
||||
)
|
||||
count = len(ordered_stems)
|
||||
return f"""\
|
||||
// =====================================================================
|
||||
// shows.h — Master show index.
|
||||
// Generated by: make shows (converter/convert_all.py)
|
||||
// Do not edit manually — add .txt files to converter/shows/ instead.
|
||||
// =====================================================================
|
||||
//
|
||||
// Show 0 is always the home/reset show (slow blue pulse).
|
||||
// Holding the button resets back to show 0.
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
#pragma once
|
||||
#include "lightshow_format.h"
|
||||
|
||||
// ---- Individual show data ----------------------------------------------
|
||||
{includes}
|
||||
|
||||
// ---- Show index (PROGMEM) ----------------------------------------------
|
||||
const ShowDef SHOWS[] PROGMEM = {{
|
||||
{entries}
|
||||
}};
|
||||
|
||||
const uint8_t SHOW_COUNT = {count};
|
||||
"""
|
||||
|
||||
|
||||
# ---- Main --------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
txt_files = sorted(SHOWS_DIR.glob("*.txt"))
|
||||
if not txt_files:
|
||||
print(f"No .txt files found in {SHOWS_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
# Separate the home show from the rest; sort the rest alphabetically.
|
||||
home_file = SHOWS_DIR / f"{HOME_SHOW}.txt"
|
||||
other_files = [f for f in txt_files if f.stem != HOME_SHOW]
|
||||
|
||||
if not home_file.exists():
|
||||
print(f"Warning: home show '{HOME_SHOW}.txt' not found — show 0 will be {txt_files[0].name}")
|
||||
ordered_files = txt_files
|
||||
else:
|
||||
ordered_files = [home_file] + other_files
|
||||
|
||||
SKETCH_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
converted = []
|
||||
errors = []
|
||||
|
||||
for txt_path in ordered_files:
|
||||
stem = txt_path.stem
|
||||
symbol = filename_to_symbol(stem)
|
||||
try:
|
||||
steps = parse_show(txt_path)
|
||||
header = render_show_header(steps, txt_path.name, symbol)
|
||||
out = SKETCH_DIR / f"show_{stem}.h"
|
||||
out.write_text(header, encoding="utf-8")
|
||||
print(f" OK {txt_path.name} → show_{stem}.h ({len(steps)} steps)")
|
||||
converted.append(stem)
|
||||
except ValueError as e:
|
||||
print(f" ERR {e}")
|
||||
errors.append(stem)
|
||||
|
||||
if errors:
|
||||
print(f"\n{len(errors)} file(s) had errors and were skipped.")
|
||||
|
||||
if not converted:
|
||||
print("No shows converted — shows.h not updated.")
|
||||
sys.exit(1)
|
||||
|
||||
# Regenerate shows.h
|
||||
index_path = SKETCH_DIR / "shows.h"
|
||||
index_path.write_text(render_shows_index(converted), encoding="utf-8")
|
||||
print(f"\n OK shows.h updated ({len(converted)} shows, show 0 = {converted[0]})")
|
||||
print( " Run 'make upload' to compile and send to the Arduino.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,9 @@
|
||||
// Blue pulse — home / reset show (always index 0)
|
||||
// A slow, gentle breathing effect in deep blue.
|
||||
// Loops continuously as the default show.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#050025, 0 // snap to dim blue — sets the starting point
|
||||
#2828FF, 3000 // slow breathe up to bright blue
|
||||
#050025, 3000 // slow breathe back down to dim
|
||||
@@ -0,0 +1,12 @@
|
||||
// Example: slow color cycle
|
||||
// Smooth fades between colors with different speeds.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
// duration_ms = time to fade FROM the previous color TO this one.
|
||||
|
||||
#FF0000, 0 // snap to red immediately
|
||||
#00FF00, 3000 // 3 second fade to green
|
||||
#0000FF, 0 // snap to blue
|
||||
#FF00FF, 2000 // 2 second fade to purple
|
||||
#FF8000, 1000 // 1 second fade to orange
|
||||
#000000, 5000 // 5 second slow fade to off
|
||||
@@ -0,0 +1,24 @@
|
||||
// Example: party mode — rapid color flashes
|
||||
//
|
||||
// Trick: to HOLD a color for N ms, add a second step with the same color
|
||||
// and set that step's duration to N. The "fade from red to red" is invisible.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#FF0000, 0 // snap to red
|
||||
#FF0000, 150 // hold red 150ms
|
||||
#FF8800, 0 // snap to orange
|
||||
#FF8800, 150 // hold orange 150ms
|
||||
#FFFF00, 0 // snap to yellow
|
||||
#FFFF00, 150 // hold yellow 150ms
|
||||
#00FF00, 0 // snap to green
|
||||
#00FF00, 150 // hold green 150ms
|
||||
#00FFFF, 0 // snap to cyan
|
||||
#00FFFF, 150 // hold cyan 150ms
|
||||
#0000FF, 0 // snap to blue
|
||||
#0000FF, 150 // hold blue 150ms
|
||||
#FF00FF, 0 // snap to magenta
|
||||
#FF00FF, 150 // hold magenta 150ms
|
||||
#FFFFFF, 0 // snap to white
|
||||
#FFFFFF, 150 // hold white 150ms
|
||||
#000000, 500 // fade to off — brief pause before looping
|
||||
@@ -0,0 +1,10 @@
|
||||
// Example: slow red pulse / heartbeat effect
|
||||
//
|
||||
// Fades from dim red to bright red and back, repeating.
|
||||
// Shows how to create an organic-feeling breathing effect.
|
||||
//
|
||||
// Format: #RRGGBB, duration_ms
|
||||
|
||||
#200000, 0 // start at dim red (no fade — sets the initial state)
|
||||
#FF0000, 1200 // fade up to bright red
|
||||
#200000, 1200 // fade back down to dim red
|
||||
Reference in New Issue
Block a user