feat(led-driver): add preset clear command and runtime debug

This commit is contained in:
2026-04-21 00:44:28 +12:00
parent a22702df4d
commit 428ed8b884
7 changed files with 119 additions and 227 deletions

View File

@@ -1,43 +0,0 @@
"""Minimal Presets-like stub for pattern smoke tests (no machine / NeoPixel)."""
class _FakeNeo:
def __init__(self, count):
self._count = count
self.pixels = [(0, 0, 0)] * count
def fill(self, color):
self.pixels = [tuple(color) for _ in range(self._count)]
def __setitem__(self, index, value):
self.pixels[index] = tuple(value)
def __getitem__(self, index):
return self.pixels[index]
def write(self):
pass
class FakePresets:
"""Subset of led-driver Presets API used by patterns/*.py."""
def __init__(self, num_leds=24):
self.num_leds = num_leds
self.n = _FakeNeo(num_leds)
self.b = 255
self.step = 0
def apply_brightness(self, color, brightness_override=None):
local = brightness_override if brightness_override is not None else 255
effective_brightness = int(local * self.b / 255)
return tuple(int(c * effective_brightness / 255) for c in color)
def fill(self, color=None):
fill_color = color if color is not None else (0, 0, 0)
for i in range(self.num_leds):
self.n[i] = fill_color
self.n.write()
def off(self):
self.fill((0, 0, 0))

View File

@@ -1,174 +0,0 @@
"""
Smoke-test pattern generators with a fake driver (no hardware).
Run from repo root (CPython or MicroPython):
python3 led-driver/tests/pattern_smoke.py
Requires only the stdlib; loads ``preset.py`` and ``patterns/*.py`` from
``led-driver/src`` via import paths (MicroPython: copy ``src`` tree to the
device and run the same command if ``importlib.util`` is available, or set
``PYTHONPATH`` to ``led-driver/src`` and use ``python3`` on the host).
Exit code 0 on success, 1 on any failure.
"""
import importlib.util
import sys
from pathlib import Path
# -----------------------------------------------------------------------------
# Host-only ``utime`` so patterns import on CPython.
# -----------------------------------------------------------------------------
_UTIME_MS = [0]
def utime_advance(ms):
_UTIME_MS[0] += int(ms)
def _install_utime_shim():
if "utime" in sys.modules:
return
import types
m = types.ModuleType("utime")
def ticks_ms():
return _UTIME_MS[0]
def ticks_diff(a, b):
return a - b
def ticks_add(a, b):
return a + b
m.ticks_ms = ticks_ms
m.ticks_diff = ticks_diff
m.ticks_add = ticks_add
sys.modules["utime"] = m
# -----------------------------------------------------------------------------
# Load ``preset`` and pattern modules from ``led-driver/src`` (no sys.path).
# -----------------------------------------------------------------------------
_SRC = Path(__file__).resolve().parent.parent / "src"
_TESTS = Path(__file__).resolve().parent
def _load_module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise RuntimeError("no spec for %s" % path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _pattern_class_from_module(mod):
for attr_name in dir(mod):
attr = getattr(mod, attr_name)
if isinstance(attr, type) and hasattr(attr, "run"):
return attr
return None
def _load_preset_class():
preset_mod = _load_module("preset", _SRC / "preset.py")
return preset_mod.Preset
def _list_pattern_basenames():
pat_dir = _SRC / "patterns"
out = []
for p in sorted(pat_dir.iterdir()):
if p.suffix != ".py":
continue
if p.name in ("__init__.py", "main.py"):
continue
out.append(p.stem)
return out
def _default_preset_dict(basename):
"""Reasonable defaults so each pattern's ``run()`` can start."""
base = {
"p": basename,
"d": 100,
"b": 200,
"a": True,
"n1": 5,
"n2": 8,
"n3": 5,
"n4": 4,
}
if basename in ("rainbow", "colour_cycle"):
base["c"] = []
base["n1"] = 2
elif basename == "transition":
base["c"] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
base["d"] = 200
elif basename in ("chase", "circle"):
base["c"] = [(255, 0, 0), (0, 0, 255)]
elif basename == "pulse":
base["c"] = [(0, 200, 100)]
base["n1"] = 50
base["n2"] = 40
base["n3"] = 50
base["d"] = 30
elif basename in ("blink", "flicker"):
base["c"] = [(255, 100, 0)]
base["d"] = 80
elif basename == "flame":
base["c"] = []
base["d"] = 40
base["n1"] = 40
base["n2"] = 2000
base["n3"] = -1
base["n4"] = 0
else:
base["c"] = [(200, 200, 200)]
return base
def _run_pattern_ticks(Preset, driver, basename, steps, ms_per_tick):
_install_utime_shim()
mod = _load_module("patterns.%s" % basename, _SRC / "patterns" / (basename + ".py"))
cls = _pattern_class_from_module(mod)
if cls is None:
raise RuntimeError("no pattern class in %s" % basename)
preset = Preset(_default_preset_dict(basename))
gen = cls(driver).run(preset)
for _ in range(steps):
utime_advance(ms_per_tick)
next(gen)
def main():
failures = []
Preset = _load_preset_class()
fake_mod = _load_module("fake_driver", _TESTS / "fake_driver.py")
FakePresets = fake_mod.FakePresets
for basename in _list_pattern_basenames():
d = FakePresets(16)
try:
_run_pattern_ticks(Preset, d, basename, steps=80, ms_per_tick=50)
print("ok patterns.%s" % basename)
except Exception as exc:
print("FAIL patterns.%s: %r" % (basename, exc))
failures.append((basename, exc))
if failures:
print("%d pattern(s) failed" % len(failures))
return 1
print("all %d pattern smoke tests passed" % len(_list_pattern_basenames()))
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(130)