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:
139
src/patterns.py
139
src/patterns.py
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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"),
|
||||||
|
Reference in New Issue
Block a user