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:
148
pico/src/main.py
148
pico/src/main.py
@@ -1,99 +1,63 @@
|
||||
import sys
|
||||
# So "from ws2812 import WS2812B" finds pico/lib when run from device / or test/
|
||||
if "lib" not in sys.path:
|
||||
sys.path.insert(0, "lib")
|
||||
if "../lib" not in sys.path:
|
||||
sys.path.insert(0, "../lib")
|
||||
from ws2812 import WS2812B
|
||||
import time
|
||||
from machine import UART, Pin
|
||||
import json
|
||||
from presets import Presets
|
||||
import gc
|
||||
|
||||
# --- Rainbow pattern (outside ws2812): pregen double buffer, show via head offset ---
|
||||
uart = UART(0, baudrate=921600, rx=Pin(1, Pin.IN))
|
||||
|
||||
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))
|
||||
presets = Presets()
|
||||
presets.load()
|
||||
|
||||
print(presets.presets.keys())
|
||||
|
||||
def make_rainbow_double(num_leds, brightness=1.0):
|
||||
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len).
|
||||
head must be in 0..strip_len-1 so 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)
|
||||
g = int(g * brightness) & 0xFF
|
||||
r = int(r * brightness) & 0xFF
|
||||
b = int(b * brightness) & 0xFF
|
||||
o = i * 3
|
||||
double_buf[o] = g
|
||||
double_buf[o + 1] = r
|
||||
double_buf[o + 2] = b
|
||||
strip_len = num_leds * 3
|
||||
return (double_buf, strip_len)
|
||||
presets.select("off")
|
||||
|
||||
#print memory usage
|
||||
print(f"Memory usage: {gc.mem_free()/1024} kB free")
|
||||
|
||||
def show_rainbow(strip, double_buf, strip_len, head):
|
||||
"""DMA reads directly from double_buf at head; no copy. head in 0..strip_len-1."""
|
||||
strip.show(double_buf, head)
|
||||
|
||||
|
||||
# --- Strips + rainbow buffers per strip ---
|
||||
# Each strip can have a different length; buffers and phase are per-strip.
|
||||
# Strip config must match pico/src/main.py pins.
|
||||
STRIP_CONFIG = (
|
||||
(7, 291),
|
||||
(3, 290),
|
||||
(6, 283),
|
||||
(28, 278),
|
||||
(29, 275),
|
||||
(4, 270),
|
||||
(0, 283),
|
||||
(2, 290),
|
||||
)
|
||||
|
||||
strips = []
|
||||
sm = 0
|
||||
for pin, num_leds in STRIP_CONFIG:
|
||||
print(pin, num_leds)
|
||||
ws = WS2812B(num_leds, pin, sm, brightness=0.2) # 1.0 so fill() is visible
|
||||
strips.append(ws)
|
||||
sm += 1
|
||||
|
||||
# Cumulative LED count before each strip; total ring size
|
||||
cumulative_leds = [0]
|
||||
for ws in strips[:-1]:
|
||||
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
|
||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
||||
bytes_per_cycle = total_ring_leds * 3
|
||||
|
||||
# One rainbow double buffer per strip (length = 2 * num_leds for that strip)
|
||||
now = time.ticks_ms()
|
||||
rainbow_data = [make_rainbow_double(ws.num_leds, ws.brightness) for ws in strips]
|
||||
# Global phase in bytes; each strip: head = (phase + cumulative_leds[i]*3) % strip_len[i]
|
||||
print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
||||
rainbow_head = 0
|
||||
step = 3
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
now = time.ticks_ms()
|
||||
for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)):
|
||||
head = (rainbow_head + cumulative_leds[i] * 3) % strip_len
|
||||
show_rainbow(strip, double_buf, strip_len, head)
|
||||
rainbow_head = (rainbow_head + step) % bytes_per_cycle
|
||||
#print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
||||
time.sleep_ms(10)
|
||||
presets.tick()
|
||||
if uart.any():
|
||||
data = uart.readline()
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except:
|
||||
# Ignore malformed JSON lines
|
||||
continue
|
||||
|
||||
# Select a preset by name (existing behaviour)
|
||||
preset_name = data.get("select")
|
||||
if preset_name is not None:
|
||||
presets.select(preset_name)
|
||||
presets.tick()
|
||||
|
||||
# Create or update a preset:
|
||||
# {"preset_edit": {"name": "<name>", "data": {<preset_dict>}}}
|
||||
edit_payload = data.get("preset_edit")
|
||||
if isinstance(edit_payload, dict):
|
||||
name = edit_payload.get("name")
|
||||
preset_data = edit_payload.get("data") or {}
|
||||
if isinstance(name, str) and isinstance(preset_data, dict):
|
||||
# Log the incoming preset payload for debugging
|
||||
print("PRESET_EDIT", name, preset_data)
|
||||
presets.edit(name, preset_data)
|
||||
|
||||
# Delete a preset:
|
||||
# {"preset_delete": "<name>"}
|
||||
delete_name = data.get("preset_delete")
|
||||
if isinstance(delete_name, str):
|
||||
print("PRESET_DELETE", delete_name)
|
||||
presets.delete(delete_name)
|
||||
|
||||
# Persist all presets to flash:
|
||||
# {"preset_save": true}
|
||||
if data.get("preset_save"):
|
||||
print("PRESET_SAVE")
|
||||
presets.save()
|
||||
|
||||
print(data)
|
||||
gc.collect()
|
||||
#print used and free memory
|
||||
print(f"Memory usage: {gc.mem_alloc()/1024} kB used, {gc.mem_free()/1024} kB free")
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import asyncio
|
||||
import aioespnow
|
||||
import json
|
||||
|
||||
async def p2p(settings, patterns):
|
||||
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
|
||||
e.active(True)
|
||||
async for mac, msg in e:
|
||||
try:
|
||||
data = json.loads(msg)
|
||||
except:
|
||||
print(f"Failed to load espnow data {msg}")
|
||||
continue
|
||||
|
||||
if "names" not in data or settings.get("name") in data.get("names", []):
|
||||
await settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
|
||||
@@ -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()
|
||||
@@ -12,6 +12,8 @@ class Preset:
|
||||
self.n4 = 0
|
||||
self.n5 = 0
|
||||
self.n6 = 0
|
||||
self.n7 = 0
|
||||
self.n8 = 0
|
||||
|
||||
# Override defaults with provided data
|
||||
self.edit(data)
|
||||
@@ -76,4 +78,6 @@ class Preset:
|
||||
"n4": self.n4,
|
||||
"n5": self.n5,
|
||||
"n6": self.n6,
|
||||
"n7": self.n7,
|
||||
"n8": self.n8,
|
||||
}
|
||||
|
||||
618
pico/src/presets.json
Normal file
618
pico/src/presets.json
Normal file
@@ -0,0 +1,618 @@
|
||||
{
|
||||
"start": {
|
||||
"p": "off",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"grab": {
|
||||
"p": "grab",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[64,0,255]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"spin1": {
|
||||
"p": "spin",
|
||||
"d": 0,
|
||||
"b": 100,
|
||||
"c": [
|
||||
[64,0,255],
|
||||
[255,105,180]
|
||||
],
|
||||
"n1": 1,
|
||||
"n2": 20,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"lift": {
|
||||
"p": "lift",
|
||||
"d": 0,
|
||||
"b": 100,
|
||||
"c": [
|
||||
[64,0,255],
|
||||
[255,105,180]
|
||||
],
|
||||
"n1": 1,
|
||||
"n2": 20,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"flare": {
|
||||
"p": "flare",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"hook": {
|
||||
"p": "hook",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 7,
|
||||
"n3": 15,
|
||||
"n4": 15
|
||||
},
|
||||
"roll1": {
|
||||
"p": "roll",
|
||||
"d": 200,
|
||||
"b": 100,
|
||||
"c": [
|
||||
[64,0,255],
|
||||
[20,20,40]
|
||||
],
|
||||
"n1": 50,
|
||||
"n2": 160,
|
||||
"n3": 1,
|
||||
"n4": 0
|
||||
},
|
||||
"invertsplit": {
|
||||
"p": "invertsplit",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"pose1": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[255,0,0],
|
||||
[0,255,0],
|
||||
[0,0,255],
|
||||
[255,255,255]
|
||||
],
|
||||
"n1": 100,
|
||||
"n2": 150,
|
||||
"n3": 650,
|
||||
"n4": 700,
|
||||
"n5": 1200,
|
||||
"n6": 1250,
|
||||
"n7": 1750,
|
||||
"n8": 1800
|
||||
},
|
||||
"pose2": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[255,105,180],
|
||||
[64,0,255],
|
||||
[255,165,0],
|
||||
[0,255,255]
|
||||
],
|
||||
"n1": 150,
|
||||
"n2": 200,
|
||||
"n3": 700,
|
||||
"n4": 750,
|
||||
"n5": 1250,
|
||||
"n6": 1300,
|
||||
"n7": 1800,
|
||||
"n8": 1850
|
||||
},
|
||||
"roll2": {
|
||||
"p": "roll",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"backbalance1": {
|
||||
"p": "backbalance",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"beat1": {
|
||||
"p": "beat",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"pose3": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[255,255,0],
|
||||
[255,0,255],
|
||||
[0,255,255],
|
||||
[255,255,255]
|
||||
],
|
||||
"n1": 200,
|
||||
"n2": 250,
|
||||
"n3": 750,
|
||||
"n4": 800,
|
||||
"n5": 1300,
|
||||
"n6": 1350,
|
||||
"n7": 1850,
|
||||
"n8": 1900
|
||||
},
|
||||
"roll3": {
|
||||
"p": "roll",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"crouch": {
|
||||
"p": "crouch",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"pose4": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[64,0,255],
|
||||
[255,105,180],
|
||||
[255,255,255],
|
||||
[255,140,0]
|
||||
],
|
||||
"n1": 250,
|
||||
"n2": 300,
|
||||
"n3": 800,
|
||||
"n4": 850,
|
||||
"n5": 1350,
|
||||
"n6": 1400,
|
||||
"n7": 1900,
|
||||
"n8": 1950
|
||||
},
|
||||
"roll4": {
|
||||
"p": "roll",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"backbendsplit": {
|
||||
"p": "backbendsplit",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"backbalance2": {
|
||||
"p": "backbalance",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"backbalance3": {
|
||||
"p": "backbalance",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"beat2": {
|
||||
"p": "beat",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"straddle": {
|
||||
"p": "straddle",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"beat3": {
|
||||
"p": "beat",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"frontbalance1": {
|
||||
"p": "frontbalance",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"pose5": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[255,0,127],
|
||||
[0,127,255],
|
||||
[127,255,0],
|
||||
[255,255,255]
|
||||
],
|
||||
"n1": 300,
|
||||
"n2": 350,
|
||||
"n3": 850,
|
||||
"n4": 900,
|
||||
"n5": 1400,
|
||||
"n6": 1450,
|
||||
"n7": 1950,
|
||||
"n8": 2000
|
||||
},
|
||||
"pose6": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[255,80,0],
|
||||
[0,200,120],
|
||||
[80,0,255],
|
||||
[255,255,255]
|
||||
],
|
||||
"n1": 350,
|
||||
"n2": 400,
|
||||
"n3": 900,
|
||||
"n4": 950,
|
||||
"n5": 1450,
|
||||
"n6": 1500,
|
||||
"n7": 2000,
|
||||
"n8": 2050
|
||||
},
|
||||
"elbowhang": {
|
||||
"p": "elbowhang",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"elbowhangspin": {
|
||||
"p": "elbowhangspin",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"spin2": {
|
||||
"p": "spin",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"dismount": {
|
||||
"p": "dismount",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"spin3": {
|
||||
"p": "spin",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"fluff": {
|
||||
"p": "fluff",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"spin4": {
|
||||
"p": "spin",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"flare2": {
|
||||
"p": "flare",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"elbowhangsplit2": {
|
||||
"p": "elbowhangsplit",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"invert": {
|
||||
"p": "invert",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"roll5": {
|
||||
"p": "roll",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"backbend": {
|
||||
"p": "backbend",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"pose7": {
|
||||
"p": "point",
|
||||
"d": 0,
|
||||
"b": 220,
|
||||
"c": [
|
||||
[255,0,0],
|
||||
[255,165,0],
|
||||
[255,255,0],
|
||||
[255,255,255]
|
||||
],
|
||||
"n1": 400,
|
||||
"n2": 450,
|
||||
"n3": 950,
|
||||
"n4": 1000,
|
||||
"n5": 1500,
|
||||
"n6": 1550,
|
||||
"n7": 2050,
|
||||
"n8": 2100
|
||||
},
|
||||
"roll6": {
|
||||
"p": "roll",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"seat": {
|
||||
"p": "seat",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"kneehang": {
|
||||
"p": "kneehang",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"legswoop": {
|
||||
"p": "legswoop",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"split": {
|
||||
"p": "split",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"foothang": {
|
||||
"p": "foothang",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
},
|
||||
"end": {
|
||||
"p": "off",
|
||||
"d": 0,
|
||||
"b": 0,
|
||||
"c": [
|
||||
[0,0,0]
|
||||
],
|
||||
"n1": 0,
|
||||
"n2": 0,
|
||||
"n3": 0,
|
||||
"n4": 0
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,92 @@
|
||||
from machine import Pin
|
||||
from ws2812 import WS2812B
|
||||
from preset import Preset
|
||||
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
|
||||
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,
|
||||
)
|
||||
import json
|
||||
|
||||
# Order: strips[0]=physical 1 … strips[7]=physical 8. (pin, num_leds, midpoint_index).
|
||||
STRIP_CONFIG = (
|
||||
(6, 291, 291 // 2), # 1
|
||||
(29, 290, 290 // 2-1), # 2
|
||||
(3, 283, 283 // 2), # 3
|
||||
(28, 278, 278 // 2-1), # 4
|
||||
(2, 278, 275 // 2), # 5 (bottom of hoop)
|
||||
(0, 283, 278 // 2-1), # 6
|
||||
(4, 290, 283 // 2), # 7
|
||||
(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, pin, num_leds, state_machine=0):
|
||||
def __init__(self):
|
||||
|
||||
self.strips = []
|
||||
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
|
||||
|
||||
state_machine = 0
|
||||
for entry in STRIP_CONFIG:
|
||||
pin, num_leds = entry[0], entry[1]
|
||||
mid = entry[2] if len(entry) >= 3 else num_leds // 2
|
||||
self.strip_midpoints.append(mid)
|
||||
self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0))
|
||||
state_machine += 1
|
||||
# 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)
|
||||
num_leds = int(num_leds)
|
||||
if isinstance(pin, Pin):
|
||||
self.n = WS2812B(pin, num_leds) # NeoPixel-style (Pin, n)
|
||||
else:
|
||||
self.n = WS2812B(num_leds, int(pin), state_machine, brightness=1.0)
|
||||
self.num_leds = num_leds
|
||||
self.step = 0
|
||||
# Remember which strip was last used as the roll head (for flare, etc.)
|
||||
self.last_roll_head = 0
|
||||
# Global brightness (0–255), controlled via UART/JSON {"b": <value>}
|
||||
self.b = 255
|
||||
|
||||
@@ -32,8 +104,79 @@ class Presets:
|
||||
"transition": Transition(self).run,
|
||||
"chase": Chase(self).run,
|
||||
"circle": Circle(self).run,
|
||||
"roll": Roll(self).run,
|
||||
"calibration": Calibration(self).run,
|
||||
"test": Test(self).run,
|
||||
"grab": Grab(self).run,
|
||||
"spin": Spin(self).run,
|
||||
"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,
|
||||
}
|
||||
|
||||
# --- Strip geometry utilities -------------------------------------------------
|
||||
|
||||
def strip_length(self, strip_idx):
|
||||
"""Return number of LEDs for a physical strip index."""
|
||||
if 0 <= strip_idx < len(self.strips):
|
||||
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:
|
||||
@@ -87,19 +230,26 @@ class Presets:
|
||||
self.generator = None
|
||||
|
||||
def select(self, preset_name, step=None):
|
||||
if preset_name is None:
|
||||
return False
|
||||
print(f"Selecting preset: {preset_name}")
|
||||
preset = None
|
||||
pattern_key = preset_name
|
||||
if preset_name in self.presets:
|
||||
preset = self.presets[preset_name]
|
||||
if preset.p in self.patterns:
|
||||
# Set step value if explicitly provided
|
||||
if step is not None:
|
||||
self.step = step
|
||||
elif preset.p == "off" or self.selected != preset_name:
|
||||
self.step = 0
|
||||
self.generator = self.patterns[preset.p](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
|
||||
pattern_key = preset.p
|
||||
if pattern_key not in self.patterns:
|
||||
return False
|
||||
# Run by pattern name (works for saved presets and built-ins like calibration, off, test)
|
||||
if preset is None:
|
||||
preset = Preset({"p": pattern_key}) if pattern_key != "off" else None
|
||||
if step is not None:
|
||||
self.step = step
|
||||
elif pattern_key == "off" or self.selected != preset_name:
|
||||
self.step = 0
|
||||
self.generator = self.patterns[pattern_key](preset)
|
||||
self.selected = preset_name
|
||||
return True
|
||||
|
||||
def update_num_leds(self, pin, num_leds):
|
||||
num_leds = int(num_leds)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import json
|
||||
import ubinascii
|
||||
import machine
|
||||
|
||||
class Settings(dict):
|
||||
SETTINGS_FILE = "/settings.json"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.load() # Load settings from file during initialization
|
||||
self.color_order = self.get_color_order(self["color_order"])
|
||||
|
||||
def _default_name(self):
|
||||
"""Device name: use unique_id on Pico (no WiFi); use AP MAC on ESP32."""
|
||||
try:
|
||||
import network
|
||||
mac = network.WLAN(network.AP_IF).config("mac")
|
||||
return "led-%s" % ubinascii.hexlify(mac).decode()
|
||||
except Exception:
|
||||
return "led-%s" % ubinascii.hexlify(machine.unique_id()).decode()
|
||||
|
||||
def set_defaults(self):
|
||||
self["led_pin"] = 10
|
||||
self["num_leds"] = 50
|
||||
self["color_order"] = "rgb"
|
||||
self["name"] = self._default_name()
|
||||
self["debug"] = False
|
||||
self["startup_preset"] = None
|
||||
self["brightness"] = 255
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
j = json.dumps(self)
|
||||
with open(self.SETTINGS_FILE, 'w') as file:
|
||||
file.write(j)
|
||||
print("Settings saved successfully.")
|
||||
except Exception as e:
|
||||
print(f"Error saving settings: {e}")
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
with open(self.SETTINGS_FILE, 'r') as file:
|
||||
loaded_settings = json.load(file)
|
||||
self.update(loaded_settings)
|
||||
print("Settings loaded successfully.")
|
||||
except Exception as e:
|
||||
print(f"Error loading settings")
|
||||
self.set_defaults()
|
||||
self.save()
|
||||
|
||||
|
||||
def get_color_order(self, color_order):
|
||||
"""Convert color order string to tuple of hex string indices."""
|
||||
color_orders = {
|
||||
"rgb": (1, 3, 5),
|
||||
"rbg": (1, 5, 3),
|
||||
"grb": (3, 1, 5),
|
||||
"gbr": (3, 5, 1),
|
||||
"brg": (5, 1, 3),
|
||||
"bgr": (5, 3, 1)
|
||||
}
|
||||
return color_orders.get(color_order.lower(), (1, 3, 5)) # Default to RGB
|
||||
|
||||
def get_rgb_channel_order(self, color_order=None):
|
||||
"""Convert color order string to RGB channel indices for reordering tuples.
|
||||
Returns tuple of channel indices: (r_channel, g_channel, b_channel)
|
||||
Example: 'grb' -> (1, 0, 2) means (G, R, B)"""
|
||||
if color_order is None:
|
||||
color_order = self.get("color_order", "rgb")
|
||||
color_order = color_order.lower()
|
||||
# Map hex string positions to RGB channel indices
|
||||
# Position 1 (R in hex) -> channel 0, Position 3 (G) -> channel 1, Position 5 (B) -> channel 2
|
||||
hex_to_channel = {1: 0, 3: 1, 5: 2}
|
||||
hex_indices = self.get_color_order(color_order)
|
||||
return tuple(hex_to_channel[pos] for pos in hex_indices)
|
||||
|
||||
# Example usage
|
||||
def main():
|
||||
settings = Settings()
|
||||
print(f"Number of LEDs: {settings['num_leds']}")
|
||||
settings['num_leds'] = 100
|
||||
print(f"Updated number of LEDs: {settings['num_leds']}")
|
||||
settings.save()
|
||||
|
||||
# Create a new Settings object to test loading
|
||||
new_settings = Settings()
|
||||
print(f"Loaded number of LEDs: {new_settings['num_leds']}")
|
||||
print(settings)
|
||||
|
||||
|
||||
|
||||
# Run the example
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user