Add beat functionality and synchronization support
- Beat: calling select() again with same preset restarts pattern - Synchronization: reset step when selecting 'off' or switching presets - Manual mode chase: advance one step per beat, calculate position from step
This commit is contained in:
266
src/patterns.py
266
src/patterns.py
@@ -1,14 +1,67 @@
|
|||||||
|
from machine import Pin
|
||||||
|
from neopixel import NeoPixel
|
||||||
import utime
|
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):
|
# Short-key parameter mapping for convenience setters
|
||||||
super().__init__(pin, num_leds, color1, color2, brightness, selected, delay)
|
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.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.step = 0
|
||||||
|
self.selected = selected
|
||||||
|
|
||||||
|
self.generator = None
|
||||||
|
self.presets = {}
|
||||||
|
|
||||||
|
# Register all pattern methods
|
||||||
self.patterns = {
|
self.patterns = {
|
||||||
"off": self.off,
|
"off": self.off,
|
||||||
"on" : self.on,
|
"on": self.on,
|
||||||
"blink": self.blink,
|
"blink": self.blink,
|
||||||
"rainbow": self.rainbow,
|
"rainbow": self.rainbow,
|
||||||
"pulse": self.pulse,
|
"pulse": self.pulse,
|
||||||
@@ -16,7 +69,86 @@ class Patterns(Patterns_Base):
|
|||||||
"chase": self.chase,
|
"chase": self.chase,
|
||||||
"circle": self.circle,
|
"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):
|
def blink(self, preset):
|
||||||
state = True # True = on, False = off
|
state = True # True = on, False = off
|
||||||
@@ -35,7 +167,6 @@ class Patterns(Patterns_Base):
|
|||||||
# Yield once per tick so other logic can run
|
# Yield once per tick so other logic can run
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def rainbow(self, preset):
|
def rainbow(self, preset):
|
||||||
step = self.step % 256
|
step = self.step % 256
|
||||||
step_amount = max(1, int(preset.n1)) # n1 controls step increment
|
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 once per tick so other logic can run
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def pulse(self, preset):
|
def pulse(self, preset):
|
||||||
self.off()
|
self.off()
|
||||||
|
|
||||||
@@ -159,7 +289,7 @@ class Patterns(Patterns_Base):
|
|||||||
|
|
||||||
if elapsed >= duration:
|
if elapsed >= duration:
|
||||||
# End of this transition step
|
# 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
|
# One-shot: transition from first to second color only
|
||||||
self.fill(self.apply_brightness(c2, preset.brightness))
|
self.fill(self.apply_brightness(c2, preset.brightness))
|
||||||
break
|
break
|
||||||
@@ -186,37 +316,89 @@ class Patterns(Patterns_Base):
|
|||||||
# Need at least 1 color
|
# Need at least 1 color
|
||||||
return
|
return
|
||||||
|
|
||||||
segment_length = 0 # Will be calculated in loop
|
# Access colors, delay, and n values from preset
|
||||||
position = 0 # Current position offset
|
if not colors:
|
||||||
step_count = 0 # Track which step we're on
|
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()
|
last_update = utime.ticks_ms()
|
||||||
|
transition_duration = max(10, int(preset.delay))
|
||||||
|
|
||||||
while True:
|
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()
|
current_time = utime.ticks_ms()
|
||||||
if utime.ticks_diff(current_time, last_update) >= transition_duration:
|
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
|
# Clear all LEDs
|
||||||
self.n.fill((0, 0, 0))
|
self.n.fill((0, 0, 0))
|
||||||
|
|
||||||
@@ -235,19 +417,9 @@ class Patterns(Patterns_Base):
|
|||||||
|
|
||||||
self.n.write()
|
self.n.write()
|
||||||
|
|
||||||
# Move position by n3 or n4 on alternate steps
|
# Increment step
|
||||||
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
|
|
||||||
|
|
||||||
step_count += 1
|
step_count += 1
|
||||||
|
self.step = step_count
|
||||||
last_update = current_time
|
last_update = current_time
|
||||||
|
|
||||||
# Yield once per tick so other logic can run
|
# Yield once per tick so other logic can run
|
||||||
@@ -325,4 +497,4 @@ class Patterns(Patterns_Base):
|
|||||||
self.n.write()
|
self.n.write()
|
||||||
|
|
||||||
# Yield once per tick so other logic can run
|
# Yield once per tick so other logic can run
|
||||||
yield
|
yield
|
||||||
|
|||||||
Reference in New Issue
Block a user