From 93560a253e51b3bbcf381dbbb7795682e1cef26e Mon Sep 17 00:00:00 2001 From: jimmy Date: Mon, 15 Sep 2025 14:12:43 +1200 Subject: [PATCH] patterns: fix blink timing; slow alternating; unify self-test with absolute tick scheduling --- src/patterns.py | 69 ++++++++++++++++++++++---------------------- src/patterns_base.py | 40 ++++++++++++------------- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/src/patterns.py b/src/patterns.py index c48930c..59a1a2a 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -1,3 +1,4 @@ + import utime import random from patterns_base import PatternBase # Import PatternBase @@ -91,9 +92,11 @@ class Patterns(PatternBase): # Inherit from PatternBase def off(self): self.fill((0, 0, 0)) + return self.delay def on(self): self.fill(self.apply_brightness(self.colors[0])) + return self.delay def color_wipe(self): color = self.apply_brightness(self.colors[0]) @@ -107,6 +110,7 @@ class Patterns(PatternBase): # Inherit from PatternBase else: self.pattern_step = 0 self.last_update = current_time + return self.delay def rainbow_cycle(self): current_time = utime.ticks_ms() @@ -126,8 +130,10 @@ class Patterns(PatternBase): # Inherit from PatternBase self.n.write() self.pattern_step = (self.pattern_step + 1) % 256 self.last_update = current_time + return max(1, int(self.delay // 5)) def theater_chase(self): + current_time = utime.ticks_ms() segment_length = self.on_width + self.off_width for i in range(self.num_leds): if (i + self.pattern_step) % segment_length < self.on_width: @@ -136,6 +142,8 @@ class Patterns(PatternBase): # Inherit from PatternBase self.n[i] = (0, 0, 0) self.n.write() self.pattern_step = (self.pattern_step + 1) % segment_length + self.last_update = current_time + return self.delay def blink(self): current_time = utime.ticks_ms() @@ -145,6 +153,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.fill((0, 0, 0)) self.pattern_step = (self.pattern_step + 1) % 2 self.last_update = current_time + return self.delay def color_transition(self): current_time = utime.ticks_ms() @@ -154,7 +163,7 @@ class Patterns(PatternBase): # Inherit from PatternBase # Still in hold phase, just display the current solid color self.fill(self.apply_brightness(self.current_color)) self.last_update = current_time # Keep updating last_update to avoid skipping frames - return + return self.delay # If hold duration is over, proceed with transition if utime.ticks_diff(current_time, self.last_update) >= self.delay: @@ -162,7 +171,7 @@ class Patterns(PatternBase): # Inherit from PatternBase if num_colors < 2: # Should not happen if select handles it, but as a safeguard self.select("on") - return + return self.delay from_color = self.colors[self.current_color_idx] to_color_idx = (self.current_color_idx + 1) % num_colors @@ -193,6 +202,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.hold_start_time = current_time # Start hold phase for the new color self.last_update = current_time + return self.delay def flicker(self): current_time = utime.ticks_ms() @@ -205,6 +215,7 @@ class Patterns(PatternBase): # Inherit from PatternBase flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness) self.fill(flicker_color) self.last_update = current_time + return max(1, int(self.delay // 5)) def scanner(self): """ @@ -238,6 +249,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.pattern_step = 0 # Reset to start self.last_update = current_time + return self.delay def bidirectional_scanner(self): """ @@ -276,6 +288,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.pattern_step = 0 # Start moving forward from the first LED self.last_update = current_time + return self.delay def fill_range(self): """ @@ -290,9 +303,10 @@ class Patterns(PatternBase): # Inherit from PatternBase for i in range(self.n1, self.n2 + 1): self.n[i] = color self.n.write() - if self.oneshot: - self.pattern_step += 1 # Increment only for one-shot + self.last_update = current_time + return self.delay self.last_update = current_time + return self.delay def n_chase(self): """ @@ -304,7 +318,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.fill((0,0,0)) self.n.write() self.last_update = current_time - return + return self.delay for i in range(self.num_leds): if (i + self.pattern_step) % segment_length < self.n1: @@ -314,6 +328,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.n.write() self.pattern_step = (self.pattern_step + 1) % segment_length self.last_update = current_time + return self.delay def alternating(self): """ @@ -325,7 +340,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.fill((0,0,0)) self.n.write() self.last_update = current_time - return + return self.delay # current_phase will alternate between 0 and 1 current_phase = self.pattern_step % 2 @@ -348,6 +363,7 @@ class Patterns(PatternBase): # Inherit from PatternBase self.n.write() self.pattern_step = (self.pattern_step + 1) % 2 # Toggle between 0 and 1 self.last_update = current_time + return self.delay * 2 def pulse(self): if self.pattern_step == 0: @@ -359,47 +375,35 @@ class Patterns(PatternBase): # Inherit from PatternBase self.fill((0, 0, 0)) print(utime.ticks_diff(utime.ticks_ms(), self.last_update)) self.run = False + return self.delay if __name__ == "__main__": - import time + import time from machine import WDT wdt = WDT(timeout=2000) # Enable watchdog with a 2 second timeout p = Patterns(pin=4, num_leds=60, color1=(255,0,0), color2=(0,0,255), brightness=127, selected="off", delay=100) print(p.colors, p.brightness) - # tests = [ - # ("off", {"duration_ms": 500}), - # ("on", {"duration_ms": 500}), - # ("color_wipe", {"delay": 200, "duration_ms": 1000}), - # ("rainbow_cycle", {"delay": 100, "duration_ms": 2500}), - # ("theater_chase", {"on_width": 3, "off_width": 3, "delay": 1000, "duration_ms": 2500}), - # ("blink", {"delay": 500, "duration_ms": 2000}), - # ("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}), - # ("flicker", {"delay": 100, "duration_ms": 2000}), - # ("scanner", {"delay": 150, "duration_ms": 2500}), - # ("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}), - # ("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}), - # ("n_chase", {"n1": 5, "n2": 5, "delay": 1000, "duration_ms": 2500}), - # ("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}), - # ("pulse", {"delay": 100, "duration_ms": 700}), - # ] - tests = [ - - ("theater_chase", {"on_width": 3, "off_width": 3, "delay": 10000, "duration_ms": 2500}), + ("off", {"duration_ms": 500}), + ("on", {"duration_ms": 500}), + ("color_wipe", {"delay": 200, "duration_ms": 1000}), + ("rainbow_cycle", {"delay": 100, "duration_ms": 2500}), + ("theater_chase", {"on_width": 3, "off_width": 3, "delay": 1000, "duration_ms": 2500}), ("blink", {"delay": 500, "duration_ms": 2000}), ("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}), ("flicker", {"delay": 100, "duration_ms": 2000}), ("scanner", {"delay": 150, "duration_ms": 2500}), ("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}), ("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}), - ("n_chase", {"n1": 5, "n2": 5, "delay": 1000, "duration_ms": 2500}), + ("n_chase", {"n1": 5, "n2": 5, "delay": 2000, "duration_ms": 2500}), ("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}), ("pulse", {"delay": 100, "duration_ms": 700}), ] + print("\n--- Running pattern self-test ---") for name, cfg in tests: print(f"\nPattern: {name}") @@ -417,17 +421,14 @@ if __name__ == "__main__": p.select(name) - # run per configured or computed duration + # run per configured duration using absolute-scheduled tick(next_due_ms) start = utime.ticks_ms() duration_ms = cfg["duration_ms"] + delay = cfg.get("delay", 0) + next_due = utime.ticks_ms() - 1 # force immediate first call while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms: - interval = p.tick() + delay = p.tick(delay) wdt.feed() - if isinstance(interval, int) and interval > 0: - # sleep a small fraction to reduce busy loop while keeping responsiveness - time.sleep_ms(max(1, interval // 10)) - else: - time.sleep_ms(5) print("\n--- Test routine finished ---") diff --git a/src/patterns_base.py b/src/patterns_base.py index 59cc268..5c025b9 100644 --- a/src/patterns_base.py +++ b/src/patterns_base.py @@ -31,7 +31,8 @@ class PatternBase: self.scanner_direction = 1 # 1 for forward, -1 for backward self.scanner_tail_length = 3 # Number of trailing pixels - # Removed: selected_delay caching + # Store last pattern-returned delay to use for subsequent gating + self._last_returned_delay = None def sync(self): self.pattern_step=0 @@ -48,27 +49,19 @@ class PatternBase: def set_pattern_step(self, step): self.pattern_step = step - def tick(self): + def tick(self, delay=0): + now =utime.ticks_ms() if self.patterns.get(self.selected) and self.run: - # Compute gating interval per pattern based on current delay - interval = None - if self.selected in ("color_wipe", "theater_chase", "blink", "scanner", "fill_range", "n_chase", "alternating"): - interval = self.delay - elif self.selected == "rainbow_cycle": - interval = max(1, int(self.delay // 5)) - elif self.selected == "flicker": - interval = max(1, int(self.delay // 5)) - elif self.selected == "bidirectional_scanner": - interval = max(1, int(self.delay // 100)) - # Patterns intentionally not gated here: off, on, external, pulse, color_transition - - if interval is not None: - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) < interval: - return interval - self.patterns[self.selected]() - return interval - return None + if delay == 0: + self.patterns[self.selected]() + print("manual tick") + return 0 + if utime.ticks_diff(now, delay) > 0: + delay = self.patterns[self.selected]() + print("auto tick") + return delay + now + else: + return delay def update_num_leds(self, pin, num_leds): self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) @@ -80,7 +73,8 @@ class PatternBase: # Update transition duration and hold duration when delay changes self.transition_duration = self.delay * 50 self.hold_duration = self.delay * 10 - # No cached interval + # Reset last returned delay so next tick recomputes + self._last_returned_delay = None def set_brightness(self, brightness): @@ -183,6 +177,8 @@ class PatternBase: if pattern in self.patterns: self.selected = pattern self.sync() # Reset pattern state when selecting a new pattern + # Reset last returned delay so gating can be recalculated for the new pattern + self._last_returned_delay = None if pattern == "color_transition": if len(self.colors) < 2: print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.")