Refactor presets scaling and clean up patterns

Made-with: Cursor
This commit is contained in:
2026-03-05 20:25:28 +13:00
parent 52a5f0f8c4
commit e75723e2e7
25 changed files with 100 additions and 371 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
settings.json
settings.json
__pycache__/

View File

@@ -28,6 +28,8 @@ class WS2812B:
self.brightness = brightness
self.invert = invert
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3)
self.dma.start_transfer(self.ar)
def show(self, array=None, offset=0):
if array is None:

View File

@@ -7,29 +7,11 @@ from .circle import Circle
from .roll import Roll
from .calibration import Calibration
from .test import Test
from .scale_test import ScaleTest
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

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Backbalance:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Backbend:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Backbendsplit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Beat:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Crouch:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Dismount:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Elbowhang:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Elbowhangspin:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Elbowhangsplit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Fluff:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Foothang:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Frontbalance:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Invert:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Invertsplit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Kneehang:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Legswoop:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -3,66 +3,16 @@ class Point:
self.driver = driver
def run(self, preset):
"""
Point pattern: color bands defined by n ranges.
- n1n2: LEDs with color1 (c[0])
- n3n4: LEDs with color2 (c[1])
- n5n6: LEDs with color3 (c[2])
- n7n8: 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)
# 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
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
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()

View File

@@ -0,0 +1,56 @@
import utime
RED = (255, 0, 0)
class ScaleTest:
"""
Animated test for the scale() helper.
A single red pixel moves along the reference strip (strip 0). For each other
strip, the position is mapped using:
n2 = scale(l1, l2, n1)
so that all lit pixels stay aligned by proportional position along the strips.
"""
def __init__(self, driver):
self.driver = driver
def run(self, preset):
strips = self.driver.strips
if not strips:
return
src_strip_idx = 0
l1 = self.driver.strip_length(src_strip_idx)
if l1 <= 0:
return
step = self.driver.step
delay_ms = max(1, int(getattr(preset, "d", 30) or 30))
last_update = utime.ticks_ms()
color = self.driver.apply_brightness(RED, getattr(preset, "b", 255))
while True:
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= delay_ms:
n1 = step % l1
# Clear all strips
for strip in strips:
strip.fill((0, 0, 0))
# Light mapped position on each strip using Presets.set/show
for dst_strip_idx, _ in enumerate(strips):
self.driver.set(dst_strip_idx, n1, color)
self.driver.show(dst_strip_idx)
step += 1
self.driver.step = step
last_update = now
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Seat:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Split:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Straddle:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -3,11 +3,7 @@ from ws2812 import WS2812B
from preset import Preset
from patterns import (
Blink, Rainbow, Pulse, Transition, Chase, Circle, Roll, Calibration, Test,
Grab, Spin, Lift, Flare, Hook, Invertsplit, Pose,
Backbalance, Beat, Crouch, Backbendsplit, Straddle,
Frontbalance, Elbowhang, Elbowhangspin, Dismount, Fluff,
Elbowhangsplit, Invert, Backbend, Seat, Kneehang,
Legswoop, Split, Foothang, Point,
Grab, Spin, Lift, Flare, Hook, Pose, Point,
)
import json
@@ -23,53 +19,9 @@ STRIP_CONFIG = (
(7, 291, 290 // 2-1), # 8
)
class StripRing:
"""Treat multiple WS2812B strips as one logical ring. Equal weight per strip (scale by strip length)."""
def __init__(self, strips):
self.strips = strips
self._cumul = [0]
for s in strips:
self._cumul.append(self._cumul[-1] + s.num_leds)
self.num_leds = self._cumul[-1]
self.num_strips = len(strips)
def _strip_and_local(self, i):
if i < 0 or i >= self.num_leds:
raise IndexError(i)
for s in range(len(self.strips)):
if i < self._cumul[s + 1]:
return s, i - self._cumul[s]
return len(self.strips) - 1, i - self._cumul[-2]
def __setitem__(self, i, color):
s, local = self._strip_and_local(i)
self.strips[s].set(local, color)
def fill(self, color):
for s in self.strips:
s.fill(color)
def write(self):
for s in self.strips:
s.show()
def position(self, i):
"""Normalized position 0..1 with equal span per strip (longer strips get same angular span)."""
s, local = self._strip_and_local(i)
strip_len = self.strips[s].num_leds
frac = (local / strip_len) if strip_len else 0
return (s + frac) / self.num_strips
def segment(self, i):
"""Segment index 0..num_strips-1 (strip index) so segments align with physical strips."""
s, _ = self._strip_and_local(i)
return s
class Presets:
def __init__(self):
self.scale_map = []
self.strips = []
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
@@ -80,9 +32,9 @@ class Presets:
self.strip_midpoints.append(mid)
self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0))
state_machine += 1
self.scale_map.append(self.create_scale_map(num_leds))
# Single logical strip over all 8 strips for patterns (n[i], .fill(), .write())
self.n = StripRing(self.strips)
self.num_leds = self.n.num_leds
# WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in)
self.step = 0
# Remember which strip was last used as the roll head (for flare, etc.)
@@ -112,26 +64,7 @@ class Presets:
"lift": Lift(self).run,
"flare": Flare(self).run,
"hook": Hook(self).run,
"invertsplit": Invertsplit(self).run,
"pose": Pose(self).run,
"backbalance": Backbalance(self).run,
"beat": Beat(self).run,
"crouch": Crouch(self).run,
"backbendsplit": Backbendsplit(self).run,
"straddle": Straddle(self).run,
"frontbalance": Frontbalance(self).run,
"elbowhang": Elbowhang(self).run,
"elbowhangspin": Elbowhangspin(self).run,
"dismount": Dismount(self).run,
"fluff": Fluff(self).run,
"elbowhangsplit": Elbowhangsplit(self).run,
"invert": Invert(self).run,
"backbend": Backbend(self).run,
"seat": Seat(self).run,
"kneehang": Kneehang(self).run,
"legswoop": Legswoop(self).run,
"split": Split(self).run,
"foothang": Foothang(self).run,
"point": Point(self).run,
}
@@ -143,40 +76,6 @@ class Presets:
return self.strips[strip_idx].num_leds
return 0
def strip_index_to_angle(self, strip_idx, index):
"""Map an LED index on a given strip to a normalized angle 0..1.
This accounts for different strip lengths so that the same angle value
corresponds to the same physical angle on all concentric strips.
"""
n = self.strip_length(strip_idx)
if n <= 0:
return 0.0
index = int(index) % n
return index / float(n)
def strip_angle_to_index(self, strip_idx, angle):
"""Map a normalized angle 0..1 to an LED index on a given strip.
Use this when you want patterns to align by angle instead of raw index,
despite strips having different circumferences.
"""
n = self.strip_length(strip_idx)
if n <= 0:
return 0
# Normalize angle into [0,1)
angle = float(angle)
angle = angle - int(angle)
if angle < 0.0:
angle += 1.0
return int(angle * n) % n
def remap_strip_index(self, src_strip_idx, dst_strip_idx, src_index):
"""Remap an index from one strip to another so they align by angle."""
angle = self.strip_index_to_angle(src_strip_idx, src_index)
return self.strip_angle_to_index(dst_strip_idx, angle)
def save(self):
"""Save the presets to a file."""
with open("presets.json", "w") as f:
@@ -251,14 +150,6 @@ class Presets:
self.selected = preset_name
return True
def update_num_leds(self, pin, num_leds):
num_leds = int(num_leds)
if isinstance(pin, Pin):
self.n = WS2812B(pin, num_leds)
else:
self.n = WS2812B(num_leds, int(pin), 0, brightness=1.0)
self.num_leds = num_leds
def apply_brightness(self, color, brightness_override=None):
# Combine per-preset brightness (override) with global brightness self.b
local = brightness_override if brightness_override is not None else 255
@@ -266,12 +157,6 @@ class Presets:
effective_brightness = int(local * self.b / 255)
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))
@@ -279,3 +164,27 @@ class Presets:
colors = preset.c
color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.b))
def fill(self, color):
for strip in self.strips:
strip.fill(color)
def fill_n(self, color, n1, n2):
for i in range(n1, n2):
for strip_idx in range(8):
self.set(strip_idx, i, color)
def set(self, strip, index, color):
if index >= self.strips[0].num_leds:
return False
self.strips[strip].set(self.scale_map[strip][index], color)
return True
def create_scale_map(self, num_leds):
ref_len = STRIP_CONFIG[0][1]
return [int(i * num_leds / ref_len) for i in range(ref_len)]
def show_all(self):
for strip in self.strips:
strip.show()