commit 11eb2584efcdde7af913f2100bbe3799144c1800 Author: Bas Grolleman Date: Thu May 21 10:16:56 2026 +0200 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5feb519 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/DETAILS.md b/DETAILS.md new file mode 100644 index 0000000..b1a3dab --- /dev/null +++ b/DETAILS.md @@ -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_.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(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(leds_a, NUM_LEDS); + FastLED.addLeds(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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e3ff020 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7bd123e --- /dev/null +++ b/Makefile @@ -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) diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..c4eb089 --- /dev/null +++ b/PLAN.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c7e084e --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/arduino/cosplay_lights/button.cpp b/arduino/cosplay_lights/button.cpp new file mode 100644 index 0000000..8a1cf79 --- /dev/null +++ b/arduino/cosplay_lights/button.cpp @@ -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 + +// ---- 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; +} diff --git a/arduino/cosplay_lights/button.h b/arduino/cosplay_lights/button.h new file mode 100644 index 0000000..4ad46bf --- /dev/null +++ b/arduino/cosplay_lights/button.h @@ -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 + +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(); diff --git a/arduino/cosplay_lights/config.h b/arduino/cosplay_lights/config.h new file mode 100644 index 0000000..e36d619 --- /dev/null +++ b/arduino/cosplay_lights/config.h @@ -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 diff --git a/arduino/cosplay_lights/cosplay_lights.ino b/arduino/cosplay_lights/cosplay_lights.ino new file mode 100644 index 0000000..7d91dc4 --- /dev/null +++ b/arduino/cosplay_lights/cosplay_lights.ino @@ -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); +} diff --git a/arduino/cosplay_lights/led_controller.cpp b/arduino/cosplay_lights/led_controller.cpp new file mode 100644 index 0000000..67ddb91 --- /dev/null +++ b/arduino/cosplay_lights/led_controller.cpp @@ -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(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(); +} diff --git a/arduino/cosplay_lights/led_controller.h b/arduino/cosplay_lights/led_controller.h new file mode 100644 index 0000000..c126cd2 --- /dev/null +++ b/arduino/cosplay_lights/led_controller.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-2-Clause +#pragma once +#include +#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(); diff --git a/arduino/cosplay_lights/lightshow_format.h b/arduino/cosplay_lights/lightshow_format.h new file mode 100644 index 0000000..5685eda --- /dev/null +++ b/arduino/cosplay_lights/lightshow_format.h @@ -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 +#include + +// 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; +} diff --git a/arduino/cosplay_lights/show_blue_pulse.h b/arduino/cosplay_lights/show_blue_pulse.h new file mode 100644 index 0000000..aa26973 --- /dev/null +++ b/arduino/cosplay_lights/show_blue_pulse.h @@ -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; diff --git a/arduino/cosplay_lights/show_example_fade.h b/arduino/cosplay_lights/show_example_fade.h new file mode 100644 index 0000000..2b6c7ea --- /dev/null +++ b/arduino/cosplay_lights/show_example_fade.h @@ -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; diff --git a/arduino/cosplay_lights/show_example_party.h b/arduino/cosplay_lights/show_example_party.h new file mode 100644 index 0000000..2945e83 --- /dev/null +++ b/arduino/cosplay_lights/show_example_party.h @@ -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; diff --git a/arduino/cosplay_lights/show_example_pulse.h b/arduino/cosplay_lights/show_example_pulse.h new file mode 100644 index 0000000..f99a818 --- /dev/null +++ b/arduino/cosplay_lights/show_example_pulse.h @@ -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; diff --git a/arduino/cosplay_lights/shows.h b/arduino/cosplay_lights/shows.h new file mode 100644 index 0000000..753a489 --- /dev/null +++ b/arduino/cosplay_lights/shows.h @@ -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; diff --git a/converter/convert.py b/converter/convert.py new file mode 100644 index 0000000..673c54e --- /dev/null +++ b/converter/convert.py @@ -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_.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() diff --git a/converter/convert_all.py b/converter/convert_all.py new file mode 100644 index 0000000..05bc07a --- /dev/null +++ b/converter/convert_all.py @@ -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_.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() diff --git a/converter/shows/blue_pulse.txt b/converter/shows/blue_pulse.txt new file mode 100644 index 0000000..c709820 --- /dev/null +++ b/converter/shows/blue_pulse.txt @@ -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 diff --git a/converter/shows/example_fade.txt b/converter/shows/example_fade.txt new file mode 100644 index 0000000..4c29732 --- /dev/null +++ b/converter/shows/example_fade.txt @@ -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 diff --git a/converter/shows/example_party.txt b/converter/shows/example_party.txt new file mode 100644 index 0000000..b2bd283 --- /dev/null +++ b/converter/shows/example_party.txt @@ -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 diff --git a/converter/shows/example_pulse.txt b/converter/shows/example_pulse.txt new file mode 100644 index 0000000..1cb3597 --- /dev/null +++ b/converter/shows/example_pulse.txt @@ -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