#!/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 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 """ 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" # 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_file(filepath: Path) -> tuple[list[tuple[int, int, int, int]], str, int]: """ 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): 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 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, mode, flags 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: 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}, {'SHOW_FLAG_SPARKLE' if flags & 0x01 else '0'}}}," + (" // 0 — home show" if i == 0 else f" // {i}") 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 NNN_.txt files to converter/shows/ instead. // ===================================================================== // // 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 #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"), key=lambda p: p.name.lower()) if not txt_files: print(f"No .txt files found in {SHOWS_DIR}") sys.exit(1) ordered_files = txt_files SKETCH_DIR.mkdir(parents=True, exist_ok=True) converted = [] # list of (stem, mode_constant) errors = [] for txt_path in ordered_files: stem = txt_path.stem symbol = filename_to_symbol(stem) try: 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") 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) 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) # Remove stale show_*.h files no longer in the converted list. 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() print(f" RM {stale.name} (removed — no matching .txt file)") # 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] 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.") if __name__ == "__main__": main()