patterns: fix blink timing; slow alternating; unify self-test with absolute tick scheduling
This commit is contained in:
@@ -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 ---")
|
||||
|
||||
|
@@ -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'.")
|
||||
|
Reference in New Issue
Block a user