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 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,6 +375,7 @@ 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__":
@@ -369,37 +386,24 @@ if __name__ == "__main__":
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 ---")

View File

@@ -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
if delay == 0:
self.patterns[self.selected]()
return interval
return None
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'.")