patterns: add rainbow, specto, and radiate (out then dark-out)

radiate: origins every n1, step by delay, stop when full, dark wave outward, ensure strip off at end, run once

alternating: use n1 as ON width and n2 as OFF width; phase via self.step

pulse: attack (n1), hold (delay), decay (n2); stop at end

tests: add specto sweep (n1_sequence) and radiate demo; include n index per message; use nested {name:{...}} schema; support iterations/repeat-delay
This commit is contained in:
2025-09-16 22:28:51 +12:00
parent d599af271b
commit 8cfb3e156b
2 changed files with 143 additions and 7 deletions

View File

@@ -18,7 +18,10 @@ class Patterns(PatternBase): # Inherit from PatternBase
"fill_range": self.fill_range, "fill_range": self.fill_range,
"n_chase": self.n_chase, "n_chase": self.n_chase,
"alternating": self.alternating, "alternating": self.alternating,
"pulse": self.pulse "pulse": self.pulse,
"rainbow": self.rainbow,
"specto": self.specto,
"radiate": self.radiate,
} }
self.step = 0 self.step = 0
@@ -110,12 +113,138 @@ class Patterns(PatternBase): # Inherit from PatternBase
def pulse(self): def pulse(self):
self.fill(self.apply_brightness(self.colors[0])) # Envelope: attack=n1 ms, hold=delay ms, decay=n2 ms
start = utime.ticks_ms() attack_ms = max(0, int(self.n1))
hold_ms = max(0, int(self.delay))
decay_ms = max(0, int(self.n2))
while utime.ticks_diff(utime.ticks_ms(), start) < self.delay: base = self.colors[0] if len(self.colors) > 0 else (255, 255, 255)
pass full_brightness = max(0, min(255, int(self.brightness)))
# Attack phase (0 -> full)
if attack_ms > 0:
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < attack_ms:
elapsed = utime.ticks_diff(utime.ticks_ms(), start)
frac = elapsed / attack_ms if attack_ms > 0 else 1.0
b = int(full_brightness * frac)
self.fill(self.apply_brightness(base, brightness_override=b))
else:
self.fill(self.apply_brightness(base, brightness_override=full_brightness))
# Hold phase
if hold_ms > 0:
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < hold_ms:
pass
# Decay phase (full -> 0)
if decay_ms > 0:
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < decay_ms:
elapsed = utime.ticks_diff(utime.ticks_ms(), start)
frac = 1.0 - (elapsed / decay_ms if decay_ms > 0 else 1.0)
if frac < 0:
frac = 0
b = int(full_brightness * frac)
self.fill(self.apply_brightness(base, brightness_override=b))
# Ensure off at the end and stop auto-run
self.fill((0, 0, 0)) self.fill((0, 0, 0))
self.run = False
return self.delay
def rainbow(self):
# Wheel function to map 0-255 to RGB
def wheel(pos):
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
elif pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
else:
pos -= 170
return (0, pos * 3, 255 - pos * 3)
for i in range(self.num_leds):
rc_index = (i * 256 // max(1, self.num_leds)) + self.pattern_step
self.n[i] = self.apply_brightness(wheel(rc_index & 255))
self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256
return max(1, int(self.delay // 5))
def specto(self):
# Light up LEDs from 0 up to n1 (exclusive) and turn the rest off
count = int(self.n1)
if count < 0:
count = 0
if count > self.num_leds:
count = self.num_leds
color = self.apply_brightness(self.colors[0] if len(self.colors) > 0 else (255, 255, 255))
for i in range(self.num_leds):
self.n[i] = color if i < count else (0, 0, 0)
self.n.write()
return self.delay
def radiate(self):
# Radiate outward from origins spaced every n1 LEDs, stepping each ring by self.delay
sep = max(1, int(self.n1) if self.n1 else 1)
color = self.apply_brightness(self.colors[0] if len(self.colors) > 0 else (255, 255, 255))
# Start with strip off
self.fill((0, 0, 0))
origins = list(range(0, self.num_leds, sep))
radius = 0
lit_total = 0
while True:
drew_any = False
for o in origins:
left = o - radius
right = o + radius
if 0 <= left < self.num_leds:
if self.n[left] == (0, 0, 0):
lit_total += 1
self.n[left] = color
drew_any = True
if 0 <= right < self.num_leds:
if self.n[right] == (0, 0, 0):
lit_total += 1
self.n[right] = color
drew_any = True
self.n.write()
# If we didn't draw anything new, we've reached beyond edges
if not drew_any:
break
# If all LEDs are now lit, immediately proceed to dark sweep
if lit_total >= self.num_leds:
break
# wait self.delay ms before next ring
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < self.delay:
pass
radius += 1
# Radiate back out (darkness outward): turn off from center to edges
last_radius = max(0, radius - 1)
for r in range(0, last_radius + 1):
for o in origins:
left = o - r
right = o + r
if 0 <= left < self.num_leds:
self.n[left] = (0, 0, 0)
if 0 <= right < self.num_leds:
self.n[right] = (0, 0, 0)
self.n.write()
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < self.delay:
pass
# ensure all LEDs are off at completion
self.fill((0, 0, 0))
# mark complete so scheduler won't auto-run again until re-selected
self.run = False
return self.delay return self.delay

View File

@@ -18,6 +18,10 @@ PATTERN_SUITE = [
{"pattern": "n_chase", "n1": 5, "n2": 5, "delay": 250, "iterations": 40, "repeat_delay": 120, "colors": ["#00ff88"]}, {"pattern": "n_chase", "n1": 5, "n2": 5, "delay": 250, "iterations": 40, "repeat_delay": 120, "colors": ["#00ff88"]},
{"pattern": "alternating", "n1": 6, "n2": 6, "delay": 300, "iterations": 20, "repeat_delay": 300, "colors": ["#ff8800"]}, {"pattern": "alternating", "n1": 6, "n2": 6, "delay": 300, "iterations": 20, "repeat_delay": 300, "colors": ["#ff8800"]},
{"pattern": "pulse", "delay": 200, "iterations": 6, "repeat_delay": 300, "colors": ["#ffffff"]}, {"pattern": "pulse", "delay": 200, "iterations": 6, "repeat_delay": 300, "colors": ["#ffffff"]},
# Specto sweep demo: increase n1 from 0 to 30 repeatedly
{"pattern": "specto", "delay": 80, "iterations": 32, "repeat_delay": 80, "colors": ["#00ff00"], "n1_sequence": list(range(0, 31)) + [30]},
# Radiate demo: origins every 8 LEDs, moderate speed
{"pattern": "radiate", "delay": 60, "iterations": 6, "repeat_delay": 600, "colors": ["#ffffff"], "n1": 8},
] ]
@@ -70,6 +74,9 @@ async def run_suite(uri: str):
interval_ms = int(cfg.get("interval_ms", cfg.get("delay", 100) or 100)) interval_ms = int(cfg.get("interval_ms", cfg.get("delay", 100) or 100))
repeat_ms = int(cfg.get("repeat_delay", interval_ms)) repeat_ms = int(cfg.get("repeat_delay", interval_ms))
for i in range(iterations): for i in range(iterations):
# Optional per-iteration n1 for specto
seq = cfg.get("n1_sequence")
n1_val = (seq[i % len(seq)] if seq else cfg.get("n1"))
msg = build_message( msg = build_message(
cfg.get("pattern", "off"), cfg.get("pattern", "off"),
i, i,
@@ -77,7 +84,7 @@ async def run_suite(uri: str):
colors=cfg.get("colors"), colors=cfg.get("colors"),
brightness=cfg.get("brightness", 127), brightness=cfg.get("brightness", 127),
num_leds=cfg.get("num_leds"), num_leds=cfg.get("num_leds"),
n1=cfg.get("n1"), n1=n1_val,
n2=cfg.get("n2"), n2=cfg.get("n2"),
name=cfg.get("name", "0"), name=cfg.get("name", "0"),
pattern_step=cfg.get("pattern_step"), pattern_step=cfg.get("pattern_step"),