diff --git a/src/patterns.py b/src/patterns.py index 080a6eb..7a0542f 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -18,7 +18,10 @@ class Patterns(PatternBase): # Inherit from PatternBase "fill_range": self.fill_range, "n_chase": self.n_chase, "alternating": self.alternating, - "pulse": self.pulse + "pulse": self.pulse, + "rainbow": self.rainbow, + "specto": self.specto, + "radiate": self.radiate, } self.step = 0 @@ -110,12 +113,138 @@ class Patterns(PatternBase): # Inherit from PatternBase def pulse(self): - self.fill(self.apply_brightness(self.colors[0])) - start = utime.ticks_ms() - - while utime.ticks_diff(utime.ticks_ms(), start) < self.delay: - pass + # Envelope: attack=n1 ms, hold=delay ms, decay=n2 ms + attack_ms = max(0, int(self.n1)) + hold_ms = max(0, int(self.delay)) + decay_ms = max(0, int(self.n2)) + + base = self.colors[0] if len(self.colors) > 0 else (255, 255, 255) + 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.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 diff --git a/test/main.py b/test/main.py index a019b46..efc95b8 100644 --- a/test/main.py +++ b/test/main.py @@ -18,6 +18,10 @@ PATTERN_SUITE = [ {"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": "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)) repeat_ms = int(cfg.get("repeat_delay", interval_ms)) 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( cfg.get("pattern", "off"), i, @@ -77,7 +84,7 @@ async def run_suite(uri: str): colors=cfg.get("colors"), brightness=cfg.get("brightness", 127), num_leds=cfg.get("num_leds"), - n1=cfg.get("n1"), + n1=n1_val, n2=cfg.get("n2"), name=cfg.get("name", "0"), pattern_step=cfg.get("pattern_step"),