From 87fc74bb510ef809521ea4fc6850509856167b67 Mon Sep 17 00:00:00 2001 From: jimmy Date: Sat, 12 Jul 2025 10:21:43 +1200 Subject: [PATCH] Add flicker pattern --- src/patterns.py | 201 ++++++++++++++++++++++++++++++------------------ 1 file changed, 126 insertions(+), 75 deletions(-) diff --git a/src/patterns.py b/src/patterns.py index 8667137..c42b56d 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -22,21 +22,33 @@ class Patterns: "random_rainbow_cycle": self.random_rainbow_cycle_step, "random_theater_chase": self.random_theater_chase_step, "random_blink": self.random_blink_step, - "color_transition": self.color_transition_step, + "color_transition": self.color_transition_step, # Added new pattern + "flicker": self.flicker_step, "external": None } self.selected = selected - self.colors = [color1, color2] - self.transition_duration = delay * 10 # Default transition duration is 10 times the delay - self.transition_step = 0 - self.current_color = self.colors[0] + # 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.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: Track the current index for color transitions - self.current_color_idx = 0 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 self.tick() def set_pattern_step(self, step): @@ -53,31 +65,53 @@ class Patterns: def set_delay(self, delay): self.delay = delay - # Update transition duration when delay changes for color_transition pattern - if self.selected == "color_transition": - self.transition_duration = self.delay * 10 # Or some other multiplier + # 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): - self.colors[0] = color - if self.selected == "color_transition": - # Restart transition if color 0 (start color) is changed - self.transition_step = 0 - self.current_color_idx = 0 # Ensure we start from the new color[0] - self.current_color = self.colors[0] + 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): - self.colors[1] = color - if self.selected == "color_transition": - # No direct effect on current_color here, but transition will eventually use it - pass + 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): - self.colors = 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 @@ -89,11 +123,12 @@ class Patterns: 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 - # Optionally reset current_color_idx if num is the start color - if num == current_from_idx: - self.current_color_idx = num - self.current_color = self.colors[num] + 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) @@ -102,6 +137,10 @@ class Patterns: 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 @@ -111,16 +150,20 @@ class Patterns: # re-evaluate the current_color_idx if self.selected == "color_transition": if len(self.colors) < 2: # Need at least two colors for transition - self.select("off") # Or some other default + print("Warning: Not enough colors for 'color_transition'. Switching to 'on'.") + self.select("on") # Or some other default else: - self.current_color_idx %= len(self.colors) # Adjust index if it's out of bounds + # 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): - return tuple(int(c * self.brightness / 255) for c in color) + 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 select(self, pattern): if pattern in self.patterns: @@ -135,7 +178,9 @@ class Patterns: self.transition_step = 0 self.current_color_idx = 0 # Start from the first color in the list self.current_color = self.colors[self.current_color_idx] - self.transition_duration = self.delay * 10 # Initialize transition duration + self.hold_start_time = utime.ticks_ms() # Reset hold timer + self.transition_duration = self.delay * 50 # Initialize transition duration + self.hold_duration = self.delay * 10 # Initialize hold duration return True return False @@ -157,7 +202,6 @@ class Patterns: def on(self): self.fill(self.apply_brightness(self.colors[0])) - def color_wipe_step(self): color = self.apply_brightness(self.colors[0]) current_time = utime.ticks_ms() @@ -230,7 +274,7 @@ class Patterns: def random_rainbow_cycle_step(self): current_time = utime.ticks_ms() - if utime.ticks_diff(current_time, self.last_update) >= self.delay: + if utime.ticks_diff(current_time, self.last_update) >= self.delay: # Kept original delay for now def wheel(pos): if pos < 85: return (pos * 3, 255 - pos * 3, 0) @@ -273,56 +317,63 @@ class Patterns: self.pattern_step = (self.pattern_step + 1) % 2 self.last_update = current_time - def interpolate_color(self, color_a, color_b, factor): - """Interpolates between two colors.""" - return tuple(int(a + (b - a) * factor) for a, b in zip(color_a, color_b)) - def color_transition_step(self): current_time = utime.ticks_ms() - if len(self.colors) < 2: - # Not enough colors to transition, possibly switch to 'on' or 'off' - self.fill(self.apply_brightness(self.colors[0])) - return # Exit if there aren't enough colors + # Check for hold duration first + if utime.ticks_diff(current_time, self.hold_start_time) < self.hold_duration: + # 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 - if utime.ticks_diff(current_time, self.last_update) >= 1: # Update frequently for smooth transition - self.transition_step += utime.ticks_diff(current_time, self.last_update) - self.last_update = current_time + # If hold duration is over, proceed with transition + if utime.ticks_diff(current_time, self.last_update) >= self.delay: + num_colors = len(self.colors) + if num_colors < 2: + # Should not happen if select handles it, but as a safeguard + self.select("on") + return - color_from = self.colors[self.current_color_idx] - color_to_idx = (self.current_color_idx + 1) % len(self.colors) - color_to = self.colors[color_to_idx] + from_color = self.colors[self.current_color_idx] + to_color_idx = (self.current_color_idx + 1) % num_colors + to_color = self.colors[to_color_idx] + + # Calculate interpolation factor (0.0 to 1.0) + # transition_step goes from 0 to transition_duration - 1 + if self.transition_duration > 0: + interp_factor = self.transition_step / self.transition_duration + else: + interp_factor = 1.0 # Immediately transition if duration is zero + + # Interpolate each color component + r = int(from_color[0] + (to_color[0] - from_color[0]) * interp_factor) + g = int(from_color[1] + (to_color[1] - from_color[1]) * interp_factor) + b = int(from_color[2] + (to_color[2] - from_color[2]) * interp_factor) + + self.current_color = (r, g, b) + self.fill(self.apply_brightness(self.current_color)) + + self.transition_step += self.delay # Advance the transition step by the delay if self.transition_step >= self.transition_duration: - # Transition complete to the next color - self.current_color_idx = color_to_idx # Move to the next color in the sequence - self.transition_step = 0 # Reset transition step + # Transition complete, move to the next color and reset for hold phase + self.current_color_idx = to_color_idx + self.current_color = self.colors[self.current_color_idx] # Ensure current_color is the exact target color + self.transition_step = 0 # Reset transition progress + self.hold_start_time = current_time # Start hold phase for the new color - # Calculate the interpolation factor (0 to 1) - factor = self.transition_step / self.transition_duration + self.last_update = current_time - # Get the interpolated color and apply brightness - interpolated_color = self.interpolate_color(color_from, color_to, factor) - self.current_color = self.apply_brightness(interpolated_color) + def flicker_step(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)) - # Fill the LEDs with the current interpolated color - self.fill(self.current_color) - - -if __name__ == "__main__": - p = Patterns(4, 180) - p.set_color1((255,0,0)) - p.set_color2((0,0,255)) # Blue - p.add_color((0,255,0)) # Green - p.add_color((255,255,0)) # Yellow - #p.set_delay(10) - try: - while True: - for key in p.patterns: - print(key) - p.select(key) - for _ in range(2000): - p.tick() - utime.sleep_ms(1) - except KeyboardInterrupt: - p.fill((0, 0, 0)) + flicker_color = self.apply_brightness(base_color, brightness_override=flicker_brightness) + self.fill(flicker_color) + self.last_update = current_time