diff --git a/src/presets.py b/src/presets.py index 6b46695..dc45175 100644 --- a/src/presets.py +++ b/src/presets.py @@ -1,28 +1,9 @@ from machine import Pin from neopixel import NeoPixel -import utime from preset import Preset +from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle -# 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 Presets: def __init__(self, pin, num_leds): self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds) @@ -39,12 +20,12 @@ class Presets: self.patterns = { "off": self.off, "on": self.on, - "blink": self.blink, - "rainbow": self.rainbow, - "pulse": self.pulse, - "transition": self.transition, - "chase": self.chase, - "circle": self.circle, + "blink": Blink(self).run, + "rainbow": Rainbow(self).run, + "pulse": Pulse(self).run, + "transition": Transition(self).run, + "chase": Chase(self).run, + "circle": Circle(self).run, } @@ -87,13 +68,6 @@ class Presets: # 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 @@ -118,389 +92,3 @@ class Presets: colors = preset.c color = colors[0] if colors else (255, 255, 255) self.fill(self.apply_brightness(color, preset.b)) - - 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): - """Blink pattern: toggles LEDs on/off using preset delay, cycling through colors.""" - # Use provided colors, or default to white if none - colors = preset.c if preset.c else [(255, 255, 255)] - color_index = 0 - state = True # True = on, False = off - last_update = utime.ticks_ms() - - while True: - current_time = utime.ticks_ms() - # Re-read delay each loop so live updates to preset.d take effect - delay_ms = max(1, int(preset.d)) - if utime.ticks_diff(current_time, last_update) >= delay_ms: - if state: - base_color = colors[color_index % len(colors)] - color = self.apply_brightness(base_color, preset.b) - self.fill(color) - # Advance to next color for the next "on" phase - color_index += 1 - else: - # "Off" phase: turn all LEDs off - self.fill((0, 0, 0)) - state = not state - last_update = current_time - # 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 - - # If auto is False, run a single step and then stop - if not preset.a: - for i in range(self.num_leds): - rc_index = (i * 256 // self.num_leds) + step - self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.b) - self.n.write() - # Increment step by n1 for next manual call - self.step = (step + step_amount) % 256 - # Allow tick() to advance the generator once - yield - return - - last_update = utime.ticks_ms() - - while True: - current_time = utime.ticks_ms() - sleep_ms = max(1, int(preset.d)) # Get delay from preset - if utime.ticks_diff(current_time, last_update) >= sleep_ms: - for i in range(self.num_leds): - rc_index = (i * 256 // self.num_leds) + step - self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.b) - self.n.write() - step = (step + step_amount) % 256 - self.step = step - last_update = current_time - # Yield once per tick so other logic can run - yield - - def pulse(self, preset): - self.off() - - # Get colors from preset - colors = preset.c - if not colors: - colors = [(255, 255, 255)] - - color_index = 0 - cycle_start = utime.ticks_ms() - - # State machine based pulse using a single generator loop - while True: - # Read current timing parameters from preset - attack_ms = max(0, int(preset.n1)) # Attack time in ms - hold_ms = max(0, int(preset.n2)) # Hold time in ms - decay_ms = max(0, int(preset.n3)) # Decay time in ms - delay_ms = max(0, int(preset.d)) - - total_ms = attack_ms + hold_ms + decay_ms + delay_ms - if total_ms <= 0: - total_ms = 1 - - now = utime.ticks_ms() - elapsed = utime.ticks_diff(now, cycle_start) - - base_color = colors[color_index % len(colors)] - - if elapsed < attack_ms and attack_ms > 0: - # Attack: fade 0 -> 1 - factor = elapsed / attack_ms - color = tuple(int(c * factor) for c in base_color) - self.fill(self.apply_brightness(color, preset.b)) - elif elapsed < attack_ms + hold_ms: - # Hold: full brightness - self.fill(self.apply_brightness(base_color, preset.b)) - elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0: - # Decay: fade 1 -> 0 - dec_elapsed = elapsed - attack_ms - hold_ms - factor = max(0.0, 1.0 - (dec_elapsed / decay_ms)) - color = tuple(int(c * factor) for c in base_color) - self.fill(self.apply_brightness(color, preset.b)) - elif elapsed < total_ms: - # Delay phase: LEDs off between pulses - self.fill((0, 0, 0)) - else: - # End of cycle, move to next color and restart timing - color_index += 1 - cycle_start = now - if not preset.a: - break - # Skip drawing this tick, start next cycle - yield - continue - - # Yield once per tick - yield - - def transition(self, preset): - """Transition between colors, blending over `delay` ms.""" - colors = preset.c - if not colors: - self.off() - yield - return - - # Only one color: just keep it on - if len(colors) == 1: - while True: - self.fill(self.apply_brightness(colors[0], preset.b)) - yield - return - - color_index = 0 - start_time = utime.ticks_ms() - - while True: - if not colors: - break - - # Get current and next color based on live list - c1 = colors[color_index % len(colors)] - c2 = colors[(color_index + 1) % len(colors)] - - duration = max(10, int(preset.d)) # At least 10ms - now = utime.ticks_ms() - elapsed = utime.ticks_diff(now, start_time) - - if elapsed >= duration: - # End of this transition step - if not preset.a: - # One-shot: transition from first to second color only - self.fill(self.apply_brightness(c2, preset.b)) - break - # Auto: move to next pair - color_index = (color_index + 1) % len(colors) - start_time = now - yield - continue - - # Interpolate between c1 and c2 - factor = elapsed / duration - interpolated = tuple( - int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3) - ) - self.fill(self.apply_brightness(interpolated, preset.b)) - - yield - - def chase(self, preset): - """Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating. - Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)""" - colors = preset.c - if len(colors) < 1: - # Need at least 1 color - return - - # 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.b) - color1 = self.apply_brightness(color1, preset.b) - - 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.a: - # 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 - # Use transition_duration for timing and force the first update to happen immediately - transition_duration = max(10, int(preset.d)) - last_update = utime.ticks_ms() - transition_duration - - while True: - 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)) - - # 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 - step_count += 1 - self.step = step_count - last_update = current_time - - # Yield once per tick so other logic can run - yield - - def circle(self, preset): - """Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4""" - head = 0 - tail = 0 - - # Calculate timing from preset - head_rate = max(1, int(preset.n1)) # n1 = head moves per second - tail_rate = max(1, int(preset.n3)) # n3 = tail moves per second - max_length = max(1, int(preset.n2)) # n2 = max length - min_length = max(0, int(preset.n4)) # n4 = min length - - head_delay = 1000 // head_rate # ms between head movements - tail_delay = 1000 // tail_rate # ms between tail movements - - last_head_move = utime.ticks_ms() - last_tail_move = utime.ticks_ms() - - phase = "growing" # "growing", "shrinking", or "off" - - # Support up to two colors (like chase). If only one color is provided, - # use black for the second; if none, default to white. - colors = preset.c - if not colors: - base0 = base1 = (255, 255, 255) - elif len(colors) == 1: - base0 = colors[0] - base1 = (0, 0, 0) - else: - base0 = colors[0] - base1 = colors[1] - - color0 = self.apply_brightness(base0, preset.b) - color1 = self.apply_brightness(base1, preset.b) - - while True: - current_time = utime.ticks_ms() - - # Background: use second color during the "off" phase, otherwise clear to black - if phase == "off": - self.n.fill(color1) - else: - self.n.fill((0, 0, 0)) - - # Calculate segment length - segment_length = (head - tail) % self.num_leds - if segment_length == 0 and head != tail: - segment_length = self.num_leds - - # Draw segment from tail to head as a solid color (no per-LED alternation) - current_color = color0 - for i in range(segment_length + 1): - led_pos = (tail + i) % self.num_leds - self.n[led_pos] = current_color - - # Move head continuously at n1 LEDs per second - if utime.ticks_diff(current_time, last_head_move) >= head_delay: - head = (head + 1) % self.num_leds - last_head_move = current_time - - # Tail behavior based on phase - if phase == "growing": - # Growing phase: tail stays at 0 until max length reached - if segment_length >= max_length: - phase = "shrinking" - elif phase == "shrinking": - # Shrinking phase: move tail forward at n3 LEDs per second - if utime.ticks_diff(current_time, last_tail_move) >= tail_delay: - tail = (tail + 1) % self.num_leds - last_tail_move = current_time - - # Check if we've reached min length - current_length = (head - tail) % self.num_leds - if current_length == 0 and head != tail: - current_length = self.num_leds - - # For min_length = 0, we need at least 1 LED (the head) - if min_length == 0 and current_length <= 1: - phase = "off" # All LEDs off for 1 step - elif min_length > 0 and current_length <= min_length: - phase = "growing" # Cycle repeats - else: # phase == "off" - # Off phase: second color fills the ring for 1 step, then restart - tail = head # Reset tail to head position to start fresh - phase = "growing" - - self.n.write() - - # Yield once per tick so other logic can run - yield