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:
@@ -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;
|
||||
Reference in New Issue
Block a user