Add Pico presets engine, patterns, and tests.
Wire the Pico to UART-driven preset selection, add pattern modules and presets data, remove old p2p/settings code, and update tests and LED driver. Made-with: Cursor
This commit is contained in:
@@ -4,3 +4,32 @@ from .pulse import Pulse
|
||||
from .transition import Transition
|
||||
from .chase import Chase
|
||||
from .circle import Circle
|
||||
from .roll import Roll
|
||||
from .calibration import Calibration
|
||||
from .test import Test
|
||||
from .grab import Grab
|
||||
from .spin import Spin
|
||||
from .lift import Lift
|
||||
from .flare import Flare
|
||||
from .hook import Hook
|
||||
from .invertsplit import Invertsplit
|
||||
from .pose import Pose
|
||||
from .backbalance import Backbalance
|
||||
from .beat import Beat
|
||||
from .crouch import Crouch
|
||||
from .backbendsplit import Backbendsplit
|
||||
from .straddle import Straddle
|
||||
from .frontbalance import Frontbalance
|
||||
from .elbowhang import Elbowhang
|
||||
from .elbowhangspin import Elbowhangspin
|
||||
from .dismount import Dismount
|
||||
from .fluff import Fluff
|
||||
from .elbowhangsplit import Elbowhangsplit
|
||||
from .invert import Invert
|
||||
from .backbend import Backbend
|
||||
from .seat import Seat
|
||||
from .kneehang import Kneehang
|
||||
from .legswoop import Legswoop
|
||||
from .split import Split
|
||||
from .foothang import Foothang
|
||||
from .point import Point
|
||||
|
||||
9
pico/src/patterns/backbalance.py
Normal file
9
pico/src/patterns/backbalance.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbalance:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/backbend.py
Normal file
9
pico/src/patterns/backbend.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbend:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/backbendsplit.py
Normal file
9
pico/src/patterns/backbendsplit.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbendsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/beat.py
Normal file
9
pico/src/patterns/beat.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Beat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
38
pico/src/patterns/calibration.py
Normal file
38
pico/src/patterns/calibration.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Calibration: strips 2 and 6 only. First 10 green, then alternating 10 blue / 10 red. 10% brightness."""
|
||||
|
||||
BRIGHTNESS = 0.10
|
||||
BLOCK = 10
|
||||
STRIPS_ON = (2, 6) # 0-based: 3rd and 7th strip only
|
||||
|
||||
GREEN = (0, 255, 0)
|
||||
RED = (255, 0, 0)
|
||||
BLUE = (0, 0, 255)
|
||||
|
||||
|
||||
def _scale(color, factor):
|
||||
return tuple(int(c * factor) for c in color)
|
||||
|
||||
|
||||
class Calibration:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
green = _scale(GREEN, BRIGHTNESS)
|
||||
red = _scale(RED, BRIGHTNESS)
|
||||
blue = _scale(BLUE, BRIGHTNESS)
|
||||
on_set = set(STRIPS_ON)
|
||||
for strip_idx, strip in enumerate(strips):
|
||||
n = strip.num_leds
|
||||
if strip_idx not in on_set:
|
||||
strip.fill((0, 0, 0))
|
||||
strip.show()
|
||||
continue
|
||||
for i in range(n):
|
||||
if i < BLOCK:
|
||||
strip.set(i, green)
|
||||
else:
|
||||
block = (i - BLOCK) // BLOCK
|
||||
strip.set(i, blue if block % 2 == 0 else red)
|
||||
strip.show()
|
||||
9
pico/src/patterns/crouch.py
Normal file
9
pico/src/patterns/crouch.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Crouch:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/dismount.py
Normal file
9
pico/src/patterns/dismount.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Dismount:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/elbowhang.py
Normal file
9
pico/src/patterns/elbowhang.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/elbowhangspin.py
Normal file
9
pico/src/patterns/elbowhangspin.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhangspin:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/elbowhangsplit.py
Normal file
9
pico/src/patterns/elbowhangsplit.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhangsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
63
pico/src/patterns/flare.py
Normal file
63
pico/src/patterns/flare.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Flare:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
Flare: on the strip used by the first roll head,
|
||||
make the strip fade up to brightness over the delay time.
|
||||
|
||||
- c[0]: color1 for the first n1 LEDs
|
||||
- c[1]: color2 for the rest of the strip
|
||||
- n1: number of LEDs from the start of the strip that use color1
|
||||
- d: fade-in duration in ms (time to reach full preset brightness b)
|
||||
"""
|
||||
strips = self.driver.strips
|
||||
|
||||
# Which strip to flare: last roll head, clamped to valid range
|
||||
strip_idx = getattr(self.driver, "last_roll_head", 0)
|
||||
if strip_idx < 0 or strip_idx >= len(strips):
|
||||
strip_idx = 0
|
||||
|
||||
strip = strips[strip_idx]
|
||||
n = strip.num_leds
|
||||
|
||||
colors = preset.c
|
||||
base_c1 = colors[0] if len(colors) > 0 else (255, 255, 255)
|
||||
base_c2 = colors[1] if len(colors) > 1 else (0, 0, 0)
|
||||
|
||||
count_c1 = max(0, min(int(preset.n1), n))
|
||||
fade_ms = max(1, int(preset.d) or 1)
|
||||
target_b = int(preset.b) if hasattr(preset, "b") else 255
|
||||
|
||||
start_time = utime.ticks_ms()
|
||||
done = False
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
elapsed = utime.ticks_diff(now, start_time)
|
||||
|
||||
if not done:
|
||||
if elapsed >= fade_ms:
|
||||
factor = 1.0
|
||||
done = True
|
||||
else:
|
||||
factor = elapsed / fade_ms if fade_ms > 0 else 1.0
|
||||
else:
|
||||
factor = 1.0
|
||||
|
||||
# Effective per-preset brightness scaled over time
|
||||
current_b = int(target_b * factor)
|
||||
|
||||
# Apply global + local brightness to both colors
|
||||
c1 = self.driver.apply_brightness(base_c1, current_b)
|
||||
c2 = self.driver.apply_brightness(base_c2, current_b)
|
||||
|
||||
for i in range(n):
|
||||
strip.set(i, c1 if i < count_c1 else c2)
|
||||
strip.show()
|
||||
|
||||
yield
|
||||
9
pico/src/patterns/fluff.py
Normal file
9
pico/src/patterns/fluff.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Fluff:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/foothang.py
Normal file
9
pico/src/patterns/foothang.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Foothang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/frontbalance.py
Normal file
9
pico/src/patterns/frontbalance.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Frontbalance:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
26
pico/src/patterns/grab.py
Normal file
26
pico/src/patterns/grab.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Grab: from center of each strip, 10 LEDs each side (21 total) in purple."""
|
||||
|
||||
SPAN = 10 # LEDs on each side of center
|
||||
PURPLE = (180, 0, 255)
|
||||
|
||||
|
||||
class Grab:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
for strip_idx, strip in enumerate(strips):
|
||||
n = strip.num_leds
|
||||
mid = self.driver.strip_midpoints[strip_idx]
|
||||
strip.fill((0, 0, 0))
|
||||
start = max(0, mid - SPAN)
|
||||
end = min(n, mid + SPAN + 1)
|
||||
for i in range(start, end):
|
||||
strip.set(i, preset.c[0])
|
||||
|
||||
for strip in strips:
|
||||
strip.show()
|
||||
|
||||
while True:
|
||||
yield
|
||||
31
pico/src/patterns/hook.py
Normal file
31
pico/src/patterns/hook.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Hook: light strips n1 to n2 with a segment span long, offset from dead center."""
|
||||
|
||||
class Hook:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
midpoints = self.driver.strip_midpoints
|
||||
n1 = max(0, int(preset.n1))
|
||||
n2 = max(n1, int(preset.n2))
|
||||
span = max(0, int(preset.n3))
|
||||
offset = int(preset.n4) # positive = toward one end
|
||||
color = preset.c[0] if preset.c else (0, 0, 0)
|
||||
|
||||
for strip_idx, strip in enumerate(strips):
|
||||
strip.fill((0, 0, 0))
|
||||
if n1 <= strip_idx <= n2:
|
||||
mid = midpoints[strip_idx]
|
||||
n = strip.num_leds
|
||||
center = mid + offset
|
||||
start = max(0, center - span)
|
||||
end = min(n, center + span + 1)
|
||||
for i in range(start, end):
|
||||
strip.set(i, color)
|
||||
|
||||
for strip in strips:
|
||||
strip.show()
|
||||
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/invert.py
Normal file
9
pico/src/patterns/invert.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Invert:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/invertsplit.py
Normal file
9
pico/src/patterns/invertsplit.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Invertsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/kneehang.py
Normal file
9
pico/src/patterns/kneehang.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Kneehang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/legswoop.py
Normal file
9
pico/src/patterns/legswoop.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Legswoop:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
116
pico/src/patterns/lift.py
Normal file
116
pico/src/patterns/lift.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Lift: opposite of Spin — arms contract from the ends toward the center. Preset color, n1 = rate."""
|
||||
|
||||
import utime
|
||||
|
||||
SPAN = 10 # LEDs on each side of center (match Grab)
|
||||
LUT_SIZE = 256
|
||||
|
||||
|
||||
class Lift:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
active_indices = (0, 4)
|
||||
c0 = preset.c[0] if preset.c else (0, 0, 0)
|
||||
c1 = preset.c[1] if len(preset.c) > 1 else c0
|
||||
|
||||
lut = []
|
||||
for k in range(LUT_SIZE):
|
||||
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
|
||||
r = int(c0[0] + (c1[0] - c0[0]) * t)
|
||||
g = int(c0[1] + (c1[1] - c0[1]) * t)
|
||||
b = int(c0[2] + (c1[2] - c0[2]) * t)
|
||||
lut.append((r, g, b))
|
||||
|
||||
midpoints = self.driver.strip_midpoints
|
||||
rate = max(1, int(preset.n1) or 1)
|
||||
delay_ms = max(1, int(preset.d) or 1)
|
||||
margin = max(0, int(preset.n2) or 0)
|
||||
|
||||
left = {}
|
||||
right = {}
|
||||
for idx in active_indices:
|
||||
if 0 <= idx < len(strips):
|
||||
strip = strips[idx]
|
||||
n = strip.num_leds
|
||||
mid = midpoints[idx]
|
||||
left[idx] = margin
|
||||
right[idx] = n - margin
|
||||
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) < delay_ms:
|
||||
yield
|
||||
continue
|
||||
last_update = now
|
||||
|
||||
for idx in active_indices:
|
||||
if idx < 0 or idx >= len(strips):
|
||||
continue
|
||||
strip = strips[idx]
|
||||
n = strip.num_leds
|
||||
mid = midpoints[idx]
|
||||
|
||||
step = max(1, rate // 2) if idx == 0 else rate
|
||||
new_left = min(mid - SPAN, left[idx] + step)
|
||||
new_right = max(mid + SPAN + 1, right[idx] - step)
|
||||
|
||||
left_len = max(0, (mid - SPAN) - new_left)
|
||||
right_len = max(0, new_right - (mid + SPAN + 1))
|
||||
bright = strip.brightness
|
||||
ar = strip.ar
|
||||
|
||||
# Clear arm regions to black so contracted pixels turn off
|
||||
for i in range(margin, mid - SPAN):
|
||||
if 0 <= i < n:
|
||||
base = i * 3
|
||||
ar[base] = ar[base + 1] = ar[base + 2] = 0
|
||||
for i in range(mid + SPAN + 1, n - margin):
|
||||
if 0 <= i < n:
|
||||
base = i * 3
|
||||
ar[base] = ar[base + 1] = ar[base + 2] = 0
|
||||
|
||||
for j, i in enumerate(range(new_left, mid - SPAN)):
|
||||
if 0 <= i < n:
|
||||
t = 1 - j / (left_len - 1) if left_len > 1 else 0
|
||||
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||
r, g, b = lut[lut_idx]
|
||||
base = i * 3
|
||||
ar[base] = int(g * bright)
|
||||
ar[base + 1] = int(r * bright)
|
||||
ar[base + 2] = int(b * bright)
|
||||
|
||||
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
|
||||
if 0 <= i < n:
|
||||
t = j / (right_len - 1) if right_len > 1 else 0
|
||||
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||
r, g, b = lut[lut_idx]
|
||||
base = i * 3
|
||||
ar[base] = int(g * bright)
|
||||
ar[base + 1] = int(r * bright)
|
||||
ar[base + 2] = int(b * bright)
|
||||
|
||||
left[idx] = new_left
|
||||
right[idx] = new_right
|
||||
|
||||
strip.show()
|
||||
|
||||
# Check if all arms have contracted to center - run once, then hold
|
||||
all_done = True
|
||||
for idx in active_indices:
|
||||
if idx < 0 or idx >= len(strips):
|
||||
continue
|
||||
mid = midpoints[idx]
|
||||
if left[idx] < mid - SPAN or right[idx] > mid + SPAN + 1:
|
||||
all_done = False
|
||||
break
|
||||
if all_done:
|
||||
while True:
|
||||
yield
|
||||
return
|
||||
|
||||
yield
|
||||
68
pico/src/patterns/point.py
Normal file
68
pico/src/patterns/point.py
Normal file
@@ -0,0 +1,68 @@
|
||||
class Point:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
Point pattern: color bands defined by n ranges.
|
||||
|
||||
- n1–n2: LEDs with color1 (c[0])
|
||||
- n3–n4: LEDs with color2 (c[1])
|
||||
- n5–n6: LEDs with color3 (c[2])
|
||||
- n7–n8: LEDs with color4 (c[3])
|
||||
|
||||
All indices are along the logical ring (driver.n), inclusive ranges.
|
||||
"""
|
||||
num_leds = self.driver.num_leds
|
||||
|
||||
# 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
|
||||
c1 = self.driver.apply_brightness(colors[0], preset.b)
|
||||
c2 = self.driver.apply_brightness(colors[1], preset.b)
|
||||
c3 = self.driver.apply_brightness(colors[2], preset.b)
|
||||
c4 = self.driver.apply_brightness(colors[3], preset.b)
|
||||
|
||||
# Helper to normalize and clamp a range
|
||||
def norm_range(a, b):
|
||||
a = int(a)
|
||||
b = int(b)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
if b < 0 or a >= num_leds:
|
||||
return None
|
||||
a = max(0, a)
|
||||
b = min(num_leds - 1, b)
|
||||
if a > b:
|
||||
return None
|
||||
return a, b
|
||||
|
||||
ranges = []
|
||||
r1 = norm_range(getattr(preset, "n1", 0), getattr(preset, "n2", -1))
|
||||
if r1:
|
||||
ranges.append((r1[0], r1[1], c1))
|
||||
r2 = norm_range(getattr(preset, "n3", 0), getattr(preset, "n4", -1))
|
||||
if r2:
|
||||
ranges.append((r2[0], r2[1], c2))
|
||||
r3 = norm_range(getattr(preset, "n5", 0), getattr(preset, "n6", -1))
|
||||
if r3:
|
||||
ranges.append((r3[0], r3[1], c3))
|
||||
r4 = norm_range(getattr(preset, "n7", 0), getattr(preset, "n8", -1))
|
||||
if r4:
|
||||
ranges.append((r4[0], r4[1], c4))
|
||||
|
||||
# Static draw: last range wins on overlaps
|
||||
for i in range(num_leds):
|
||||
color = (0, 0, 0)
|
||||
for start, end, c in ranges:
|
||||
if start <= i <= end:
|
||||
color = c
|
||||
self.driver.n[i] = color
|
||||
self.driver.n.write()
|
||||
|
||||
while True:
|
||||
yield
|
||||
|
||||
75
pico/src/patterns/pose.py
Normal file
75
pico/src/patterns/pose.py
Normal file
@@ -0,0 +1,75 @@
|
||||
class Pose:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
Pose pattern: simple static bands that turn the hoop on
|
||||
within the specified n ranges, across ALL strips.
|
||||
|
||||
Uses the preset's n values as inclusive ranges over the
|
||||
logical ring (driver.n):
|
||||
|
||||
- n1–n2: color c[0]
|
||||
- n3–n4: color c[1]
|
||||
- n5–n6: color c[2]
|
||||
- n7–n8: color c[3]
|
||||
"""
|
||||
# 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
|
||||
c1 = self.driver.apply_brightness(colors[0], preset.b)
|
||||
c2 = self.driver.apply_brightness(colors[1], preset.b)
|
||||
c3 = self.driver.apply_brightness(colors[2], preset.b)
|
||||
c4 = self.driver.apply_brightness(colors[3], preset.b)
|
||||
|
||||
# Helper to normalize and clamp a range
|
||||
def norm_range(a, b, max_len):
|
||||
a = int(a)
|
||||
b = int(b)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
if b < 0 or a >= max_len:
|
||||
return None
|
||||
a = max(0, a)
|
||||
b = min(max_len - 1, b)
|
||||
if a > b:
|
||||
return None
|
||||
return a, b
|
||||
|
||||
# For Pose, apply the same ranges on EVERY strip:
|
||||
# each color band is repeated across all strips.
|
||||
for strip in self.driver.strips:
|
||||
strip_len = strip.num_leds
|
||||
|
||||
ranges = []
|
||||
r1 = norm_range(getattr(preset, "n1", 0), getattr(preset, "n2", -1), strip_len)
|
||||
if r1:
|
||||
ranges.append((r1[0], r1[1], c1))
|
||||
r2 = norm_range(getattr(preset, "n3", 0), getattr(preset, "n4", -1), strip_len)
|
||||
if r2:
|
||||
ranges.append((r2[0], r2[1], c2))
|
||||
r3 = norm_range(getattr(preset, "n5", 0), getattr(preset, "n6", -1), strip_len)
|
||||
if r3:
|
||||
ranges.append((r3[0], r3[1], c3))
|
||||
r4 = norm_range(getattr(preset, "n7", 0), getattr(preset, "n8", -1), strip_len)
|
||||
if r4:
|
||||
ranges.append((r4[0], r4[1], c4))
|
||||
|
||||
# Static draw on this strip: last range wins on overlaps
|
||||
for i in range(strip_len):
|
||||
color = (0, 0, 0)
|
||||
for start, end, c in ranges:
|
||||
if start <= i <= end:
|
||||
color = c
|
||||
strip.set(i, color)
|
||||
|
||||
# Flush all strips
|
||||
for strip in self.driver.strips:
|
||||
strip.show()
|
||||
|
||||
while True:
|
||||
yield
|
||||
@@ -1,51 +1,97 @@
|
||||
import utime
|
||||
|
||||
|
||||
def _hue_to_rgb(hue):
|
||||
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
|
||||
h = hue % 360
|
||||
x = 1 - abs((h / 60) % 2 - 1)
|
||||
if h < 60:
|
||||
r, g, b = 1, x, 0
|
||||
elif h < 120:
|
||||
r, g, b = x, 1, 0
|
||||
elif h < 180:
|
||||
r, g, b = 0, 1, x
|
||||
elif h < 240:
|
||||
r, g, b = 0, x, 1
|
||||
elif h < 300:
|
||||
r, g, b = x, 0, 1
|
||||
else:
|
||||
r, g, b = 1, 0, x
|
||||
return (int(r * 255), int(g * 255), int(b * 255))
|
||||
|
||||
|
||||
def _make_rainbow_double(num_leds, brightness=1.0):
|
||||
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len_bytes).
|
||||
DMA reads double_buf[head:head+strip_len] with no copy."""
|
||||
n = 2 * num_leds
|
||||
double_buf = bytearray(n * 3)
|
||||
for i in range(n):
|
||||
hue = (i / n) * 360 * 2
|
||||
r, g, b = _hue_to_rgb(hue)
|
||||
double_buf[i * 3] = int(g * brightness) & 0xFF
|
||||
double_buf[i * 3 + 1] = int(r * brightness) & 0xFF
|
||||
double_buf[i * 3 + 2] = int(b * brightness) & 0xFF
|
||||
strip_len_bytes = num_leds * 3
|
||||
return (double_buf, strip_len_bytes)
|
||||
|
||||
|
||||
def _ensure_buffers(driver, preset, buffers_cache):
|
||||
"""Build or refresh per-strip double buffers with current brightness. Returns (rainbow_data, cumulative_bytes)."""
|
||||
effective = (preset.b * driver.b) / (255 * 255)
|
||||
key = (preset.b, driver.b)
|
||||
if buffers_cache.get("key") == key and buffers_cache.get("data"):
|
||||
return buffers_cache["data"], buffers_cache["cumulative_bytes"]
|
||||
strips = driver.strips
|
||||
rainbow_data = [_make_rainbow_double(s.num_leds, effective) for s in strips]
|
||||
cumulative_bytes = [0]
|
||||
for s in strips:
|
||||
cumulative_bytes.append(cumulative_bytes[-1] + s.num_leds * 3)
|
||||
buffers_cache["key"] = key
|
||||
buffers_cache["data"] = rainbow_data
|
||||
buffers_cache["cumulative_bytes"] = cumulative_bytes
|
||||
return rainbow_data, cumulative_bytes
|
||||
|
||||
|
||||
class Rainbow:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
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)
|
||||
self._buffers_cache = {}
|
||||
|
||||
def run(self, preset):
|
||||
step = self.driver.step % 256
|
||||
step_amount = max(1, int(preset.n1)) # n1 controls step increment
|
||||
step_amount = max(1, int(preset.n1)) # n1 = bytes to advance per frame (speed)
|
||||
total_ring_bytes = self.driver.num_leds * 3
|
||||
# Phase in bytes; driver.step kept in 0..255 for compatibility
|
||||
phase = (self.driver.step * total_ring_bytes) // 256
|
||||
|
||||
# If auto is False, run a single step and then stop
|
||||
rainbow_data, cumulative_bytes = _ensure_buffers(
|
||||
self.driver, preset, self._buffers_cache
|
||||
)
|
||||
strips = self.driver.strips
|
||||
|
||||
def show_frame(phase):
|
||||
for i, (strip, (double_buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)):
|
||||
head = (phase + cumulative_bytes[i]) % strip_len_bytes
|
||||
strip.show(double_buf, head)
|
||||
self.driver.step = (phase * 256) // total_ring_bytes
|
||||
|
||||
# Single step then stop
|
||||
if not preset.a:
|
||||
for i in range(self.driver.num_leds):
|
||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
||||
self.driver.n[i] = self.driver.apply_brightness(self._wheel(rc_index & 255), preset.b)
|
||||
self.driver.n.write()
|
||||
# Increment step by n1 for next manual call
|
||||
self.driver.step = (step + step_amount) % 256
|
||||
# Allow tick() to advance the generator once
|
||||
show_frame(phase)
|
||||
phase = (phase + step_amount) % total_ring_bytes
|
||||
self.driver.step = (phase * 256) // total_ring_bytes
|
||||
yield
|
||||
return
|
||||
|
||||
last_update = utime.ticks_ms()
|
||||
sleep_ms = max(1, int(preset.d))
|
||||
|
||||
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.driver.num_leds):
|
||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
||||
self.driver.n[i] = self.driver.apply_brightness(
|
||||
self._wheel(rc_index & 255),
|
||||
preset.b,
|
||||
)
|
||||
self.driver.n.write()
|
||||
step = (step + step_amount) % 256
|
||||
self.driver.step = step
|
||||
rainbow_data, cumulative_bytes = _ensure_buffers(
|
||||
self.driver, preset, self._buffers_cache
|
||||
)
|
||||
show_frame(phase)
|
||||
phase = (phase + step_amount) % total_ring_bytes
|
||||
last_update = current_time
|
||||
# Yield once per tick so other logic can run
|
||||
yield
|
||||
|
||||
154
pico/src/patterns/roll.py
Normal file
154
pico/src/patterns/roll.py
Normal file
@@ -0,0 +1,154 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Roll:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Roll: moving band with gradient from color1 to color2 over the strips.
|
||||
|
||||
- n1: offset from start of strip (effective start = start + n1)
|
||||
- n2: offset from end of strip (effective end = end - n2, inclusive)
|
||||
- n3: number of full rotations before stopping (0 = infinite)
|
||||
- n4: direction (0 = clockwise, 1 = anti-clockwise)
|
||||
- c[0]: color1 at the head strip
|
||||
- c[1]: color2 at the tail strip
|
||||
"""
|
||||
colors = preset.c
|
||||
color1_raw = colors[0] if colors else (255, 255, 255)
|
||||
color2_raw = colors[1] if len(colors) > 1 else (0, 0, 0)
|
||||
color1 = self.driver.apply_brightness(color1_raw, preset.b)
|
||||
color2 = self.driver.apply_brightness(color2_raw, preset.b)
|
||||
|
||||
n_segments = self.driver.n.num_strips if hasattr(self.driver.n, "num_strips") else 1
|
||||
# Margins from the start and end of each strip
|
||||
start_margin = max(0, int(getattr(preset, "n1", 0)))
|
||||
end_margin = max(0, int(getattr(preset, "n2", 0)))
|
||||
|
||||
# Debug info to see why roll might be black
|
||||
try:
|
||||
print(
|
||||
"ROLL preset",
|
||||
"p=", getattr(preset, "p", None),
|
||||
"b=", getattr(preset, "b", None),
|
||||
"colors_raw=", color1_raw, color2_raw,
|
||||
"colors_bright=", color1, color2,
|
||||
)
|
||||
print(
|
||||
"ROLL n1..n4",
|
||||
getattr(preset, "n1", None),
|
||||
getattr(preset, "n2", None),
|
||||
getattr(preset, "n3", None),
|
||||
getattr(preset, "n4", None),
|
||||
"n_segments=", n_segments,
|
||||
"start_margin=", start_margin,
|
||||
"end_margin=", end_margin,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# n3: number of rotations (0 = infinite)
|
||||
max_rotations = int(getattr(preset, "n3", 0)) or 0
|
||||
# n4: direction (0=cw, 1=ccw); default clockwise if missing
|
||||
clockwise = int(getattr(preset, "n4", 0)) == 0
|
||||
|
||||
step = self.driver.step
|
||||
delay_ms = max(1, int(preset.d) or 1)
|
||||
last_update = utime.ticks_ms()
|
||||
rotations_done = 0
|
||||
|
||||
def scale_color(c, f):
|
||||
return tuple(int(x * f) for x in c)
|
||||
|
||||
def lerp_color(c1, c2, t):
|
||||
"""Linear gradient between two colors."""
|
||||
if t <= 0:
|
||||
return c1
|
||||
if t >= 1:
|
||||
return c2
|
||||
return (
|
||||
int(c1[0] + (c2[0] - c1[0]) * t),
|
||||
int(c1[1] + (c2[1] - c1[1]) * t),
|
||||
int(c1[2] + (c2[2] - c1[2]) * t),
|
||||
)
|
||||
|
||||
def draw(head):
|
||||
# Remember head strip for flare
|
||||
try:
|
||||
self.driver.last_roll_head = head
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
strips_list = self.driver.strips
|
||||
|
||||
for strip_idx, strip in enumerate(strips_list):
|
||||
if strip_idx < 0 or strip_idx >= n_segments:
|
||||
continue
|
||||
|
||||
# Distance from head along direction, 0..n_segments-1
|
||||
if clockwise:
|
||||
dist = (head - strip_idx) % n_segments
|
||||
else:
|
||||
dist = (strip_idx - head) % n_segments
|
||||
|
||||
# Color gradient from color1 at the head strip to color2 at the tail strip
|
||||
if n_segments > 1:
|
||||
t = dist / (n_segments - 1)
|
||||
else:
|
||||
t = 0.0
|
||||
c_strip = lerp_color(color1, color2, t)
|
||||
|
||||
n = strip.num_leds
|
||||
# Effective segment per strip:
|
||||
# start = 0 + start_margin
|
||||
# end = (n - 1) - end_margin (inclusive)
|
||||
width = n - start_margin - end_margin
|
||||
if width <= 0:
|
||||
# If margins are too large, fall back to full strip
|
||||
seg_s = 0
|
||||
seg_e = n
|
||||
else:
|
||||
seg_s = max(0, min(n, start_margin))
|
||||
seg_e = min(n, n - end_margin)
|
||||
|
||||
# Debug for first strip/head to see segment
|
||||
try:
|
||||
if strip_idx == 0 and head == 0:
|
||||
print("ROLL seg strip0 n=", n, "seg_s=", seg_s, "seg_e=", seg_e)
|
||||
except Exception:
|
||||
pass
|
||||
for i in range(n):
|
||||
if seg_s <= i < seg_e:
|
||||
strip.set(i, c_strip)
|
||||
else:
|
||||
strip.set(i, (0, 0, 0))
|
||||
strip.show()
|
||||
|
||||
if not preset.a:
|
||||
head = step % n_segments if n_segments > 0 else 0
|
||||
draw(head)
|
||||
self.driver.step = step + 1
|
||||
yield
|
||||
return
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
if utime.ticks_diff(current_time, last_update) >= delay_ms:
|
||||
head = step % n_segments if n_segments > 0 else 0
|
||||
if not clockwise and n_segments > 0:
|
||||
head = (n_segments - 1 - head)
|
||||
|
||||
draw(head)
|
||||
step += 1
|
||||
|
||||
if max_rotations > 0 and n_segments > 0 and (step % n_segments) == 0:
|
||||
rotations_done += 1
|
||||
if rotations_done >= max_rotations:
|
||||
self.driver.step = step
|
||||
last_update = current_time
|
||||
return
|
||||
|
||||
self.driver.step = step
|
||||
last_update = current_time
|
||||
yield
|
||||
9
pico/src/patterns/seat.py
Normal file
9
pico/src/patterns/seat.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Seat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
98
pico/src/patterns/spin.py
Normal file
98
pico/src/patterns/spin.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Spin: continues from Grab — segment (10 each side of center) moves slowly up to the top. Preset color, n1 = rate."""
|
||||
|
||||
import utime
|
||||
|
||||
SPAN = 10 # LEDs on each side of center (match Grab)
|
||||
LUT_SIZE = 256 # gradient lookup table entries
|
||||
|
||||
|
||||
class Spin:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
active_indices = (0, 4)
|
||||
c0 = preset.c[0]
|
||||
c1 = preset.c[1]
|
||||
|
||||
# Precompute gradient LUT: t in [0,1] maps to (r,g,b)
|
||||
lut = []
|
||||
for k in range(LUT_SIZE):
|
||||
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
|
||||
r = int(c0[0] + (c1[0] - c0[0]) * t)
|
||||
g = int(c0[1] + (c1[1] - c0[1]) * t)
|
||||
b = int(c0[2] + (c1[2] - c0[2]) * t)
|
||||
lut.append((r, g, b))
|
||||
|
||||
# For each active strip we expand from just outside the grab center
|
||||
# left: from (mid - SPAN) down to 0
|
||||
# right: from (mid + SPAN) up to end
|
||||
midpoints = self.driver.strip_midpoints
|
||||
rate = max(1, int(preset.n1) or 1)
|
||||
delay_ms = max(1, int(preset.d) or 1)
|
||||
margin = max(0, int(preset.n2) or 0)
|
||||
|
||||
# Track current extents of each arm
|
||||
left = {}
|
||||
right = {}
|
||||
for idx in active_indices:
|
||||
if 0 <= idx < len(strips):
|
||||
mid = midpoints[idx]
|
||||
left[idx] = mid - SPAN # inner edge of left arm
|
||||
right[idx] = mid + SPAN + 1 # inner edge of right arm
|
||||
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) < delay_ms:
|
||||
yield
|
||||
continue
|
||||
last_update = now
|
||||
|
||||
for idx in active_indices:
|
||||
if idx < 0 or idx >= len(strips):
|
||||
continue
|
||||
strip = strips[idx]
|
||||
n = strip.num_leds
|
||||
mid = midpoints[idx]
|
||||
|
||||
# Expand arms: inside (strip 1, idx 0) moves slower, outside (strip 5, idx 4) faster
|
||||
step = max(1, rate // 2) if idx == 0 else rate
|
||||
new_left = max(margin, left[idx] - step)
|
||||
new_right = min(n - margin, right[idx] + step)
|
||||
|
||||
# Left arm: c1 at outer, c0 at inner. Right arm: c0 at inner, c1 at outer.
|
||||
left_len = max(0, (mid - SPAN) - new_left)
|
||||
right_len = max(0, new_right - (mid + SPAN + 1))
|
||||
bright = strip.brightness
|
||||
ar = strip.ar
|
||||
|
||||
for j, i in enumerate(range(new_left, mid - SPAN)):
|
||||
if 0 <= i < n:
|
||||
t = 1 - j / (left_len - 1) if left_len > 1 else 0
|
||||
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||
r, g, b = lut[lut_idx]
|
||||
base = i * 3
|
||||
ar[base] = int(g * bright)
|
||||
ar[base + 1] = int(r * bright)
|
||||
ar[base + 2] = int(b * bright)
|
||||
|
||||
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
|
||||
if 0 <= i < n:
|
||||
t = j / (right_len - 1) if right_len > 1 else 0
|
||||
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||
r, g, b = lut[lut_idx]
|
||||
base = i * 3
|
||||
ar[base] = int(g * bright)
|
||||
ar[base + 1] = int(r * bright)
|
||||
ar[base + 2] = int(b * bright)
|
||||
|
||||
left[idx] = new_left
|
||||
right[idx] = new_right
|
||||
|
||||
# Show only on this strip
|
||||
strip.show()
|
||||
|
||||
yield
|
||||
9
pico/src/patterns/split.py
Normal file
9
pico/src/patterns/split.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Split:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
9
pico/src/patterns/straddle.py
Normal file
9
pico/src/patterns/straddle.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Straddle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
28
pico/src/patterns/test.py
Normal file
28
pico/src/patterns/test.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Test pattern: strip i has (i+1) LEDs on at the start (indices 0..i) plus the midpoint LED on. 50% red."""
|
||||
|
||||
BRIGHTNESS = 0.50
|
||||
|
||||
RED = (255, 0, 0)
|
||||
|
||||
|
||||
def _scale(color, factor):
|
||||
return tuple(int(c * factor) for c in color)
|
||||
|
||||
|
||||
class Test:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
red = _scale(RED, BRIGHTNESS)
|
||||
for strip_idx, strip in enumerate(strips):
|
||||
n = strip.num_leds
|
||||
mid = self.driver.strip_midpoints[strip_idx] # from STRIP_CONFIG
|
||||
strip.fill((0, 0, 0))
|
||||
# First (strip_idx + 1) LEDs on: indices 0..strip_idx
|
||||
for i in range(min(strip_idx + 1, n)):
|
||||
strip.set(i, red)
|
||||
# Midpoint LED on
|
||||
strip.set(mid, red)
|
||||
strip.show()
|
||||
Reference in New Issue
Block a user