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 <noreply@anthropic.com>
This commit is contained in:
+34
-32
@@ -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_<name>.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_<name>.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.")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
// Solid green
|
||||
//
|
||||
// mode: loop
|
||||
|
||||
#00CC00, 0 // snap to green
|
||||
#00CC00, 5000 // hold (fade from green to green is invisible)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user