11eb2584ef
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>
126 lines
3.8 KiB
Python
126 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
convert.py — Convert a single .txt show file to an Arduino header.
|
|
|
|
Use this to preview or test a single show.
|
|
For the full workflow (all shows + regenerate shows.h), use convert_all.py.
|
|
|
|
Usage:
|
|
python converter/convert.py path/to/show.txt
|
|
|
|
Output:
|
|
show_<name>.h written to the current directory.
|
|
Move it to arduino/cosplay_lights/ then run: make upload
|
|
|
|
Show file format:
|
|
Each step is one line: #RRGGBB, duration_ms
|
|
Lines starting with // are comments and are ignored.
|
|
Blank lines are ignored.
|
|
|
|
duration_ms controls how long the fade from the PREVIOUS color takes:
|
|
0 = instant switch (no fade)
|
|
500 = half-second fade
|
|
5000 = five-second slow fade
|
|
|
|
To hold a color, add a second step with the same color and the hold time as duration:
|
|
#FF0000, 0 // snap to red
|
|
#FF0000, 2000 // hold red for 2 seconds
|
|
#0000FF, 3000 // fade to blue over 3 seconds
|
|
|
|
SPDX-License-Identifier: BSD-2-Clause
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
from pathlib import Path
|
|
|
|
STEP_PATTERN = re.compile(r'^\s*#([0-9A-Fa-f]{6})\s*,\s*(\d+)')
|
|
|
|
|
|
def filename_to_symbol(stem: str) -> str:
|
|
"""Convert a filename stem to an uppercase C array symbol."""
|
|
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"Line {lineno}: expected '#RRGGBB, duration_ms' "
|
|
f"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("Show file contains no steps.")
|
|
return steps
|
|
|
|
|
|
def render_header(steps: list, source_name: str, symbol: str) -> str:
|
|
"""Render parsed steps as a C++ header file string."""
|
|
lines = [
|
|
f"// Generated by convert.py from: {source_name}",
|
|
"// Do not edit manually — edit the .txt file instead.",
|
|
"// 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 main() -> None:
|
|
if len(sys.argv) != 2:
|
|
print(__doc__)
|
|
sys.exit(1)
|
|
|
|
source_path = Path(sys.argv[1])
|
|
if not source_path.exists():
|
|
print(f"Error: file not found: {source_path}")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
steps = parse_show(source_path)
|
|
except ValueError as e:
|
|
print(f"Error: {e}")
|
|
sys.exit(1)
|
|
|
|
symbol = filename_to_symbol(source_path.stem)
|
|
header_text = render_header(steps, source_path.name, symbol)
|
|
output_path = Path(f"show_{source_path.stem}.h")
|
|
output_path.write_text(header_text, encoding="utf-8")
|
|
|
|
print(f"OK {len(steps)} steps → {output_path}")
|
|
print(f" Move to 'arduino/cosplay_lights/' then run 'make upload'.")
|
|
print(f" Or run 'make shows' to convert all shows at once.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|