Refactor patterns to use preset-based API and fix initialization order

- Fix initialization order: initialize self.presets before calling self.select()
- Separate add() and edit() methods: add() creates new presets, edit() updates existing ones
- Update all test files to use add() instead of edit() for creating presets
- Add comprehensive auto/manual mode test
- Remove tool.py (moved to led-tool project)
This commit is contained in:
2026-01-25 23:23:14 +13:00
parent f4ef415b5a
commit b7d2f52fc3
16 changed files with 1593 additions and 1105 deletions

View File

@@ -1,7 +1,7 @@
import utime
from patterns_base import atternsBase
from patterns_base import Patterns_Base
class Patterns(PatternsBase):
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)
self.auto = True
@@ -18,15 +18,16 @@ class Patterns(PatternsBase):
}
def blink(self):
def blink(self, preset):
state = True # True = on, False = off
last_update = utime.ticks_ms()
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= self.delay:
if utime.ticks_diff(current_time, last_update) >= preset.delay:
if state:
self.fill(self.apply_brightness(self.colors[0]))
color = preset.colors[0] if preset.colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.brightness))
else:
self.fill((0, 0, 0))
state = not state
@@ -35,15 +36,15 @@ class Patterns(PatternsBase):
yield
def rainbow(self):
def rainbow(self, preset):
step = self.step % 256
step_amount = max(1, int(self.n1)) # n1 controls step increment
step_amount = max(1, int(preset.n1)) # n1 controls step increment
# If auto is False, run a single step and then stop
if not self.auto:
if not preset.auto:
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))
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.brightness)
self.n.write()
# Increment step by n1 for next manual call
self.step = (step + step_amount) % 256
@@ -55,11 +56,11 @@ class Patterns(PatternsBase):
while True:
current_time = utime.ticks_ms()
sleep_ms = max(1, int(self.delay)) # Access delay directly
sleep_ms = max(1, int(preset.delay)) # 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))
self.n[i] = self.apply_brightness(self.wheel(rc_index & 255), preset.brightness)
self.n.write()
step = (step + step_amount) % 256
self.step = step
@@ -68,23 +69,24 @@ class Patterns(PatternsBase):
yield
def pulse(self):
def pulse(self, preset):
self.off()
# Ensure we have at least one color
if not self.colors:
self.colors = [(255, 255, 255)]
# Get colors from preset
colors = preset.colors
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 each cycle so they can be changed live
attack_ms = max(0, int(self.n1)) # Attack time in ms
hold_ms = max(0, int(self.n2)) # Hold time in ms
decay_ms = max(0, int(self.n3)) # Decay time in ms
delay_ms = max(0, int(self.delay))
# 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.delay))
total_ms = attack_ms + hold_ms + decay_ms + delay_ms
if total_ms <= 0:
@@ -93,22 +95,22 @@ class Patterns(PatternsBase):
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, cycle_start)
base_color = self.colors[color_index % len(self.colors)]
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))
self.fill(self.apply_brightness(color, preset.brightness))
elif elapsed < attack_ms + hold_ms:
# Hold: full brightness
self.fill(self.apply_brightness(base_color))
self.fill(self.apply_brightness(base_color, preset.brightness))
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))
self.fill(self.apply_brightness(color, preset.brightness))
elif elapsed < total_ms:
# Delay phase: LEDs off between pulses
self.fill((0, 0, 0))
@@ -116,7 +118,7 @@ class Patterns(PatternsBase):
# End of cycle, move to next color and restart timing
color_index += 1
cycle_start = now
if not self.auto:
if not preset.auto:
break
# Skip drawing this tick, start next cycle
yield
@@ -125,17 +127,18 @@ class Patterns(PatternsBase):
# Yield once per tick
yield
def transition(self):
def transition(self, preset):
"""Transition between colors, blending over `delay` ms."""
if not self.colors:
colors = preset.colors
if not colors:
self.off()
yield
return
# Only one color: just keep it on
if len(self.colors) == 1:
if len(colors) == 1:
while True:
self.fill(self.apply_brightness(self.colors[0]))
self.fill(self.apply_brightness(colors[0], preset.brightness))
yield
return
@@ -143,25 +146,25 @@ class Patterns(PatternsBase):
start_time = utime.ticks_ms()
while True:
if not self.colors:
if not colors:
break
# Get current and next color based on live list
c1 = self.colors[color_index % len(self.colors)]
c2 = self.colors[(color_index + 1) % len(self.colors)]
c1 = colors[color_index % len(colors)]
c2 = colors[(color_index + 1) % len(colors)]
duration = max(10, int(self.delay)) # At least 10ms
duration = max(10, int(preset.delay)) # At least 10ms
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time)
if elapsed >= duration:
# End of this transition step
if not self.auto and color_index >= 0:
if not preset.auto and color_index >= 0:
# One-shot: transition from first to second color only
self.fill(self.apply_brightness(c2))
self.fill(self.apply_brightness(c2, preset.brightness))
break
# Auto: move to next pair
color_index = (color_index + 1) % len(self.colors)
color_index = (color_index + 1) % len(colors)
start_time = now
yield
continue
@@ -171,14 +174,15 @@ class Patterns(PatternsBase):
interpolated = tuple(
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
)
self.fill(self.apply_brightness(interpolated))
self.fill(self.apply_brightness(interpolated, preset.brightness))
yield
def chase(self):
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)"""
if len(self.colors) < 1:
colors = preset.colors
if len(colors) < 1:
# Need at least 1 color
return
@@ -189,27 +193,27 @@ class Patterns(PatternsBase):
last_update = utime.ticks_ms()
while True:
# Access colors, delay, and n values directly for live updates
if not self.colors:
# Access colors, delay, and n values from preset
if not colors:
break
# If only one color provided, use it for both colors
if len(self.colors) < 2:
color0 = self.colors[0]
color1 = self.colors[0]
if len(colors) < 2:
color0 = colors[0]
color1 = colors[0]
else:
color0 = self.colors[0]
color1 = self.colors[1]
color0 = colors[0]
color1 = colors[1]
color0 = self.apply_brightness(color0)
color1 = self.apply_brightness(color1)
color0 = self.apply_brightness(color0, preset.brightness)
color1 = self.apply_brightness(color1, preset.brightness)
n1 = max(1, int(self.n1)) # LEDs of color 0
n2 = max(1, int(self.n2)) # LEDs of color 1
n3 = int(self.n3) # Step movement on odd steps (can be negative)
n4 = int(self.n4) # Step movement on even steps (can be negative)
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(self.delay))
transition_duration = max(10, int(preset.delay))
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= transition_duration:
@@ -249,16 +253,16 @@ class Patterns(PatternsBase):
# Yield once per tick so other logic can run
yield
def circle(self):
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
head_rate = max(1, int(self.n1)) # n1 = head moves per second
tail_rate = max(1, int(self.n3)) # n3 = tail moves per second
max_length = max(1, int(self.n2)) # n2 = max length
min_length = max(0, int(self.n4)) # n4 = min length
# 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
@@ -268,6 +272,9 @@ class Patterns(PatternsBase):
phase = "growing" # "growing", "shrinking", or "off"
colors = preset.colors
color = self.apply_brightness(colors[0] if colors else (255, 255, 255), preset.brightness)
while True:
current_time = utime.ticks_ms()
@@ -280,7 +287,6 @@ class Patterns(PatternsBase):
segment_length = self.num_leds
# Draw segment from tail to head
color = self.apply_brightness(self.colors[0])
for i in range(segment_length + 1):
led_pos = (tail + i) % self.num_leds
self.n[led_pos] = color

View File

@@ -24,6 +24,31 @@ param_mapping = {
"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_Base:
def __init__(self, pin, num_leds, color1=(0,0,0), color2=(0,0,0), brightness=127, selected="off", delay=100):
self.n = NeoPixel(Pin(pin, Pin.OUT), num_leds)
@@ -46,7 +71,26 @@ class Patterns_Base:
self.n6 = 0
self.generator = None
self.select(self.selected)
self.presets = {}
self.select(self.selected)
def edit(self, name, data):
if name in self.presets:
self.presets[name].edit(data)
return True
return False
def add(self, name, data):
self.presets[name] = Preset(data)
return
def delete(self, name):
if name in self.presets:
del self.presets[name]
return True
return False
def tick(self):
@@ -57,13 +101,14 @@ class Patterns_Base:
except StopIteration:
self.generator = None
def select(self, pattern):
if pattern in self.patterns:
self.selected = pattern
self.generator = self.patterns[pattern]()
print(f"Selected pattern: {pattern}")
return True
# If pattern doesn't exist, default to "off"
def select(self, preset_name):
if preset_name in self.presets:
preset = self.presets[preset_name]
if preset.pattern in self.patterns:
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):
@@ -108,11 +153,13 @@ class Patterns_Base:
self.n[i] = fill_color
self.n.write()
def off(self):
def off(self, preset=None):
self.fill((0, 0, 0))
def on(self):
self.fill(self.apply_brightness(self.colors[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))
@@ -127,4 +174,3 @@ class Patterns_Base:
pos -= 170
return (0, pos * 3, 255 - pos * 3)