Add segments and double_circle patterns with shared presets
Introduce double_circle and segments-based patterns on the Pico, refactor the Presets engine to expose a logical ring over all strips, and migrate presets/test code from the old point pattern to segments while switching to a top-level presets.json. Made-with: Cursor
This commit is contained in:
136
pico/src/patterns/segments_transition.py
Normal file
136
pico/src/patterns/segments_transition.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import utime
|
||||
|
||||
|
||||
class SegmentsTransition:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
SegmentsTransition: fade from whatever is currently on the strips
|
||||
to a new static Segments layout defined by n1–n8 and c[0..3].
|
||||
|
||||
- Uses the existing strip buffers as the starting state.
|
||||
- Target state matches the Segments pattern: up to 4 colored bands
|
||||
along the logical reference strip, mapped to all physical strips.
|
||||
- Transition duration is taken from preset.d (ms), minimum 50ms.
|
||||
"""
|
||||
strips = self.driver.strips
|
||||
if not strips:
|
||||
while True:
|
||||
yield
|
||||
|
||||
# Snapshot starting GRB buffers (already scaled by per-strip brightness)
|
||||
start_bufs = [bytes(strip.ar) for strip in strips]
|
||||
|
||||
# Prepare target buffers (same length as each strip's ar)
|
||||
target_bufs = [bytearray(len(strip.ar)) for strip in strips]
|
||||
|
||||
# Base colors (up to 4), missing ones default to black
|
||||
colors = list(preset.c) if getattr(preset, "c", None) else []
|
||||
while len(colors) < 4:
|
||||
colors.append((0, 0, 0))
|
||||
|
||||
# Apply preset/global brightness once per color
|
||||
bright_colors = [
|
||||
self.driver.apply_brightness(colors[0], preset.b),
|
||||
self.driver.apply_brightness(colors[1], preset.b),
|
||||
self.driver.apply_brightness(colors[2], preset.b),
|
||||
self.driver.apply_brightness(colors[3], preset.b),
|
||||
]
|
||||
|
||||
# Logical reference length for all strips (from scale_map[0])
|
||||
ref_len = len(self.driver.scale_map[0]) if self.driver.scale_map else 0
|
||||
if ref_len <= 0:
|
||||
# Fallback: nothing to do, just hold current state
|
||||
while True:
|
||||
yield
|
||||
|
||||
# Helper to clamp and normalize a logical range [a, b] (inclusive) over ref_len.
|
||||
# Returns (start, end_exclusive) suitable for range(start, end_exclusive).
|
||||
def norm_range(a, b):
|
||||
a = int(a)
|
||||
b = int(b)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
if b < 0 or a >= ref_len:
|
||||
return None
|
||||
a = max(0, a)
|
||||
b = min(ref_len - 1, b)
|
||||
if a > b:
|
||||
return None
|
||||
return a, b + 1
|
||||
|
||||
raw_ranges = [
|
||||
(getattr(preset, "n1", 0), getattr(preset, "n2", -1), bright_colors[0]),
|
||||
(getattr(preset, "n3", 0), getattr(preset, "n4", -1), bright_colors[1]),
|
||||
(getattr(preset, "n5", 0), getattr(preset, "n6", -1), bright_colors[2]),
|
||||
(getattr(preset, "n7", 0), getattr(preset, "n8", -1), bright_colors[3]),
|
||||
]
|
||||
|
||||
# Build target buffers using the same logical indexing idea as Segments
|
||||
for strip_idx, strip in enumerate(strips):
|
||||
bright = strip.brightness
|
||||
scale_map = self.driver.scale_map[strip_idx]
|
||||
buf = target_bufs[strip_idx]
|
||||
n_leds = strip.num_leds
|
||||
|
||||
# Start from black everywhere
|
||||
for i in range(len(buf)):
|
||||
buf[i] = 0
|
||||
|
||||
# Apply each logical range to this strip
|
||||
for a, b, color in raw_ranges:
|
||||
rng = norm_range(a, b)
|
||||
if not rng:
|
||||
continue
|
||||
start, end = rng
|
||||
r, g, bl = color
|
||||
for logical_idx in range(start, end):
|
||||
if logical_idx < 0 or logical_idx >= len(scale_map):
|
||||
continue
|
||||
phys_idx = scale_map[logical_idx]
|
||||
if phys_idx < 0 or phys_idx >= n_leds:
|
||||
continue
|
||||
base = phys_idx * 3
|
||||
if base + 2 >= len(buf):
|
||||
continue
|
||||
buf[base] = int(g * bright)
|
||||
buf[base + 1] = int(r * bright)
|
||||
buf[base + 2] = int(bl * bright)
|
||||
|
||||
# Duration in ms for the whole transition (slower by default)
|
||||
# If preset.d is provided, use it; otherwise default to a slow 3000ms fade.
|
||||
raw_d = int(getattr(preset, "d", 3000) or 3000)
|
||||
duration = max(1000, raw_d) # enforce at least 1s for a clearly visible transition
|
||||
start_time = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
elapsed = utime.ticks_diff(now, start_time)
|
||||
|
||||
if elapsed >= duration:
|
||||
# Final frame: commit target buffers and hold, then update all strips together
|
||||
for strip, target in zip(strips, target_bufs):
|
||||
ar = strip.ar
|
||||
for i in range(len(ar)):
|
||||
ar[i] = target[i]
|
||||
self.driver.show_all()
|
||||
while True:
|
||||
yield
|
||||
|
||||
# Interpolation factor in [0,1]
|
||||
factor = elapsed / duration
|
||||
inv = 1.0 - factor
|
||||
|
||||
# Blend from start to target in GRB space per byte
|
||||
for idx, strip in enumerate(strips):
|
||||
start_buf = start_bufs[idx]
|
||||
target_buf = target_bufs[idx]
|
||||
ar = strip.ar
|
||||
for i in range(len(ar)):
|
||||
ar[i] = int(start_buf[i] * inv + target_buf[i] * factor)
|
||||
self.driver.show_all()
|
||||
|
||||
yield
|
||||
|
||||
Reference in New Issue
Block a user