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:
@@ -4,6 +4,7 @@ from .pulse import Pulse
|
||||
from .transition import Transition
|
||||
from .chase import Chase
|
||||
from .circle import Circle
|
||||
from .double_circle import DoubleCircle
|
||||
from .roll import Roll
|
||||
from .calibration import Calibration
|
||||
from .test import Test
|
||||
@@ -14,4 +15,5 @@ from .lift import Lift
|
||||
from .flare import Flare
|
||||
from .hook import Hook
|
||||
from .pose import Pose
|
||||
from .point import Point
|
||||
from .segments import Segments
|
||||
from .segments_transition import SegmentsTransition
|
||||
|
||||
84
pico/src/patterns/double_circle.py
Normal file
84
pico/src/patterns/double_circle.py
Normal file
@@ -0,0 +1,84 @@
|
||||
class DoubleCircle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
DoubleCircle: symmetric band around a center index on the logical ring.
|
||||
|
||||
- n1: center index on the logical ring (0-based, on reference strip 0)
|
||||
- n2: radius of the band (max distance from center)
|
||||
- n3: direction mode
|
||||
0 → LEDs start ALL OFF and turn ON n4 LEDs at a time outward from n1 toward n1±n2
|
||||
1 → LEDs start ALL ON within radius n2 and turn OFF n4 LEDs at a time inward toward n1
|
||||
- n4: step size in LEDs per update
|
||||
- c[0]: base color used for the band
|
||||
"""
|
||||
num_leds = self.driver.num_leds
|
||||
if num_leds <= 0:
|
||||
while True:
|
||||
yield
|
||||
|
||||
colors = preset.c or []
|
||||
base1 = colors[0] if len(colors) >= 1 else (255, 255, 255)
|
||||
off = (0, 0, 0)
|
||||
|
||||
# Apply preset/global brightness
|
||||
color_on = self.driver.apply_brightness(base1, preset.b)
|
||||
color_off = off
|
||||
|
||||
# Center index and radius from preset; clamp center to ring length
|
||||
center = int(getattr(preset, "n1", 0)) % num_leds
|
||||
radius = max(1, int(getattr(preset, "n2", 0)) or 1)
|
||||
mode = int(getattr(preset, "n3", 0) or 0) # 0 = grow band outward, 1 = shrink band inward
|
||||
step_size = max(1, int(getattr(preset, "n4", 1)) or 1)
|
||||
|
||||
num_strips = len(self.driver.strips)
|
||||
|
||||
# Current "front" of the band, as a distance from center
|
||||
# mode 0: grow band outward (0 → radius)
|
||||
# mode 1: shrink band inward (radius → 0)
|
||||
if mode == 0:
|
||||
current = 0
|
||||
else:
|
||||
current = radius
|
||||
|
||||
while True:
|
||||
# Draw current frame based on current radius
|
||||
for i in range(num_leds):
|
||||
# Shortest circular distance from i to center
|
||||
forward = (i - center) % num_leds
|
||||
backward = (center - i) % num_leds
|
||||
dist = forward if forward < backward else backward
|
||||
|
||||
if dist > radius:
|
||||
c = color_off
|
||||
else:
|
||||
if mode == 0:
|
||||
# Grow outward: lit if within current radius
|
||||
c = color_on if dist <= current else color_off
|
||||
else:
|
||||
# Shrink inward: lit if within current radius (band contracts toward center)
|
||||
c = color_on if dist <= current else color_off
|
||||
|
||||
for strip_idx in range(num_strips):
|
||||
self.driver.set(strip_idx, i, c)
|
||||
|
||||
self.driver.show_all()
|
||||
|
||||
# Update current radius for next frame
|
||||
if mode == 0:
|
||||
if current >= radius:
|
||||
# Finished growing; hold final frame
|
||||
while True:
|
||||
yield
|
||||
current = min(radius, current + step_size)
|
||||
else:
|
||||
if current <= 0:
|
||||
# Finished shrinking; hold final frame
|
||||
while True:
|
||||
yield
|
||||
current = max(0, current - step_size)
|
||||
|
||||
yield
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
class Point:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
# Apply preset/global brightness once per color
|
||||
c1 = self.driver.apply_brightness(preset.c[0], preset.b)
|
||||
c2 = self.driver.apply_brightness(preset.c[1], preset.b)
|
||||
c3 = self.driver.apply_brightness(preset.c[2], preset.b)
|
||||
c4 = self.driver.apply_brightness(preset.c[3], preset.b)
|
||||
|
||||
# Helper to normalize and clamp a range
|
||||
self.driver.fill_n(c1, preset.n1, preset.n2)
|
||||
self.driver.fill_n(c2, preset.n3, preset.n4)
|
||||
self.driver.fill_n(c3, preset.n5, preset.n6)
|
||||
self.driver.fill_n(c4, preset.n7, preset.n8)
|
||||
self.driver.show_all()
|
||||
|
||||
18
pico/src/patterns/segments.py
Normal file
18
pico/src/patterns/segments.py
Normal file
@@ -0,0 +1,18 @@
|
||||
class Segments:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
# Apply preset/global brightness once per color
|
||||
ranges = [
|
||||
(preset.n1, preset.n2),
|
||||
(preset.n3, preset.n4),
|
||||
(preset.n5, preset.n6),
|
||||
(preset.n7, preset.n8),
|
||||
]
|
||||
|
||||
for n, color in enumerate(preset.c):
|
||||
self.driver.fill_n(color, ranges[n][0], ranges[n][1])
|
||||
|
||||
self.driver.show_all()
|
||||
|
||||
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