From ab2c1b34b4f4e64f6065511f570e0d3a24df660d Mon Sep 17 00:00:00 2001 From: Bas Grolleman Date: Sun, 24 May 2026 13:59:25 +0200 Subject: [PATCH] Replace example shows with numbered production shows and add sparkle flag - Rename all show .txt files with NNN_ numeric prefix so order is explicit and controlled by filename (001_heartbeat_red through 006_party) - Drop HOME_SHOW special-casing from convert_all.py; show 0 is simply the lowest-numbered file - Add SHOW_FLAG_SPARKLE support: shows can declare '// flags: sparkle' to overlay random white flashes on top of the base color each frame - Wire sparkle into led_controller and config.h (SPARKLE_CHANCE/FRAMES) - Replace old placeholder/example shows with the six production shows Co-Authored-By: Claude Sonnet 4.6 --- arduino/cosplay_lights/config.h | 10 ++- arduino/cosplay_lights/cosplay_lights.ino | 7 +- arduino/cosplay_lights/led_controller.cpp | 17 +++-- arduino/cosplay_lights/led_controller.h | 3 +- arduino/cosplay_lights/lightshow_format.h | 5 ++ ...1_heartbeat.h => show_001_heartbeat_red.h} | 13 ++-- arduino/cosplay_lights/show_002_breath_red.h | 13 ---- .../show_002_heartbeat_breathe.h | 12 ++++ ..._blue_breath.h => show_003_blue_sparkle.h} | 6 +- arduino/cosplay_lights/show_003_solid_red.h | 12 ---- .../cosplay_lights/show_004_pulse_yellow.h | 13 ++++ .../cosplay_lights/show_005_green_static.h | 12 ++++ ...{show_example_party.h => show_006_party.h} | 6 +- arduino/cosplay_lights/show_example_fade.h | 16 ----- arduino/cosplay_lights/show_example_pulse.h | 13 ---- arduino/cosplay_lights/shows.h | 32 +++++---- converter/convert_all.py | 66 ++++++++++--------- converter/shows/001_heartbeat.txt | 29 -------- converter/shows/001_heartbeat_red.txt | 23 +++++++ converter/shows/002_breath_red.txt | 10 --- converter/shows/002_heartbeat_breathe.txt | 7 ++ converter/shows/003_blue_sparkle.txt | 10 +++ converter/shows/003_solid_red.txt | 8 --- converter/shows/004_pulse_yellow.txt | 7 ++ converter/shows/005_green_static.txt | 6 ++ converter/shows/006_party.txt | 22 +++++++ converter/shows/blue_breath.txt | 12 ---- converter/shows/example_fade.txt | 12 ---- converter/shows/example_party.txt | 24 ------- converter/shows/example_pulse.txt | 10 --- 30 files changed, 205 insertions(+), 231 deletions(-) rename arduino/cosplay_lights/{show_001_heartbeat.h => show_001_heartbeat_red.h} (65%) delete mode 100644 arduino/cosplay_lights/show_002_breath_red.h create mode 100644 arduino/cosplay_lights/show_002_heartbeat_breathe.h rename arduino/cosplay_lights/{show_blue_breath.h => show_003_blue_sparkle.h} (71%) delete mode 100644 arduino/cosplay_lights/show_003_solid_red.h create mode 100644 arduino/cosplay_lights/show_004_pulse_yellow.h create mode 100644 arduino/cosplay_lights/show_005_green_static.h rename arduino/cosplay_lights/{show_example_party.h => show_006_party.h} (86%) delete mode 100644 arduino/cosplay_lights/show_example_fade.h delete mode 100644 arduino/cosplay_lights/show_example_pulse.h delete mode 100644 converter/shows/001_heartbeat.txt create mode 100644 converter/shows/001_heartbeat_red.txt delete mode 100644 converter/shows/002_breath_red.txt create mode 100644 converter/shows/002_heartbeat_breathe.txt create mode 100644 converter/shows/003_blue_sparkle.txt delete mode 100644 converter/shows/003_solid_red.txt create mode 100644 converter/shows/004_pulse_yellow.txt create mode 100644 converter/shows/005_green_static.txt create mode 100644 converter/shows/006_party.txt delete mode 100644 converter/shows/blue_breath.txt delete mode 100644 converter/shows/example_fade.txt delete mode 100644 converter/shows/example_party.txt delete mode 100644 converter/shows/example_pulse.txt diff --git a/arduino/cosplay_lights/config.h b/arduino/cosplay_lights/config.h index 40a26cc..24c3306 100644 --- a/arduino/cosplay_lights/config.h +++ b/arduino/cosplay_lights/config.h @@ -43,6 +43,14 @@ // -- Active pattern ------------------------------------------ // Controls how the current color is mapped to the LEDs. -// Only PATTERN_SOLID is included; see DETAILS.md for adding more. +// See DETAILS.md for adding more patterns. #define PATTERN_SOLID 0 #define ACTIVE_PATTERN PATTERN_SOLID + +// -- Sparkle tuning ------------------------------------------ +// Applied to shows that have the SHOW_FLAG_SPARKLE flag set. +// SPARKLE_CHANCE : 0–255 probability of a new sparkle each frame (~60fps). +// 80 ≈ 31%, giving roughly 2–3 simultaneous sparkles on 60 LEDs. +// SPARKLE_FRAMES : how many frames each sparkle stays white (1 frame ≈ 16ms). +#define SPARKLE_CHANCE 80 +#define SPARKLE_FRAMES 6 diff --git a/arduino/cosplay_lights/cosplay_lights.ino b/arduino/cosplay_lights/cosplay_lights.ino index 7993a79..56d3b57 100644 --- a/arduino/cosplay_lights/cosplay_lights.ino +++ b/arduino/cosplay_lights/cosplay_lights.ino @@ -6,7 +6,7 @@ * 1 tap — next show * 2 taps — previous show * 3 taps — lights off (any tap or hold resumes) - * hold — reset to show 0 (blue breath) + * hold — reset to show 0 (red heartbeat) * * To add or update shows: * 1. Add or edit a .txt file in converter/shows/ @@ -79,7 +79,7 @@ void setup() { s_button.setPressMs(800); s_button.attachClick([]() { load_show((s_show_index + 1) % SHOW_COUNT); }); s_button.attachDoubleClick([]() { load_show((s_show_index + SHOW_COUNT - 1) % SHOW_COUNT); }); - s_button.attachMultiClick([]() { if (s_button.getNumberClicks() == 3) { s_lights_off = true; leds_apply_color(CRGB::Black); leds_show(); } }); + s_button.attachMultiClick([]() { if (s_button.getNumberClicks() == 3) { s_lights_off = true; leds_apply_color(CRGB::Black, false); leds_show(); } }); s_button.attachLongPressStart([]() { load_show(0); }); } @@ -97,7 +97,8 @@ void loop() { CRGB to = CRGB(step.r, step.g, step.b); uint8_t t8 = step_progress(step, now); - leds_apply_color(blend(s_from_color, to, t8)); + bool sparkle = (s_show.flags & SHOW_FLAG_SPARKLE) != 0; + leds_apply_color(blend(s_from_color, to, t8), sparkle); leds_show(); if (t8 == 255) advance_step(to, now); diff --git a/arduino/cosplay_lights/led_controller.cpp b/arduino/cosplay_lights/led_controller.cpp index 67ddb91..78d0e7e 100644 --- a/arduino/cosplay_lights/led_controller.cpp +++ b/arduino/cosplay_lights/led_controller.cpp @@ -10,14 +10,21 @@ void leds_begin() { FastLED.clear(true); // clear + show: strip starts fully off } -void leds_apply_color(CRGB color) { +void leds_apply_color(CRGB color, bool sparkle) { #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 + if (!sparkle) return; + static uint8_t sparkle_timer[NUM_LEDS]; + if (random8() < SPARKLE_CHANCE) { + sparkle_timer[random8(NUM_LEDS)] = SPARKLE_FRAMES; + } + for (uint8_t i = 0; i < NUM_LEDS; i++) { + if (sparkle_timer[i] > 0) { + leds[i] = CRGB::White; + sparkle_timer[i]--; + } + } } void leds_show() { diff --git a/arduino/cosplay_lights/led_controller.h b/arduino/cosplay_lights/led_controller.h index c126cd2..6df6c93 100644 --- a/arduino/cosplay_lights/led_controller.h +++ b/arduino/cosplay_lights/led_controller.h @@ -7,8 +7,9 @@ void leds_begin(); // Apply 'color' to all LEDs using the pattern defined in config.h (ACTIVE_PATTERN). +// If 'sparkle' is true, randomly flash individual LEDs white on top of the base fill. // Call leds_show() afterwards to push the update to the physical strip. -void leds_apply_color(CRGB color); +void leds_apply_color(CRGB color, bool sparkle = false); // 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 index 01f6a4c..5ff85b6 100644 --- a/arduino/cosplay_lights/lightshow_format.h +++ b/arduino/cosplay_lights/lightshow_format.h @@ -16,6 +16,9 @@ #define SHOW_LOOP 0 // repeat the show indefinitely #define SHOW_SINGLE 1 // play once, then auto-advance to the next show +// Show flags (bitfield for ShowDef.flags). +#define SHOW_FLAG_SPARKLE 0x01 // overlay random white sparkles on this show + // 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) @@ -27,6 +30,7 @@ struct ShowDef { const Step* steps; // Pointer to a PROGMEM Step array uint16_t length; // Number of steps in the array uint8_t mode; // SHOW_LOOP or SHOW_SINGLE + uint8_t flags; // Bitfield: see SHOW_FLAG_* above }; // Read one Step from PROGMEM into a regular struct. @@ -47,5 +51,6 @@ inline ShowDef read_show_def(const ShowDef* ptr) { sd.steps = (const Step*)pgm_read_word(&ptr->steps); sd.length = pgm_read_word(&ptr->length); sd.mode = pgm_read_byte(&ptr->mode); + sd.flags = pgm_read_byte(&ptr->flags); return sd; } \ No newline at end of file diff --git a/arduino/cosplay_lights/show_001_heartbeat.h b/arduino/cosplay_lights/show_001_heartbeat_red.h similarity index 65% rename from arduino/cosplay_lights/show_001_heartbeat.h rename to arduino/cosplay_lights/show_001_heartbeat_red.h index 173309e..56db628 100644 --- a/arduino/cosplay_lights/show_001_heartbeat.h +++ b/arduino/cosplay_lights/show_001_heartbeat_red.h @@ -1,24 +1,23 @@ -// Generated by convert_all.py from: 001_heartbeat.txt +// Generated by convert_all.py from: 001_heartbeat_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_001_HEARTBEAT[] PROGMEM = { +const Step SHOW_001_HEARTBEAT_RED[] 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 + { 5, 0, 0, 600}, // #050000, 600ms {209, 14, 41, 120}, // #D10E29, 120ms { 48, 0, 0, 100}, // #300000, 100ms {209, 14, 41, 100}, // #D10E29, 100ms - { 5, 0, 0, 700}, // #050000, 700ms + { 5, 0, 0, 600}, // #050000, 600ms {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 + { 5, 0, 0, 600}, // #050000, 600ms }; -const uint16_t SHOW_001_HEARTBEAT_LENGTH = 14; +const uint16_t SHOW_001_HEARTBEAT_RED_LENGTH = 13; diff --git a/arduino/cosplay_lights/show_002_breath_red.h b/arduino/cosplay_lights/show_002_breath_red.h deleted file mode 100644 index 808b53a..0000000 --- a/arduino/cosplay_lights/show_002_breath_red.h +++ /dev/null @@ -1,13 +0,0 @@ -// 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_002_heartbeat_breathe.h b/arduino/cosplay_lights/show_002_heartbeat_breathe.h new file mode 100644 index 0000000..96ffa00 --- /dev/null +++ b/arduino/cosplay_lights/show_002_heartbeat_breathe.h @@ -0,0 +1,12 @@ +// Generated by convert_all.py from: 002_heartbeat_breathe.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_HEARTBEAT_BREATHE[] PROGMEM = { + {209, 14, 41, 3600}, // #D10E29, 3600ms + { 5, 0, 0, 3600}, // #050000, 3600ms +}; +const uint16_t SHOW_002_HEARTBEAT_BREATHE_LENGTH = 2; diff --git a/arduino/cosplay_lights/show_blue_breath.h b/arduino/cosplay_lights/show_003_blue_sparkle.h similarity index 71% rename from arduino/cosplay_lights/show_blue_breath.h rename to arduino/cosplay_lights/show_003_blue_sparkle.h index fb03f87..273ba44 100644 --- a/arduino/cosplay_lights/show_blue_breath.h +++ b/arduino/cosplay_lights/show_003_blue_sparkle.h @@ -1,15 +1,15 @@ -// Generated by convert_all.py from: blue_breath.txt +// Generated by convert_all.py from: 003_blue_sparkle.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_BREATH[] PROGMEM = { +const Step SHOW_003_BLUE_SPARKLE[] PROGMEM = { { 5, 0, 24, 0}, // #050018, 0ms { 32, 32, 255, 2500}, // #2020FF, 2500ms { 32, 32, 255, 400}, // #2020FF, 400ms { 5, 0, 24, 2500}, // #050018, 2500ms { 5, 0, 24, 800}, // #050018, 800ms }; -const uint16_t SHOW_BLUE_BREATH_LENGTH = 5; +const uint16_t SHOW_003_BLUE_SPARKLE_LENGTH = 5; diff --git a/arduino/cosplay_lights/show_003_solid_red.h b/arduino/cosplay_lights/show_003_solid_red.h deleted file mode 100644 index 8b36c76..0000000 --- a/arduino/cosplay_lights/show_003_solid_red.h +++ /dev/null @@ -1,12 +0,0 @@ -// 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/show_004_pulse_yellow.h b/arduino/cosplay_lights/show_004_pulse_yellow.h new file mode 100644 index 0000000..41111d8 --- /dev/null +++ b/arduino/cosplay_lights/show_004_pulse_yellow.h @@ -0,0 +1,13 @@ +// Generated by convert_all.py from: 004_pulse_yellow.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_004_PULSE_YELLOW[] PROGMEM = { + { 32, 32, 0, 0}, // #202000, 0ms + {255, 255, 0, 1200}, // #FFFF00, 1200ms + { 32, 32, 0, 1200}, // #202000, 1200ms +}; +const uint16_t SHOW_004_PULSE_YELLOW_LENGTH = 3; diff --git a/arduino/cosplay_lights/show_005_green_static.h b/arduino/cosplay_lights/show_005_green_static.h new file mode 100644 index 0000000..e5510c6 --- /dev/null +++ b/arduino/cosplay_lights/show_005_green_static.h @@ -0,0 +1,12 @@ +// Generated by convert_all.py from: 005_green_static.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_005_GREEN_STATIC[] PROGMEM = { + { 0, 204, 0, 0}, // #00CC00, 0ms + { 0, 204, 0, 5000}, // #00CC00, 5000ms +}; +const uint16_t SHOW_005_GREEN_STATIC_LENGTH = 2; diff --git a/arduino/cosplay_lights/show_example_party.h b/arduino/cosplay_lights/show_006_party.h similarity index 86% rename from arduino/cosplay_lights/show_example_party.h rename to arduino/cosplay_lights/show_006_party.h index 2945e83..a1d6c12 100644 --- a/arduino/cosplay_lights/show_example_party.h +++ b/arduino/cosplay_lights/show_006_party.h @@ -1,11 +1,11 @@ -// Generated by convert_all.py from: example_party.txt +// Generated by convert_all.py from: 006_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 = { +const Step SHOW_006_PARTY[] PROGMEM = { {255, 0, 0, 0}, // #FF0000, 0ms {255, 0, 0, 150}, // #FF0000, 150ms {255, 136, 0, 0}, // #FF8800, 0ms @@ -24,4 +24,4 @@ const Step SHOW_EXAMPLE_PARTY[] PROGMEM = { {255, 255, 255, 150}, // #FFFFFF, 150ms { 0, 0, 0, 500}, // #000000, 500ms }; -const uint16_t SHOW_EXAMPLE_PARTY_LENGTH = 17; +const uint16_t SHOW_006_PARTY_LENGTH = 17; diff --git a/arduino/cosplay_lights/show_example_fade.h b/arduino/cosplay_lights/show_example_fade.h deleted file mode 100644 index 2b6c7ea..0000000 --- a/arduino/cosplay_lights/show_example_fade.h +++ /dev/null @@ -1,16 +0,0 @@ -// 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_pulse.h b/arduino/cosplay_lights/show_example_pulse.h deleted file mode 100644 index f99a818..0000000 --- a/arduino/cosplay_lights/show_example_pulse.h +++ /dev/null @@ -1,13 +0,0 @@ -// 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 index e5e8a9c..1f545b9 100644 --- a/arduino/cosplay_lights/shows.h +++ b/arduino/cosplay_lights/shows.h @@ -1,10 +1,10 @@ // ===================================================================== // shows.h — Master show index. // Generated by: make shows (converter/convert_all.py) -// Do not edit manually — add .txt files to converter/shows/ instead. +// Do not edit manually — add NNN_.txt files to converter/shows/ instead. // ===================================================================== // -// Show 0 is always the home/reset show (blue breath). +// Show 0 is the lowest-numbered .txt file (home/reset show). // Holding the button resets back to show 0. // // SPDX-License-Identifier: BSD-2-Clause @@ -13,23 +13,21 @@ #include "lightshow_format.h" // ---- 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" +#include "show_001_heartbeat_red.h" +#include "show_002_heartbeat_breathe.h" +#include "show_003_blue_sparkle.h" +#include "show_004_pulse_yellow.h" +#include "show_005_green_static.h" +#include "show_006_party.h" // ---- Show index (PROGMEM) ---------------------------------------------- const ShowDef SHOWS[] PROGMEM = { - {SHOW_BLUE_BREATH, SHOW_BLUE_BREATH_LENGTH, SHOW_LOOP}, // 0 — home show - {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 + {SHOW_001_HEARTBEAT_RED, SHOW_001_HEARTBEAT_RED_LENGTH, SHOW_SINGLE, 0}, // 0 — home show + {SHOW_002_HEARTBEAT_BREATHE, SHOW_002_HEARTBEAT_BREATHE_LENGTH, SHOW_LOOP, 0}, // 1 + {SHOW_003_BLUE_SPARKLE, SHOW_003_BLUE_SPARKLE_LENGTH, SHOW_LOOP, SHOW_FLAG_SPARKLE}, // 2 + {SHOW_004_PULSE_YELLOW, SHOW_004_PULSE_YELLOW_LENGTH, SHOW_LOOP, 0}, // 3 + {SHOW_005_GREEN_STATIC, SHOW_005_GREEN_STATIC_LENGTH, SHOW_LOOP, 0}, // 4 + {SHOW_006_PARTY, SHOW_006_PARTY_LENGTH, SHOW_SINGLE, 0}, // 5 }; -const uint8_t SHOW_COUNT = 7; +const uint8_t SHOW_COUNT = 6; diff --git a/converter/convert_all.py b/converter/convert_all.py index e7fc00a..4a68920 100644 --- a/converter/convert_all.py +++ b/converter/convert_all.py @@ -10,8 +10,12 @@ Usage: python converter/convert_all.py (or via Makefile: make shows) -Show 0 is always 'blue_breath' — the home/reset show. -All other shows are sorted alphabetically and follow after it. +Show files must be named NNN_.txt (e.g. 001_heartbeat_red.txt). +They run in numeric order; show 0 (the home/reset show) is the lowest-numbered file. + +Show file directives (in comment lines): + // mode: single — play once, then advance to the next show (default: loop) + // flags: sparkle — overlay random white sparkles on this show SPDX-License-Identifier: BSD-2-Clause """ @@ -27,9 +31,6 @@ 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_breath" - # Regex to match a valid step line: #RRGGBB, duration_ms STEP_PATTERN = re.compile(r'^\s*#([0-9A-Fa-f]{6})\s*,\s*(\d+)') @@ -46,21 +47,29 @@ 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_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], str]: +def parse_show_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], str, int]: """ - Parse a .txt show file in one pass. Returns (steps, mode_constant). + Parse a .txt show file in one pass. Returns (steps, mode_constant, flags). steps: list of (r, g, b, duration_ms) tuples mode_constant: 'SHOW_LOOP' or 'SHOW_SINGLE' (default SHOW_LOOP if not set) + flags: integer bitmask (0x01 = sparkle, matches SHOW_FLAG_SPARKLE in lightshow_format.h) Raises ValueError on malformed lines or empty files. """ steps = [] mode = "SHOW_LOOP" + flags = 0 with open(filepath) as f: for lineno, raw_line in enumerate(f, 1): - m = re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE) - if m: + if re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE): + m = re.match(r'^\s*//\s*mode:\s*(\w+)', raw_line, re.IGNORECASE) mode = "SHOW_SINGLE" if m.group(1).lower() == "single" else "SHOW_LOOP" continue + mf = re.match(r'^\s*//\s*flags:\s*(.+)', raw_line, re.IGNORECASE) + if mf: + flag_tokens = [t.strip().lower() for t in mf.group(1).split(",")] + if "sparkle" in flag_tokens: + flags |= 0x01 + continue line = raw_line.split("//")[0].strip() if not line: continue @@ -75,7 +84,7 @@ def parse_show_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], st steps.append((r, g, b, duration)) if not steps: raise ValueError(f"{filepath.name}: file contains no steps.") - return steps, mode + return steps, mode, flags def render_show_header(steps: list, source_name: str, symbol: str) -> str: @@ -101,23 +110,23 @@ def render_show_header(steps: list, source_name: str, symbol: str) -> str: return "\n".join(lines) -def render_shows_index(ordered: list[tuple[str, str]]) -> str: - """Render the master shows.h index file. ordered = [(stem, mode_constant), ...]""" - includes = "\n".join(f'#include "show_{stem}.h"' for stem, _ in ordered) +def render_shows_index(ordered: list[tuple[str, str, int]]) -> str: + """Render the master shows.h index file. ordered = [(stem, mode_constant, flags), ...]""" + includes = "\n".join(f'#include "show_{stem}.h"' for stem, _, __ in ordered) entries = "\n".join( - f" {{{filename_to_symbol(s)}, {filename_to_symbol(s)}_LENGTH, {mode}}}," + f" {{{filename_to_symbol(s)}, {filename_to_symbol(s)}_LENGTH, {mode}, {'SHOW_FLAG_SPARKLE' if flags & 0x01 else '0'}}}," + (" // 0 — home show" if i == 0 else f" // {i}") - for i, (s, mode) in enumerate(ordered) + for i, (s, mode, flags) in enumerate(ordered) ) count = len(ordered) 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. +// Do not edit manually — add NNN_.txt files to converter/shows/ instead. // ===================================================================== // -// Show 0 is always the home/reset show (blue breath). +// Show 0 is the lowest-numbered .txt file (home/reset show). // Holding the button resets back to show 0. // // SPDX-License-Identifier: BSD-2-Clause @@ -140,20 +149,12 @@ const uint8_t SHOW_COUNT = {count}; # ---- Main -------------------------------------------------------------- def main() -> None: - txt_files = sorted(SHOWS_DIR.glob("*.txt"), key=lambda p: p.stem.lower()) + txt_files = sorted(SHOWS_DIR.glob("*.txt"), key=lambda p: p.name.lower()) 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 + ordered_files = txt_files SKETCH_DIR.mkdir(parents=True, exist_ok=True) @@ -164,12 +165,13 @@ def main() -> None: stem = txt_path.stem symbol = filename_to_symbol(stem) try: - steps, mode = parse_show_file(txt_path) + steps, mode, flags = parse_show_file(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, {mode})") - converted.append((stem, mode)) + flag_str = " sparkle" if flags & 0x01 else "" + print(f" OK {txt_path.name} → show_{stem}.h ({len(steps)} steps, {mode}{flag_str})") + converted.append((stem, mode, flags)) except ValueError as e: print(f" ERR {e}") errors.append(stem) @@ -182,7 +184,7 @@ def main() -> None: sys.exit(1) # Remove stale show_*.h files no longer in the converted list. - expected = {SKETCH_DIR / f"show_{stem}.h" for stem, _ in converted} + expected = {SKETCH_DIR / f"show_{stem}.h" for stem, _, __ in converted} for stale in SKETCH_DIR.glob("show_*.h"): if stale not in expected: stale.unlink() @@ -191,7 +193,7 @@ def main() -> None: # Regenerate shows.h index_path = SKETCH_DIR / "shows.h" index_path.write_text(render_shows_index(converted), encoding="utf-8") - stems = [s for s, _ in converted] + stems = [s for s, _, __ in converted] print(f"\n OK shows.h updated ({len(converted)} shows, show 0 = {stems[0]})") print( " Run 'make upload' to compile and send to the Arduino.") diff --git a/converter/shows/001_heartbeat.txt b/converter/shows/001_heartbeat.txt deleted file mode 100644 index ca6b0ed..0000000 --- a/converter/shows/001_heartbeat.txt +++ /dev/null @@ -1,29 +0,0 @@ -// 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/001_heartbeat_red.txt b/converter/shows/001_heartbeat_red.txt new file mode 100644 index 0000000..194e063 --- /dev/null +++ b/converter/shows/001_heartbeat_red.txt @@ -0,0 +1,23 @@ +// Red Heartbeat × 2 then Red Breathing — 2 lub-dub beats, then slow breathing, repeat. +// +// mode: single + +#050000, 0 // start near black + +// beat 1 +#d10e29, 120 // lub — fast rise to bright red +#300000, 100 // dip between beats +#d10e29, 100 // dub +#050000, 600 // rest + +// beat 2 +#d10e29, 120 // lub +#300000, 100 // dip +#d10e29, 100 // dub +#050000, 600 // rest into breathing + +// beat 3 +#d10e29, 120 // lub +#300000, 100 // dip +#d10e29, 100 // dub +#050000, 600 // rest into breathing diff --git a/converter/shows/002_breath_red.txt b/converter/shows/002_breath_red.txt deleted file mode 100644 index 41b9609..0000000 --- a/converter/shows/002_breath_red.txt +++ /dev/null @@ -1,10 +0,0 @@ -// 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/002_heartbeat_breathe.txt b/converter/shows/002_heartbeat_breathe.txt new file mode 100644 index 0000000..09078c7 --- /dev/null +++ b/converter/shows/002_heartbeat_breathe.txt @@ -0,0 +1,7 @@ +// Red Heartbeat × 2 then Red Breathing — 2 lub-dub beats, then slow breathing, repeat. +// +// mode: loop + +// breathing +#d10e29, 3600 // fade up to bright red +#050000, 3600 // fade back down to near black diff --git a/converter/shows/003_blue_sparkle.txt b/converter/shows/003_blue_sparkle.txt new file mode 100644 index 0000000..843656a --- /dev/null +++ b/converter/shows/003_blue_sparkle.txt @@ -0,0 +1,10 @@ +// Blue breath with sparkle overlay +// +// flags: sparkle +// mode: loop + +#050018, 0 // snap to near-black blue +#2020FF, 2500 // breathe in — slow rise to bright blue +#2020FF, 400 // hold at peak +#050018, 2500 // breathe out — slow fall to near-black +#050018, 800 // pause before next breath diff --git a/converter/shows/003_solid_red.txt b/converter/shows/003_solid_red.txt deleted file mode 100644 index 5074db9..0000000 --- a/converter/shows/003_solid_red.txt +++ /dev/null @@ -1,8 +0,0 @@ -// 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) diff --git a/converter/shows/004_pulse_yellow.txt b/converter/shows/004_pulse_yellow.txt new file mode 100644 index 0000000..f15f616 --- /dev/null +++ b/converter/shows/004_pulse_yellow.txt @@ -0,0 +1,7 @@ +// Yellow pulse — slow breathing effect +// +// mode: loop + +#202000, 0 // start at dim yellow +#FFFF00, 1200 // fade up to bright yellow +#202000, 1200 // fade back down to dim yellow diff --git a/converter/shows/005_green_static.txt b/converter/shows/005_green_static.txt new file mode 100644 index 0000000..1ea1933 --- /dev/null +++ b/converter/shows/005_green_static.txt @@ -0,0 +1,6 @@ +// Solid green +// +// mode: loop + +#00CC00, 0 // snap to green +#00CC00, 5000 // hold (fade from green to green is invisible) diff --git a/converter/shows/006_party.txt b/converter/shows/006_party.txt new file mode 100644 index 0000000..00fe537 --- /dev/null +++ b/converter/shows/006_party.txt @@ -0,0 +1,22 @@ +// Party mode — rapid rainbow color flashes +// Plays once then auto-advances to the next show. +// +// mode: single + +#FF0000, 0 // snap to red +#FF0000, 150 // hold red +#FF8800, 0 // snap to orange +#FF8800, 150 // hold orange +#FFFF00, 0 // snap to yellow +#FFFF00, 150 // hold yellow +#00FF00, 0 // snap to green +#00FF00, 150 // hold green +#00FFFF, 0 // snap to cyan +#00FFFF, 150 // hold cyan +#0000FF, 0 // snap to blue +#0000FF, 150 // hold blue +#FF00FF, 0 // snap to magenta +#FF00FF, 150 // hold magenta +#FFFFFF, 0 // snap to white +#FFFFFF, 150 // hold white +#000000, 500 // fade to off — brief pause before advancing diff --git a/converter/shows/blue_breath.txt b/converter/shows/blue_breath.txt deleted file mode 100644 index 55d8617..0000000 --- a/converter/shows/blue_breath.txt +++ /dev/null @@ -1,12 +0,0 @@ -// Blue breath — home / reset show (always index 0) -// A slow, natural breathing effect in deep blue. -// Loops continuously as the default show. -// -// mode: loop -// Format: #RRGGBB, duration_ms - -#050018, 0 // snap to near-black blue — sets the starting point -#2020FF, 2500 // breathe in — slow rise to bright blue -#2020FF, 400 // hold at peak -#050018, 2500 // breathe out — slow fall to near-black -#050018, 800 // pause before next breath diff --git a/converter/shows/example_fade.txt b/converter/shows/example_fade.txt deleted file mode 100644 index 4c29732..0000000 --- a/converter/shows/example_fade.txt +++ /dev/null @@ -1,12 +0,0 @@ -// 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 deleted file mode 100644 index b2bd283..0000000 --- a/converter/shows/example_party.txt +++ /dev/null @@ -1,24 +0,0 @@ -// 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 deleted file mode 100644 index 1cb3597..0000000 --- a/converter/shows/example_pulse.txt +++ /dev/null @@ -1,10 +0,0 @@ -// 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