diff --git a/DETAILS.md b/DETAILS.md index b1a3dab..ff107fe 100644 --- a/DETAILS.md +++ b/DETAILS.md @@ -22,10 +22,10 @@ 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`. +3. Compute progress `t` (0–255) as `elapsed * 255 / duration_ms`, clamped. +4. Blend `s_from_color` toward the target color using FastLED's `blend()`. 5. Push the blended color to the LEDs. -6. If `t >= 1.0`, advance to the next step (loops at end of show). +6. If `t == 255`, advance to the next step. For `SHOW_LOOP` shows this wraps; for `SHOW_SINGLE` shows this loads the next show. To **hold** a color, add two steps with the same color — the second transition is from the color to itself, which is invisible. @@ -33,23 +33,26 @@ To **hold** a color, add two steps with the same color — the second transition 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. +Each `Step` is 5 bytes (3 bytes RGB + 2 bytes duration). A `ShowDef` is 6 bytes (2-byte pointer + 2-byte length + 1-byte mode + 1 byte padding). The sketch currently uses roughly 25% of flash — there is room for many more shows. --- ## How the button works -`button.h/.cpp` implements a state machine with software debouncing: +Button handling uses the [OneButton](https://github.com/mathertel/OneButton) library. A `OneButton` instance is created in `cosplay_lights.ino` with callbacks attached in `setup()`: -1. **Debounce** — the raw pin state must be stable for `DEBOUNCE_MS` (50ms) before the debounced state updates. This filters contact bounce. +- **Single tap** → next show +- **Double tap** → previous show +- **Long press** → reset to show 0 -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. +The key timing values (configured via `setClickMs` and `setPressMs`): -3. **Tap counting** — each clean release increments a tap counter and records the release time. +| Setting | Value | Effect | +|---------|-------|--------| +| `setClickMs(400)` | 400ms | Single tap confirmed after 400ms of silence (to distinguish from a double tap) | +| `setPressMs(800)` | 800ms | Hold duration before long press fires | -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. +The button is polled every `loop()` iteration (not just once per frame), so it responds immediately even during the 16ms LED update window. OneButton handles debouncing internally. --- @@ -59,7 +62,7 @@ Timing constants are at the top of `button.cpp` and can be adjusted. The generation pipeline: 1. Reads every `.txt` in `converter/shows/`. -2. Converts `blue_pulse.txt` first (always index 0), then all others alphabetically. +2. Converts `blue_breath.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. diff --git a/PLAN.md b/PLAN.md index c4eb089..351f8dd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -11,7 +11,7 @@ This file records the original design brief and decisions so future contributors 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) + - Hold — reset to show 0 (blue breath) 3. **Arduino + WS2812B** — runs on an Arduino Uno with a WS2812B (NeoPixel) LED strip. @@ -56,7 +56,7 @@ Write/edit .txt files in converter/shows/ ## Future ideas (not yet implemented) -- Per-LED patterns (chase, sparkle, gradient segments) +- Per-LED patterns — chase, sparkle (random white flashes over a base color), gradient segments. The `led_controller.cpp` pattern system already has the hook: add a `#define PATTERN_SPARKLE` in `config.h` and implement the branch in `leds_apply_color()` - Multiple strip support - Battery power optimization (auto-dim, sleep) - SD card loading (no recompile needed for new shows) diff --git a/README.md b/README.md index c7e084e..8f439e8 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Uses the internal pull-up resistor. No external resistor needed. Pin 2 can be ch |-------|--------| | 1 tap | Next show | | 2 taps | Previous show | -| Hold (~0.8s) | Reset to show 0 (slow blue pulse) | +| Hold (~0.8s) | Reset to show 0 (blue breath) | ### Power warning — read this @@ -70,9 +70,12 @@ A 60-LED strip at full white = **3.6A** — far more than the Arduino's 5V pin c ## Quick start -### 1. Install the FastLED library +### 1. Install required libraries -Arduino IDE: **Sketch → Include Library → Manage Libraries** → search `FastLED` → Install. +Arduino IDE: **Sketch → Include Library → Manage Libraries**, then install both: + +- **FastLED** — LED strip control and color math +- **OneButton** — button debounce and gesture detection ### 2. Configure your hardware @@ -84,14 +87,28 @@ Edit `arduino/cosplay_lights/config.h`: ### 3. Write or edit show files -Show files live in `converter/shows/`. Four are included: +Show files live in `converter/shows/`. These 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 | +| File | Description | Mode | +|------|-------------|------| +| `blue_breath.txt` | Slow blue breathing — always show 0 (home/reset) | loop | +| `001_heartbeat.txt` | 3 heartbeats in bright red, then auto-advances to breathing | single | +| `002_breath_red.txt` | Breathing between dark maroon and bright red, 5s cycle | loop | +| `003_solid_red.txt` | Constant deep red | loop | +| `example_fade.txt` | Slow color cycle | loop | +| `example_party.txt` | Fast rainbow flashes | loop | +| `example_pulse.txt` | Red heartbeat | loop | + +**Show modes:** + +- `loop` — repeats indefinitely until the button is pressed +- `single` — plays once, then automatically advances to the next show + +Set the mode with a comment anywhere in the `.txt` file: +``` +// mode: single +``` +If no mode line is present, the show loops. **Show file format:** @@ -133,7 +150,21 @@ make upload PORT=/dev/ttyUSB1 # target a specific port 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. +The order in which shows appear when cycling through with the button is: `blue_breath` first (always show 0), then all others sorted alphabetically by filename. Prefix filenames with numbers (`001_`, `002_`, …) to control order. + +--- + +## File overview + +--- + +## Resources + +- [Wokwi](https://wokwi.com/projects/new/arduino-uno) — browser-based Arduino + LED strip emulator, useful for testing shows without hardware +- [tweaking4all — LED strip effects](https://www.tweaking4all.nl/hardware/arduino/adruino-led-strip-effecten/) — reference for animation patterns (sparkle, rainbow, etc.) +- [FastLED color fills reference](https://fastled.io/docs/da/de3/group___color_fills.html) +- [Adafruit LED animation guide](https://learn.adafruit.com/circuitpython-led-animations/basic-animations) +- [htmlcolorcodes.com](https://htmlcolorcodes.com/) — hex color picker for choosing show colors --- @@ -144,7 +175,6 @@ 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) diff --git a/arduino/cosplay_lights/show_001_heartbeat.h b/arduino/cosplay_lights/show_001_heartbeat.h new file mode 100644 index 0000000..173309e --- /dev/null +++ b/arduino/cosplay_lights/show_001_heartbeat.h @@ -0,0 +1,24 @@ +// Generated by convert_all.py from: 001_heartbeat.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_001_HEARTBEAT[] PROGMEM = { + { 5, 0, 0, 0}, // #050000, 0ms + {209, 14, 41, 120}, // #D10E29, 120ms + { 48, 0, 0, 100}, // #300000, 100ms + {209, 14, 41, 100}, // #D10E29, 100ms + { 5, 0, 0, 700}, // #050000, 700ms + {209, 14, 41, 120}, // #D10E29, 120ms + { 48, 0, 0, 100}, // #300000, 100ms + {209, 14, 41, 100}, // #D10E29, 100ms + { 5, 0, 0, 700}, // #050000, 700ms + {209, 14, 41, 120}, // #D10E29, 120ms + { 48, 0, 0, 100}, // #300000, 100ms + {209, 14, 41, 100}, // #D10E29, 100ms + { 5, 0, 0, 700}, // #050000, 700ms + {153, 40, 58, 2000}, // #99283A, 2000ms +}; +const uint16_t SHOW_001_HEARTBEAT_LENGTH = 14; diff --git a/arduino/cosplay_lights/show_002_breath_red.h b/arduino/cosplay_lights/show_002_breath_red.h new file mode 100644 index 0000000..808b53a --- /dev/null +++ b/arduino/cosplay_lights/show_002_breath_red.h @@ -0,0 +1,13 @@ +// Generated by convert_all.py from: 002_breath_red.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_002_BREATH_RED[] PROGMEM = { + {153, 40, 58, 0}, // #99283A, 0ms + {217, 30, 30, 2500}, // #D91E1E, 2500ms + {153, 40, 58, 2500}, // #99283A, 2500ms +}; +const uint16_t SHOW_002_BREATH_RED_LENGTH = 3; diff --git a/arduino/cosplay_lights/show_003_solid_red.h b/arduino/cosplay_lights/show_003_solid_red.h new file mode 100644 index 0000000..8b36c76 --- /dev/null +++ b/arduino/cosplay_lights/show_003_solid_red.h @@ -0,0 +1,12 @@ +// Generated by convert_all.py from: 003_solid_red.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_003_SOLID_RED[] PROGMEM = { + {153, 40, 58, 0}, // #99283A, 0ms + {153, 40, 58, 30000}, // #99283A, 30000ms +}; +const uint16_t SHOW_003_SOLID_RED_LENGTH = 2; diff --git a/arduino/cosplay_lights/shows.h b/arduino/cosplay_lights/shows.h index d0bb1af..e5e8a9c 100644 --- a/arduino/cosplay_lights/shows.h +++ b/arduino/cosplay_lights/shows.h @@ -14,6 +14,9 @@ // ---- Individual show data ---------------------------------------------- #include "show_blue_breath.h" +#include "show_001_heartbeat.h" +#include "show_002_breath_red.h" +#include "show_003_solid_red.h" #include "show_example_fade.h" #include "show_example_party.h" #include "show_example_pulse.h" @@ -21,9 +24,12 @@ // ---- Show index (PROGMEM) ---------------------------------------------- const ShowDef SHOWS[] PROGMEM = { {SHOW_BLUE_BREATH, SHOW_BLUE_BREATH_LENGTH, SHOW_LOOP}, // 0 — home show - {SHOW_EXAMPLE_FADE, SHOW_EXAMPLE_FADE_LENGTH, SHOW_LOOP}, // 1 - {SHOW_EXAMPLE_PARTY, SHOW_EXAMPLE_PARTY_LENGTH, SHOW_LOOP}, // 2 - {SHOW_EXAMPLE_PULSE, SHOW_EXAMPLE_PULSE_LENGTH, SHOW_LOOP}, // 3 + {SHOW_001_HEARTBEAT, SHOW_001_HEARTBEAT_LENGTH, SHOW_SINGLE}, // 1 + {SHOW_002_BREATH_RED, SHOW_002_BREATH_RED_LENGTH, SHOW_LOOP}, // 2 + {SHOW_003_SOLID_RED, SHOW_003_SOLID_RED_LENGTH, SHOW_LOOP}, // 3 + {SHOW_EXAMPLE_FADE, SHOW_EXAMPLE_FADE_LENGTH, SHOW_LOOP}, // 4 + {SHOW_EXAMPLE_PARTY, SHOW_EXAMPLE_PARTY_LENGTH, SHOW_LOOP}, // 5 + {SHOW_EXAMPLE_PULSE, SHOW_EXAMPLE_PULSE_LENGTH, SHOW_LOOP}, // 6 }; -const uint8_t SHOW_COUNT = 4; +const uint8_t SHOW_COUNT = 7; diff --git a/converter/shows/001_heartbeat.txt b/converter/shows/001_heartbeat.txt new file mode 100644 index 0000000..ca6b0ed --- /dev/null +++ b/converter/shows/001_heartbeat.txt @@ -0,0 +1,29 @@ +// Monster Hunter cosplay — heartbeat intro +// Three heartbeats in bright red, then fades into the base color of the breathing show. +// Auto-advances to 002_breath_red when done. +// +// mode: single +// Format: #RRGGBB, duration_ms + +#050000, 0 // start near black + +// beat 1 +#d10e29, 120 // lub — fast rise to bright red +#300000, 100 // dip between beats +#d10e29, 100 // dub +#050000, 700 // rest + +// beat 2 +#d10e29, 120 // lub +#300000, 100 // dip +#d10e29, 100 // dub +#050000, 700 // rest + +// beat 3 +#d10e29, 120 // lub +#300000, 100 // dip +#d10e29, 100 // dub +#050000, 700 // rest + +// 2-second fade into the base color of the breathing show +#99283a, 2000 diff --git a/converter/shows/002_breath_red.txt b/converter/shows/002_breath_red.txt new file mode 100644 index 0000000..41b9609 --- /dev/null +++ b/converter/shows/002_breath_red.txt @@ -0,0 +1,10 @@ +// Monster Hunter cosplay — breathing red +// Slow breathing between dark maroon (#99283a) and bright red (#d91e1e). +// One breath cycle lasts 5 seconds. Loops continuously. +// +// mode: loop +// Format: #RRGGBB, duration_ms + +#99283a, 0 // snap to dim base color +#d91e1e, 2500 // breathe in — slow rise to bright red +#99283a, 2500 // breathe out — slow fall back to dim diff --git a/converter/shows/003_solid_red.txt b/converter/shows/003_solid_red.txt new file mode 100644 index 0000000..5074db9 --- /dev/null +++ b/converter/shows/003_solid_red.txt @@ -0,0 +1,8 @@ +// Monster Hunter cosplay — solid deep red +// Constant dark maroon (#99283a). Stays on until the button is pressed. +// +// mode: loop +// Format: #RRGGBB, duration_ms + +#99283a, 0 // snap on instantly +#99283a, 30000 // hold (30s loop is invisible — same color each cycle)