import utime import random from patterns_base import PatternBase # Import PatternBase 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): 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, "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 } # 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 handled by PatternBase # Transition attributes handled by PatternBase # Scanner attributes handled by PatternBase # self.run handled by PatternBase def set_on_width(self, on_width): self.on_width = on_width 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 set_fill_range(self, n1, n2): self.n1 = n1 self.n2 = n2 self.sync() 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: 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'.") self.selected = "on" # Fallback if not enough colors self.sync() # Re-sync for the new pattern else: 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.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 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]) current_time = utime.ticks_ms() 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 return self.delay def rainbow_cycle(self): current_time = utime.ticks_ms() 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) 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 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: 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 return self.delay 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 return self.delay def color_transition(self): current_time = utime.ticks_ms() # 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 self.delay # 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 self.delay 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, 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 self.last_update = current_time return self.delay def flicker(self): current_time = utime.ticks_ms() 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 return max(1, int(self.delay // 5)) def scanner(self): """ Mimics a 'Knight Rider' style scanner, moving in one direction. """ current_time = utime.ticks_ms() 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]) # 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 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.last_update = current_time return self.delay def bidirectional_scanner(self): """ Mimics a 'Knight Rider' style scanner, moving back and forth. """ current_time = utime.ticks_ms() 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 return self.delay 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]) for i in range(self.n1, self.n2 + 1): self.n[i] = color self.n.write() self.last_update = current_time return self.delay self.last_update = current_time return self.delay 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 self.delay 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 return self.delay 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 self.delay # 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 return self.delay * 2 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 return self.delay if __name__ == "__main__": 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": 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}") # 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 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: delay = p.tick(delay) wdt.feed() print("\n--- Test routine finished ---")