diff --git a/src/patterns.py b/src/patterns.py index 9ae3a9f..192ac49 100644 --- a/src/patterns.py +++ b/src/patterns.py @@ -1,14 +1,67 @@ +from machine import Pin +from neopixel import NeoPixel import utime -from patterns_base import Patterns_Base -class Patterns(Patterns_Base): - def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100): - super().__init__(pin, num_leds, color1, color2, brightness, selected, delay) + +# Short-key parameter mapping for convenience setters +param_mapping = { + "pt": "selected", + "pa": "selected", + "cl": "colors", + "br": "brightness", + "dl": "delay", + "nl": "num_leds", + "co": "color_order", + "lp": "led_pin", + "n1": "n1", + "n2": "n2", + "n3": "n3", + "n4": "n4", + "n5": "n5", + "n6": "n6", + "auto": "auto", +} + +class Preset: + def __init__(self, data): + # Set default values for all preset attributes + self.pattern = "off" + self.delay = 100 + self.brightness = 127 + self.colors = [(255, 255, 255)] self.auto = True + self.n1 = 0 + self.n2 = 0 + self.n3 = 0 + self.n4 = 0 + self.n5 = 0 + self.n6 = 0 + + # Override defaults with provided data + self.edit(data) + + def edit(self, data=None): + if not data: + return False + for key, value in data.items(): + setattr(self, key, value) + return True + +class Patterns: + def __init__(self, pin, num_leds, brightness=127, selected="off", delay=100): + self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) + self.num_leds = num_leds + self.brightness = brightness self.step = 0 + self.selected = selected + + self.generator = None + self.presets = {} + + # Register all pattern methods self.patterns = { "off": self.off, - "on" : self.on, + "on": self.on, "blink": self.blink, "rainbow": self.rainbow, "pulse": self.pulse, @@ -16,7 +69,86 @@ class Patterns(Patterns_Base): "chase": self.chase, "circle": self.circle, } + + self.select(self.selected) + def edit(self, name, data): + """Create or update a preset with the given name.""" + if name in self.presets: + # Update existing preset + self.presets[name].edit(data) + else: + # Create new preset + self.presets[name] = Preset(data) + return True + + def delete(self, name): + if name in self.presets: + del self.presets[name] + return True + return False + + def tick(self): + if self.generator is None: + return + try: + next(self.generator) + except StopIteration: + self.generator = None + + def select(self, preset_name, step=None): + if preset_name in self.presets: + preset = self.presets[preset_name] + if preset.pattern in self.patterns: + # Set step value if explicitly provided + if step is not None: + self.step = step + elif preset.pattern == "off" or self.selected != preset_name: + self.step = 0 + self.generator = self.patterns[preset.pattern](preset) + self.selected = preset_name # Store the preset name, not the object + return True + # If preset doesn't exist or pattern not found, default to "off" + return False + + def set_param(self, key, value): + if key in param_mapping: + setattr(self, param_mapping[key], value) + return True + print(f"Invalid parameter: {key}") + return False + + def update_num_leds(self, pin, num_leds): + self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) + self.num_leds = num_leds + + 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 fill(self, color=None): + fill_color = color if color is not None else (0, 0, 0) + for i in range(self.num_leds): + self.n[i] = fill_color + self.n.write() + + def off(self, preset=None): + self.fill((0, 0, 0)) + + def on(self, preset): + colors = preset.colors + color = colors[0] if colors else (255, 255, 255) + self.fill(self.apply_brightness(color, preset.brightness)) + + def wheel(self, 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) def blink(self, preset): state = True # True = on, False = off @@ -35,7 +167,6 @@ class Patterns(Patterns_Base): # Yield once per tick so other logic can run yield - def rainbow(self, preset): step = self.step % 256 step_amount = max(1, int(preset.n1)) # n1 controls step increment @@ -68,7 +199,6 @@ class Patterns(Patterns_Base): # Yield once per tick so other logic can run yield - def pulse(self, preset): self.off() @@ -159,7 +289,7 @@ class Patterns(Patterns_Base): if elapsed >= duration: # End of this transition step - if not preset.auto and color_index >= 0: + if not preset.auto: # One-shot: transition from first to second color only self.fill(self.apply_brightness(c2, preset.brightness)) break @@ -186,37 +316,89 @@ class Patterns(Patterns_Base): # Need at least 1 color return - segment_length = 0 # Will be calculated in loop - position = 0 # Current position offset - step_count = 0 # Track which step we're on + # Access colors, delay, and n values from preset + if not colors: + return + # If only one color provided, use it for both colors + if len(colors) < 2: + color0 = colors[0] + color1 = colors[0] + else: + color0 = colors[0] + color1 = colors[1] + color0 = self.apply_brightness(color0, preset.brightness) + color1 = self.apply_brightness(color1, preset.brightness) + + n1 = max(1, int(preset.n1)) # LEDs of color 0 + n2 = max(1, int(preset.n2)) # LEDs of color 1 + n3 = int(preset.n3) # Step movement on even steps (can be negative) + n4 = int(preset.n4) # Step movement on odd steps (can be negative) + + segment_length = n1 + n2 + + # Calculate position from step_count + step_count = self.step + # Position alternates: step 0 adds n3, step 1 adds n4, step 2 adds n3, etc. + if step_count % 2 == 0: + # Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3 + position = (step_count // 2) * (n3 + n4) + n3 + else: + # Odd steps: ((step_count+1)//2) pairs of (n3+n4) + position = ((step_count + 1) // 2) * (n3 + n4) + + # Wrap position to keep it reasonable + max_pos = self.num_leds + segment_length + position = position % max_pos + if position < 0: + position += max_pos + + # If auto is False, run a single step and then stop + if not preset.auto: + # Clear all LEDs + self.n.fill((0, 0, 0)) + + # Draw repeating pattern starting at position + for i in range(self.num_leds): + # Calculate position in the repeating segment + relative_pos = (i - position) % segment_length + if relative_pos < 0: + relative_pos = (relative_pos + segment_length) % segment_length + + # Determine which color based on position in segment + if relative_pos < n1: + self.n[i] = color0 + else: + self.n[i] = color1 + + self.n.write() + + # Increment step for next beat + self.step = step_count + 1 + + # Allow tick() to advance the generator once + yield + return + + # Auto mode: continuous loop last_update = utime.ticks_ms() - + transition_duration = max(10, int(preset.delay)) + while True: - # Access colors, delay, and n values from preset - if not colors: - break - # If only one color provided, use it for both colors - if len(colors) < 2: - color0 = colors[0] - color1 = colors[0] - else: - color0 = colors[0] - color1 = colors[1] - - color0 = self.apply_brightness(color0, preset.brightness) - color1 = self.apply_brightness(color1, preset.brightness) - - n1 = max(1, int(preset.n1)) # LEDs of color 0 - n2 = max(1, int(preset.n2)) # LEDs of color 1 - n3 = int(preset.n3) # Step movement on odd steps (can be negative) - n4 = int(preset.n4) # Step movement on even steps (can be negative) - - segment_length = n1 + n2 - transition_duration = max(10, int(preset.delay)) - current_time = utime.ticks_ms() if utime.ticks_diff(current_time, last_update) >= transition_duration: + # Calculate current position from step_count + if step_count % 2 == 0: + position = (step_count // 2) * (n3 + n4) + n3 + else: + position = ((step_count + 1) // 2) * (n3 + n4) + + # Wrap position + max_pos = self.num_leds + segment_length + position = position % max_pos + if position < 0: + position += max_pos + # Clear all LEDs self.n.fill((0, 0, 0)) @@ -235,19 +417,9 @@ class Patterns(Patterns_Base): self.n.write() - # Move position by n3 or n4 on alternate steps - if step_count % 2 == 0: - position = position + n3 - else: - position = position + n4 - - # Wrap position to keep it reasonable - max_pos = self.num_leds + segment_length - position = position % max_pos - if position < 0: - position += max_pos - + # Increment step step_count += 1 + self.step = step_count last_update = current_time # Yield once per tick so other logic can run @@ -325,4 +497,4 @@ class Patterns(Patterns_Base): self.n.write() # Yield once per tick so other logic can run - yield \ No newline at end of file + yield