diff --git a/src/patterns.py b/src/patterns.py index 585366f..c48930c 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -1,178 +1,79 @@ -from machine import Pin -from neopixel import NeoPixel import utime import random +from patterns_base import PatternBase # Import PatternBase -class Patterns: +class Patterns(PatternBase): # Inherit from PatternBase def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="rainbow_cycle", delay=100): - self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) - self.num_leds = num_leds - self.pattern_step = 0 - self.last_update = utime.ticks_ms() - self.delay = delay - self.brightness = brightness + super().__init__(pin, num_leds, color1, color2, brightness, selected, delay) # Call parent constructor + + # Pattern-specific initializations + self.on_width = 1 # Default on width + self.off_width = 2 # Default off width (so total segment is 3, matching original behavior) + self.n1 = 0 # Default start of fill range + self.n2 = self.num_leds - 1 # Default end of fill range + self.oneshot = False # New: One-shot flag for patterns like fill_range self.patterns = { "off": self.off, "on" : self.on, - "color_wipe": self.color_wipe_step, - "rainbow_cycle": self.rainbow_cycle_step, - "theater_chase": self.theater_chase_step, - "blink": self.blink_step, - "color_transition": self.color_transition_step, # Added new pattern - "flicker": self.flicker_step, - "scanner": self.scanner_step, # New: Single direction scanner - "bidirectional_scanner": self.bidirectional_scanner_step, # New: Bidirectional scanner + "color_wipe": self.color_wipe, + "rainbow_cycle": self.rainbow_cycle, + "theater_chase": self.theater_chase, + "blink": self.blink, + "color_transition": self.color_transition, # Added new pattern + "flicker": self.flicker, + "scanner": self.scanner, # New: Single direction scanner + "bidirectional_scanner": self.bidirectional_scanner, # New: Bidirectional scanner + "fill_range": self.fill_range, # New: Fill from n1 to n2 + "n_chase": self.n_chase, # New: N1 on, N2 off repeating chase + "alternating": self.alternating, # New: N1 on/off, N2 off/on alternating chase "external": None, "pulse": self.pulse } - self.selected = selected + # Beat-related functionality removed + # self.selected is already initialized in PatternBase, but we need to ensure it uses our patterns dict + # self.selected = selected # Handled by PatternBase + # Ensure colors list always starts with at least two for robust transition handling - self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same - if not self.colors: # Ensure at least one color exists - self.colors = [(0, 0, 0)] + # self.colors handled by PatternBase - self.transition_duration = delay * 50 # Default transition duration - self.hold_duration = delay * 10 # Default hold duration at each color - self.transition_step = 0 # Current step in the transition - self.current_color_idx = 0 # Index of the color currently being held/transitioned from - self.current_color = self.colors[self.current_color_idx] # The actual blended color - - self.hold_start_time = utime.ticks_ms() # Time when the current color hold started - - # New attributes for scanner patterns - self.scanner_direction = 1 # 1 for forward, -1 for backward - self.scanner_tail_length = 3 # Number of trailing pixels + # Transition attributes handled by PatternBase + # Scanner attributes handled by PatternBase + # self.run handled by PatternBase + def sync(self): - self.pattern_step=0 - self.last_update = utime.ticks_ms() - self.delay - if self.selected == "color_transition": - self.transition_step = 0 - self.current_color_idx = 0 - self.current_color = self.colors[self.current_color_idx] - self.hold_start_time = utime.ticks_ms() # Reset hold time - # Reset scanner specific variables - self.scanner_direction = 1 + super().sync() # Call parent sync + # Reset pattern_step for theater_chase when chase_width changes + if self.selected == "theater_chase" or self.selected == "fill_range" or self.selected == "n_chase" or self.selected == "alternating": + self.pattern_step = 0 self.tick() - def set_pattern_step(self, step): - self.pattern_step = step + def set_on_width(self, on_width): + self.on_width = on_width - def tick(self): - if self.patterns[self.selected]: - self.patterns[self.selected]() + def set_off_width(self, off_width): + self.off_width = off_width + + def set_on_off_width(self, on_width, off_width): + self.on_width = on_width + self.off_width = off_width + self.sync() - def update_num_leds(self, pin, num_leds): - self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) - self.num_leds = num_leds - self.pattern_step = 0 + def set_fill_range(self, n1, n2): + self.n1 = n1 + self.n2 = n2 + self.sync() - def set_delay(self, delay): - self.delay = delay - # Update transition duration and hold duration when delay changes - self.transition_duration = self.delay * 50 - self.hold_duration = self.delay * 10 - - - def set_brightness(self, brightness): - self.brightness = brightness - - def set_color1(self, color): - if len(self.colors) > 0: - self.colors[0] = color - if self.selected == "color_transition": - # If the first color is changed, potentially reset transition - # to start from this new color if we were about to transition from it - if self.current_color_idx == 0: - self.transition_step = 0 - self.current_color = self.colors[0] - self.hold_start_time = utime.ticks_ms() - else: - self.colors.append(color) - - - def set_color2(self, color): - if len(self.colors) > 1: - self.colors[1] = color - elif len(self.colors) == 1: - self.colors.append(color) - else: # List is empty - self.colors.append((0,0,0)) # Dummy color - self.colors.append(color) - - - def set_colors(self, colors): - if colors and len(colors) >= 2: - self.colors = colors - if self.selected == "color_transition": - self.sync() # Reset transition if new color list is provided - elif colors and len(colors) == 1: - self.colors = [colors[0], (255,255,255)] # Add a default second color - if self.selected == "color_transition": - print("Warning: 'color_transition' requires at least two colors. Adding a default second color.") - self.sync() - else: - print("Error: set_colors requires a list of at least one color.") - self.colors = [(0,0,0), (255,255,255)] # Fallback - if self.selected == "color_transition": - self.sync() - - def set_color(self, num, color): - # Changed: More robust index check - if 0 <= num < len(self.colors): - self.colors[num] = color - # If the changed color is part of the current or next transition, - # restart the transition for smoother updates - if self.selected == "color_transition": - current_from_idx = self.current_color_idx - current_to_idx = (self.current_color_idx + 1) % len(self.colors) - if num == current_from_idx or num == current_to_idx: - # If we change a color involved in the current transition, - # it's best to restart the transition state for smoothness. - self.transition_step = 0 - self.current_color_idx = current_from_idx # Stay at the current starting color - self.current_color = self.colors[self.current_color_idx] - self.hold_start_time = utime.ticks_ms() # Reset hold - return True - elif num == len(self.colors): # Allow setting a new color at the end - self.colors.append(color) - return True - return False - - def add_color(self, color): - self.colors.append(color) - if self.selected == "color_transition" and len(self.colors) == 2: - # If we just added the second color needed for transition - self.sync() - - - def del_color(self, num): - # Changed: More robust index check and using del for lists - if 0 <= num < len(self.colors): - del self.colors[num] - # If the color being deleted was part of the current transition, - # re-evaluate the current_color_idx - if self.selected == "color_transition": - if len(self.colors) < 2: # Need at least two colors for transition - print("Warning: Not enough colors for 'color_transition'. Switching to 'on'.") - self.select("on") # Or some other default - else: - # Adjust index if it's out of bounds after deletion or was the one transitioning from - self.current_color_idx %= len(self.colors) - self.transition_step = 0 - self.current_color = self.colors[self.current_color_idx] - self.hold_start_time = utime.ticks_ms() - return True - return False - - def apply_brightness(self, color, brightness_override=None): - effective_brightness = brightness_override if brightness_override is not None else self.brightness - return tuple(int(c * effective_brightness / 255) for c in color) + def set_oneshot(self, oneshot_value): + self.oneshot = oneshot_value + if self.oneshot: # Reset pattern step if enabling one-shot + self.pattern_step = 0 + self.sync() def select(self, pattern): if pattern in self.patterns: - self.selected = pattern - self.sync() # Reset pattern state when selecting a new pattern + super().select(pattern) # Use parent select to set self.selected and self.transition_step + self.run = True # Set run flag if pattern == "color_transition": if len(self.colors) < 2: print("Warning: 'color_transition' requires at least two colors. Switching to 'on'.") @@ -187,82 +88,65 @@ class Patterns: self.hold_duration = self.delay * 10 # Initialize hold duration return True return False - - def set(self, i, color): - self.n[i] = color - - def write(self): - self.n.write() - - def fill(self, color=None): - fill_color = color if color is not None else self.colors[0] - for i in range(self.num_leds): - self.n[i] = fill_color - self.n.write() - + def off(self): self.fill((0, 0, 0)) def on(self): self.fill(self.apply_brightness(self.colors[0])) - def color_wipe_step(self): + def color_wipe(self): color = self.apply_brightness(self.colors[0]) current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - if self.pattern_step < self.num_leds: - for i in range(self.num_leds): - self.n[i] = (0, 0, 0) - self.n[self.pattern_step] = self.apply_brightness(color) - self.n.write() - self.pattern_step += 1 - else: - self.pattern_step = 0 - self.last_update = current_time - - def rainbow_cycle_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay/5: - 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) - + if self.pattern_step < self.num_leds: for i in range(self.num_leds): - rc_index = (i * 256 // self.num_leds) + self.pattern_step - self.n[i] = self.apply_brightness(wheel(rc_index & 255)) + self.n[i] = (0, 0, 0) + self.n[self.pattern_step] = self.apply_brightness(color) self.n.write() - self.pattern_step = (self.pattern_step + 1) % 256 - self.last_update = current_time + self.pattern_step += 1 + else: + self.pattern_step = 0 + self.last_update = current_time - def theater_chase_step(self): + def rainbow_cycle(self): current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - for i in range(self.num_leds): - if (i + self.pattern_step) % 3 == 0: - self.n[i] = self.apply_brightness(self.colors[0]) - else: - self.n[i] = (0, 0, 0) - self.n.write() - self.pattern_step = (self.pattern_step + 1) % 3 - self.last_update = current_time - - def blink_step(self): - current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - if self.pattern_step % 2 == 0: - self.fill(self.apply_brightness(self.colors[0])) + 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: - self.fill((0, 0, 0)) - self.pattern_step = (self.pattern_step + 1) % 2 - self.last_update = current_time + pos -= 170 + return (0, pos * 3, 255 - pos * 3) - def color_transition_step(self): + for i in range(self.num_leds): + rc_index = (i * 256 // 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 + self.last_update = current_time + + def theater_chase(self): + 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: + self.n[i] = self.apply_brightness(self.colors[0]) + else: + self.n[i] = (0, 0, 0) + self.n.write() + self.pattern_step = (self.pattern_step + 1) % segment_length + + def blink(self): + current_time = utime.ticks_ms() + if self.pattern_step % 2 == 0: + self.fill(self.apply_brightness(self.colors[0])) + else: + self.fill((0, 0, 0)) + self.pattern_step = (self.pattern_step + 1) % 2 + self.last_update = current_time + + def color_transition(self): current_time = utime.ticks_ms() # Check for hold duration first @@ -310,108 +194,241 @@ class Patterns: self.last_update = current_time - def flicker_step(self): + def flicker(self): current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay/5: - base_color = self.colors[0] - # Increase the range for flicker_brightness_offset - # Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity) - flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5)) - flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset)) + base_color = self.colors[0] + # Increase the range for flicker_brightness_offset + # Changed from self.brightness // 4 to self.brightness // 2 (or even self.brightness for max intensity) + flicker_brightness_offset = random.randint(-int(self.brightness // 1.5), int(self.brightness // 1.5)) + flicker_brightness = max(0, min(255, self.brightness + flicker_brightness_offset)) - flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness) - self.fill(flicker_color) - self.last_update = current_time + flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness) + self.fill(flicker_color) + self.last_update = current_time - def scanner_step(self): + def scanner(self): """ Mimics a 'Knight Rider' style scanner, moving in one direction. """ current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: - self.fill((0, 0, 0)) # Clear all LEDs + self.fill((0, 0, 0)) # Clear all LEDs - # Calculate the head and tail position - head_pos = self.pattern_step - color = self.apply_brightness(self.colors[0]) + # Calculate the head and tail position + head_pos = self.pattern_step + color = self.apply_brightness(self.colors[0]) - # Draw the head - if 0 <= head_pos < self.num_leds: - self.n[head_pos] = color + # Draw the head + if 0 <= head_pos < self.num_leds: + self.n[head_pos] = color - # Draw the trailing pixels with decreasing brightness - for i in range(1, self.scanner_tail_length + 1): - tail_pos = head_pos - i - if 0 <= tail_pos < self.num_leds: - # Calculate fading color for tail - # Example: linear fade from full brightness to off - fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) - faded_color = tuple(int(c * fade_factor) for c in color) - self.n[tail_pos] = faded_color + # Draw the trailing pixels with decreasing brightness + for i in range(1, self.scanner_tail_length + 1): + tail_pos = head_pos - i + if 0 <= tail_pos < self.num_leds: + # Calculate fading color for tail + # Example: linear fade from full brightness to off + fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) + faded_color = tuple(int(c * fade_factor) for c in color) + self.n[tail_pos] = faded_color - self.n.write() + self.n.write() - self.pattern_step += 1 - if self.pattern_step >= self.num_leds + self.scanner_tail_length: - self.pattern_step = 0 # Reset to start + self.pattern_step += 1 + if self.pattern_step >= self.num_leds + self.scanner_tail_length: + self.pattern_step = 0 # Reset to start - self.last_update = current_time + self.last_update = current_time - def bidirectional_scanner_step(self): + def bidirectional_scanner(self): """ Mimics a 'Knight Rider' style scanner, moving back and forth. """ current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay/100: - self.fill((0, 0, 0)) # Clear all LEDs + self.fill((0, 0, 0)) # Clear all LEDs + color = self.apply_brightness(self.colors[0]) + + # Calculate the head position based on direction + head_pos = self.pattern_step + + # Draw the head + if 0 <= head_pos < self.num_leds: + self.n[head_pos] = color + + # Draw the trailing pixels with decreasing brightness + for i in range(1, self.scanner_tail_length + 1): + tail_pos = head_pos - (i * self.scanner_direction) + if 0 <= tail_pos < self.num_leds: + fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) + faded_color = tuple(int(c * fade_factor) for c in color) + self.n[tail_pos] = faded_color + + self.n.write() + + self.pattern_step += self.scanner_direction + + # Change direction if boundaries are reached + if self.scanner_direction == 1 and self.pattern_step >= self.num_leds: + self.scanner_direction = -1 + self.pattern_step = self.num_leds - 1 # Start moving back from the last LED + elif self.scanner_direction == -1 and self.pattern_step < 0: + self.scanner_direction = 1 + self.pattern_step = 0 # Start moving forward from the first LED + + self.last_update = current_time + + def fill_range(self): + """ + Fills a range of LEDs from n1 to n2 with a solid color. + If self.oneshot is True, it fills once and then turns off the LEDs. + """ + current_time = utime.ticks_ms() + if self.oneshot and self.pattern_step >= 1: + self.fill((0, 0, 0)) # Turn off LEDs if one-shot already happened + else: color = self.apply_brightness(self.colors[0]) - - # Calculate the head position based on direction - head_pos = self.pattern_step - - # Draw the head - if 0 <= head_pos < self.num_leds: - self.n[head_pos] = color - - # Draw the trailing pixels with decreasing brightness - for i in range(1, self.scanner_tail_length + 1): - tail_pos = head_pos - (i * self.scanner_direction) - if 0 <= tail_pos < self.num_leds: - fade_factor = 1.0 - (i / (self.scanner_tail_length + 1)) - faded_color = tuple(int(c * fade_factor) for c in color) - self.n[tail_pos] = faded_color - + 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 - self.pattern_step += self.scanner_direction - - # Change direction if boundaries are reached - if self.scanner_direction == 1 and self.pattern_step >= self.num_leds: - self.scanner_direction = -1 - self.pattern_step = self.num_leds - 1 # Start moving back from the last LED - elif self.scanner_direction == -1 and self.pattern_step < 0: - self.scanner_direction = 1 - self.pattern_step = 0 # Start moving forward from the first LED - + def n_chase(self): + """ + A theater chase pattern using n1 for on-width and n2 for off-width. + """ + current_time = utime.ticks_ms() + segment_length = self.n1 + self.n2 + if segment_length == 0: # Avoid division by zero + self.fill((0,0,0)) + self.n.write() self.last_update = current_time + return + + for i in range(self.num_leds): + if (i + self.pattern_step) % segment_length < self.n1: + self.n[i] = self.apply_brightness(self.colors[0]) + else: + self.n[i] = (0, 0, 0) + self.n.write() + self.pattern_step = (self.pattern_step + 1) % segment_length + self.last_update = current_time + + def alternating(self): + """ + An alternating pattern where n1 LEDs are ON/OFF and n2 LEDs are OFF/ON globally, without moving. + """ + current_time = utime.ticks_ms() + total_segment_length = self.n1 + self.n2 + if total_segment_length == 0: + self.fill((0,0,0)) + self.n.write() + self.last_update = current_time + return + + # current_phase will alternate between 0 and 1 + current_phase = self.pattern_step % 2 + + for i in range(self.num_leds): + # Position within a single repeating segment (n1 + n2) + pos_in_segment = i % total_segment_length + + if current_phase == 0: # State 0: n1 ON, n2 OFF + if pos_in_segment < self.n1: + self.n[i] = self.apply_brightness(self.colors[0]) # n1 is ON + else: + self.n[i] = (0, 0, 0) # n2 is OFF + else: # State 1: n1 OFF, n2 ON + if pos_in_segment < self.n1: + self.n[i] = (0, 0, 0) # n1 is OFF + else: + self.n[i] = self.apply_brightness(self.colors[0]) # n2 is ON + + self.n.write() + self.pattern_step = (self.pattern_step + 1) % 2 # Toggle between 0 and 1 + self.last_update = current_time def pulse(self): if self.pattern_step == 0: self.fill(self.apply_brightness(self.colors[0])) self.pattern_step = 1 + self.last_update = utime.ticks_ms() if utime.ticks_diff(utime.ticks_ms(), self.last_update) > self.delay: self.fill((0, 0, 0)) + print(utime.ticks_diff(utime.ticks_ms(), self.last_update)) + self.run = False if __name__ == "__main__": import time from machine import WDT wdt = WDT(timeout=2000) # Enable watchdog with a 2 second timeout - p = Patterns(pin=10, num_leds=200, color1=(255,0,0), color2=(0,0,255), brightness=127, selected="bidirectional_scanner", delay=50) - p.select("pulse") - for i in range(1000): - p.tick() - wdt.feed() - time.sleep_ms(1) \ No newline at end of file + 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}), + ("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}), + ] + + print("\n--- Running pattern self-test ---") + for name, cfg in tests: + print(f"\nPattern: {name}") + # apply simple config helpers + if "delay" in cfg: + p.set_delay(cfg["delay"]) + if "on_width" in cfg: + p.set_on_width(cfg["on_width"]) + if "off_width" in cfg: + p.set_off_width(cfg["off_width"]) + if "n1" in cfg and "n2" in cfg: + p.set_fill_range(cfg["n1"], cfg["n2"]) + if "colors" in cfg: + p.set_colors(cfg["colors"]) + + p.select(name) + + # run per configured or computed duration + start = utime.ticks_ms() + duration_ms = cfg["duration_ms"] + while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms: + interval = p.tick() + 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 b9ef410..59cc268 100644 --- a/src/patterns_base.py +++ b/src/patterns_base.py @@ -13,6 +13,7 @@ class PatternBase: self.brightness = brightness self.patterns = {} self.selected = selected + self.run = True # Ensure colors list always starts with at least two for robust transition handling self.colors = [color1, color2] if color1 != color2 else [color1, (255, 255, 255)] # Fallback if initial colors are same if not self.colors: # Ensure at least one color exists @@ -26,10 +27,12 @@ class PatternBase: self.hold_start_time = utime.ticks_ms() # Time when the current color hold started - # New attributes for scanner patterns + # New attributes for scanner patterns (moved from Patterns to PatternBase as they are generic enough) self.scanner_direction = 1 # 1 for forward, -1 for backward self.scanner_tail_length = 3 # Number of trailing pixels + # Removed: selected_delay caching + def sync(self): self.pattern_step=0 self.last_update = utime.ticks_ms() - self.delay @@ -40,14 +43,32 @@ class PatternBase: self.hold_start_time = utime.ticks_ms() # Reset hold time # Reset scanner specific variables self.scanner_direction = 1 - self.tick() + # self.tick() # Tick moved to Patterns, as patterns dict is there def set_pattern_step(self, step): self.pattern_step = step def tick(self): - if self.patterns[self.selected]: + 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 def update_num_leds(self, pin, num_leds): self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) @@ -59,6 +80,7 @@ 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 def set_brightness(self, brightness): @@ -157,6 +179,7 @@ class PatternBase: return tuple(int(c * effective_brightness / 255) for c in color) def select(self, pattern): + # Removed self.run = True here. It should be handled by Patterns class. if pattern in self.patterns: self.selected = pattern self.sync() # Reset pattern state when selecting a new pattern