Files
Amirine_Cosplay_Lights/converter/convert_all.py
T
bgrolleman 11eb2584ef 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>
2026-05-21 10:16:56 +02:00

186 lines
6.0 KiB
Python

#!/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_<name>.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 0 is always 'blue_pulse' — the home/reset show.
All other shows are sorted alphabetically and follow after it.
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"
# The special first show — always placed at index 0.
HOME_SHOW = "blue_pulse"
# 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(filepath: Path) -> list[tuple[int, int, int, int]]:
"""
Parse a .txt show file. Returns list of (r, g, b, duration_ms) tuples.
Raises ValueError on malformed lines.
"""
steps = []
with open(filepath) as f:
for lineno, raw_line in enumerate(f, 1):
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
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_stems: list[str]) -> str:
"""Render the master shows.h index file."""
includes = "\n".join(f'#include "show_{stem}.h"' for stem in ordered_stems)
entries = "\n".join(
f" {{{filename_to_symbol(s)}, {filename_to_symbol(s)}_LENGTH}},"
+ (" // 0 — home show" if i == 0 else f" // {i}")
for i, s in enumerate(ordered_stems)
)
count = len(ordered_stems)
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.
// =====================================================================
//
// 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 ----------------------------------------------
{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"))
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
SKETCH_DIR.mkdir(parents=True, exist_ok=True)
converted = []
errors = []
for txt_path in ordered_files:
stem = txt_path.stem
symbol = filename_to_symbol(stem)
try:
steps = parse_show(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)")
converted.append(stem)
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)
# Regenerate shows.h
index_path = SKETCH_DIR / "shows.h"
index_path.write_text(render_shows_index(converted), encoding="utf-8")
print(f"\n OK shows.h updated ({len(converted)} shows, show 0 = {converted[0]})")
print( " Run 'make upload' to compile and send to the Arduino.")
if __name__ == "__main__":
main()