Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
07e931b46This file is pinned to the selected release. Download the original to open it in your design tool.
"""
Convert firmware-linked pattern headers into freestanding module sources.
python firmware/toolchain/port_preset.py --all
python firmware/toolchain/port_preset.py firmware/patternflow/presets/preset_0510.h
python firmware/toolchain/port_preset.py --out-dir /tmp/mods submitted.h
For each input it writes <out-dir>/<slug>/pattern.cpp and module.json, defaulting
to firmware/modules/. The build service passes --out-dir so a submitted pattern
never lands in the repo.
The conversion is deliberately mechanical, because every pattern in this repo
shares one shape (NAME, KNOB_LABELS, setup, update, draw inside a namespace):
1. drop `#pragma once` and every firmware `#include`
2. insert a single `#include "pf_module.h"` (the module SDK)
3. append PF_REGISTER_PATTERN(<namespace>)
The pattern body itself is copied verbatim - the SDK recreates PFCanvas,
PFMath, PFColor, PFNoise, PFTables, PFMem, PANEL_RES_* and the Arduino
min/max/constrain helpers with identical semantics.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
# The worker captures this output and the console here may be cp949, while
# pattern names legitimately contain accents, CJK and emoji. Emit UTF-8 and
# never let an unprintable character turn a successful build into a crash.
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
ROOT = Path(__file__).resolve().parents[1] # firmware/
SKETCH = ROOT / "patternflow"
OUT_ROOT = ROOT / "modules"
NAMESPACE_RE = re.compile(r"^\s*namespace\s+([A-Za-z_]\w*)\s*\{", re.MULTILINE)
NAME_RE = re.compile(r'\bNAME\s*=\s*"([^"]*)"')
LABELS_RE = re.compile(r"KNOB_LABELS\s*\[\s*4\s*\]\s*=\s*\{(.*?)\}", re.DOTALL)
STRING_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')
AUTHOR_RE = re.compile(r"^//\s*Author:\s*(.+?)\s*$", re.MULTILINE)
LICENSE_RE = re.compile(r"^//\s*SPDX-License-Identifier:\s*(.+?)\s*$", re.MULTILINE)
def slugify(name: str) -> str:
slug = re.sub(r"[^A-Za-z0-9]+", "_", name).strip("_").lower()
return slug or "pattern"
def convert(src_path: Path, out_root: Path = OUT_ROOT) -> dict:
text = src_path.read_text(encoding="utf-8", errors="replace")
ns_match = NAMESPACE_RE.search(text)
if not ns_match:
raise SystemExit(f"{src_path}: no top-level namespace found")
namespace = ns_match.group(1)
name_match = NAME_RE.search(text)
display_name = name_match.group(1) if name_match else namespace
labels = ["k1", "k2", "k3", "k4"]
labels_match = LABELS_RE.search(text)
if labels_match:
found = STRING_RE.findall(labels_match.group(1))
if len(found) == 4:
labels = found
author_match = AUTHOR_RE.search(text)
license_match = LICENSE_RE.search(text)
kept: list[str] = []
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("#include"):
continue
if stripped.startswith("#pragma once"):
continue
kept.append(line)
body = "\n".join(kept).strip("\n")
header = (
f"// Generated by toolchain/port_preset.py from {src_path.as_posix()}\n"
f"// Do not edit directly - edit the source pattern and re-run the porter.\n"
f'#include "pf_module.h"\n'
)
out = f"{header}\n{body}\n\nPF_REGISTER_PATTERN({namespace})\n"
slug = slugify(display_name)
out_dir = out_root / slug
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "pattern.cpp").write_text(out, encoding="utf-8", newline="\n")
meta = {
"name": display_name,
"namespace": namespace,
"author": author_match.group(1) if author_match else "unknown",
"license": license_match.group(1) if license_match else "CC-BY-SA-4.0",
"abi": 1,
"knobs": labels,
"source": src_path.as_posix(),
}
(out_dir / "module.json").write_text(
json.dumps(meta, indent=2) + "\n", encoding="utf-8", newline="\n"
)
return {"slug": slug, **meta}
def discover() -> list[Path]:
files = sorted((SKETCH / "presets").glob("preset_*.h"))
files += sorted(SKETCH.glob("custom[0-9]*.h"))
return files
def resolve_source(argument: Path) -> Path:
"""Accept a header as absolute, cwd-relative, firmware-relative, or sketch-relative."""
for candidate in (argument, Path.cwd() / argument, ROOT / argument, SKETCH / argument):
if candidate.is_file():
return candidate.resolve()
raise SystemExit(f"No such pattern header: {argument}")
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("sources", nargs="*", type=Path, help="Pattern headers to convert")
ap.add_argument("--all", action="store_true", help="Convert presets/ and custom*.h")
ap.add_argument("--out-dir", type=Path, default=OUT_ROOT,
help="Where to write <slug>/pattern.cpp (default: firmware/modules)")
args = ap.parse_args()
sources = discover() if args.all else [resolve_source(p) for p in args.sources]
if not sources:
raise SystemExit("Nothing to convert. Pass files or --all.")
results = [convert(p, args.out_dir) for p in sources]
for r in results:
print(f"{r['slug']:<28} {r['name']:<24} ns={r['namespace']}")
print(f"\nConverted {len(results)} pattern(s) into {args.out_dir}")
if __name__ == "__main__":
main()