patterns: fix blink timing; slow alternating; unify self-test with absolute tick scheduling

This commit is contained in:
2025-09-15 14:12:43 +12:00
parent d68817ea18
commit 93560a253e
2 changed files with 53 additions and 56 deletions

View File

@@ -1,3 +1,4 @@
import utime import utime
import random import random
from patterns_base import PatternBase # Import PatternBase from patterns_base import PatternBase # Import PatternBase
@@ -91,9 +92,11 @@ class Patterns(PatternBase): # Inherit from PatternBase
def off(self): def off(self):
self.fill((0, 0, 0)) self.fill((0, 0, 0))
return self.delay
def on(self): def on(self):
self.fill(self.apply_brightness(self.colors[0])) self.fill(self.apply_brightness(self.colors[0]))
return self.delay
def color_wipe(self): def color_wipe(self):
color = self.apply_brightness(self.colors[0]) color = self.apply_brightness(self.colors[0])
@@ -107,6 +110,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
else: else:
self.pattern_step = 0 self.pattern_step = 0
self.last_update = current_time self.last_update = current_time
return self.delay
def rainbow_cycle(self): def rainbow_cycle(self):
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
@@ -126,8 +130,10 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % 256 self.pattern_step = (self.pattern_step + 1) % 256
self.last_update = current_time self.last_update = current_time
return max(1, int(self.delay // 5))
def theater_chase(self): def theater_chase(self):
current_time = utime.ticks_ms()
segment_length = self.on_width + self.off_width segment_length = self.on_width + self.off_width
for i in range(self.num_leds): for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.on_width: 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[i] = (0, 0, 0)
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length self.pattern_step = (self.pattern_step + 1) % segment_length
self.last_update = current_time
return self.delay
def blink(self): def blink(self):
current_time = utime.ticks_ms() current_time = utime.ticks_ms()
@@ -145,6 +153,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.fill((0, 0, 0)) self.fill((0, 0, 0))
self.pattern_step = (self.pattern_step + 1) % 2 self.pattern_step = (self.pattern_step + 1) % 2
self.last_update = current_time self.last_update = current_time
return self.delay
def color_transition(self): def color_transition(self):
current_time = utime.ticks_ms() 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 # Still in hold phase, just display the current solid color
self.fill(self.apply_brightness(self.current_color)) self.fill(self.apply_brightness(self.current_color))
self.last_update = current_time # Keep updating last_update to avoid skipping frames 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 hold duration is over, proceed with transition
if utime.ticks_diff(current_time, self.last_update) >= self.delay: 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: if num_colors < 2:
# Should not happen if select handles it, but as a safeguard # Should not happen if select handles it, but as a safeguard
self.select("on") self.select("on")
return return self.delay
from_color = self.colors[self.current_color_idx] from_color = self.colors[self.current_color_idx]
to_color_idx = (self.current_color_idx + 1) % num_colors 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.hold_start_time = current_time # Start hold phase for the new color
self.last_update = current_time self.last_update = current_time
return self.delay
def flicker(self): def flicker(self):
current_time = utime.ticks_ms() 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) flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness)
self.fill(flicker_color) self.fill(flicker_color)
self.last_update = current_time self.last_update = current_time
return max(1, int(self.delay // 5))
def scanner(self): def scanner(self):
""" """
@@ -238,6 +249,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.pattern_step = 0 # Reset to start self.pattern_step = 0 # Reset to start
self.last_update = current_time self.last_update = current_time
return self.delay
def bidirectional_scanner(self): 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.pattern_step = 0 # Start moving forward from the first LED
self.last_update = current_time self.last_update = current_time
return self.delay
def fill_range(self): def fill_range(self):
""" """
@@ -290,9 +303,10 @@ class Patterns(PatternBase): # Inherit from PatternBase
for i in range(self.n1, self.n2 + 1): for i in range(self.n1, self.n2 + 1):
self.n[i] = color self.n[i] = color
self.n.write() self.n.write()
if self.oneshot: self.last_update = current_time
self.pattern_step += 1 # Increment only for one-shot return self.delay
self.last_update = current_time self.last_update = current_time
return self.delay
def n_chase(self): def n_chase(self):
""" """
@@ -304,7 +318,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.fill((0,0,0)) self.fill((0,0,0))
self.n.write() self.n.write()
self.last_update = current_time self.last_update = current_time
return return self.delay
for i in range(self.num_leds): for i in range(self.num_leds):
if (i + self.pattern_step) % segment_length < self.n1: if (i + self.pattern_step) % segment_length < self.n1:
@@ -314,6 +328,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % segment_length self.pattern_step = (self.pattern_step + 1) % segment_length
self.last_update = current_time self.last_update = current_time
return self.delay
def alternating(self): def alternating(self):
""" """
@@ -325,7 +340,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.fill((0,0,0)) self.fill((0,0,0))
self.n.write() self.n.write()
self.last_update = current_time self.last_update = current_time
return return self.delay
# current_phase will alternate between 0 and 1 # current_phase will alternate between 0 and 1
current_phase = self.pattern_step % 2 current_phase = self.pattern_step % 2
@@ -348,6 +363,7 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.n.write() self.n.write()
self.pattern_step = (self.pattern_step + 1) % 2 # Toggle between 0 and 1 self.pattern_step = (self.pattern_step + 1) % 2 # Toggle between 0 and 1
self.last_update = current_time self.last_update = current_time
return self.delay * 2
def pulse(self): def pulse(self):
if self.pattern_step == 0: if self.pattern_step == 0:
@@ -359,47 +375,35 @@ class Patterns(PatternBase): # Inherit from PatternBase
self.fill((0, 0, 0)) self.fill((0, 0, 0))
print(utime.ticks_diff(utime.ticks_ms(), self.last_update)) print(utime.ticks_diff(utime.ticks_ms(), self.last_update))
self.run = False self.run = False
return self.delay
if __name__ == "__main__": if __name__ == "__main__":
import time import time
from machine import WDT from machine import WDT
wdt = WDT(timeout=2000) # Enable watchdog with a 2 second timeout 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) 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) 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 = [ tests = [
("off", {"duration_ms": 500}),
("theater_chase", {"on_width": 3, "off_width": 3, "delay": 10000, "duration_ms": 2500}), ("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}), ("blink", {"delay": 500, "duration_ms": 2000}),
("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}), ("color_transition", {"delay": 150, "colors": [(255,0,0),(0,255,0),(0,0,255)], "duration_ms": 5000}),
("flicker", {"delay": 100, "duration_ms": 2000}), ("flicker", {"delay": 100, "duration_ms": 2000}),
("scanner", {"delay": 150, "duration_ms": 2500}), ("scanner", {"delay": 150, "duration_ms": 2500}),
("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}), ("bidirectional_scanner", {"delay": 50, "duration_ms": 2500}),
("fill_range", {"n1": 10, "n2": 20, "delay": 500, "duration_ms": 2000}), ("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}), ("alternating", {"n1": 5, "n2": 5, "delay": 500, "duration_ms": 2500}),
("pulse", {"delay": 100, "duration_ms": 700}), ("pulse", {"delay": 100, "duration_ms": 700}),
] ]
print("\n--- Running pattern self-test ---") print("\n--- Running pattern self-test ---")
for name, cfg in tests: for name, cfg in tests:
print(f"\nPattern: {name}") print(f"\nPattern: {name}")
@@ -417,17 +421,14 @@ if __name__ == "__main__":
p.select(name) p.select(name)
# run per configured or computed duration # run per configured duration using absolute-scheduled tick(next_due_ms)
start = utime.ticks_ms() start = utime.ticks_ms()
duration_ms = cfg["duration_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: while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
interval = p.tick() delay = p.tick(delay)
wdt.feed() 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 ---") print("\n--- Test routine finished ---")

View File

@@ -31,7 +31,8 @@ class PatternBase:
self.scanner_direction = 1 # 1 for forward, -1 for backward self.scanner_direction = 1 # 1 for forward, -1 for backward
self.scanner_tail_length = 3 # Number of trailing pixels 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): def sync(self):
self.pattern_step=0 self.pattern_step=0
@@ -48,27 +49,19 @@ class PatternBase:
def set_pattern_step(self, step): def set_pattern_step(self, step):
self.pattern_step = 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: if self.patterns.get(self.selected) and self.run:
# Compute gating interval per pattern based on current delay if delay == 0:
interval = None self.patterns[self.selected]()
if self.selected in ("color_wipe", "theater_chase", "blink", "scanner", "fill_range", "n_chase", "alternating"): print("manual tick")
interval = self.delay return 0
elif self.selected == "rainbow_cycle": if utime.ticks_diff(now, delay) > 0:
interval = max(1, int(self.delay // 5)) delay = self.patterns[self.selected]()
elif self.selected == "flicker": print("auto tick")
interval = max(1, int(self.delay // 5)) return delay + now
elif self.selected == "bidirectional_scanner": else:
interval = max(1, int(self.delay // 100)) return delay
# 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
def update_num_leds(self, pin, num_leds): def update_num_leds(self, pin, num_leds):
self.n = NeoPixel(Pin(pin, Pin.OUT), 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 # Update transition duration and hold duration when delay changes
self.transition_duration = self.delay * 50 self.transition_duration = self.delay * 50
self.hold_duration = self.delay * 10 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): def set_brightness(self, brightness):
@@ -183,6 +177,8 @@ class PatternBase:
if pattern in self.patterns: if pattern in self.patterns:
self.selected = pattern self.selected = pattern
self.sync() # Reset pattern state when selecting a new 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 pattern == "color_transition":
if len(self.colors) < 2: if len(self.colors) < 2:
print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.") print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.")