feat(patterns): add expanded animation pack with smoke tests

Add a broad set of new pattern modules and matching pattern smoke scripts so the new effects can be validated directly on-device.
This commit is contained in:
2026-04-23 20:10:01 +12:00
parent 4575ef16ad
commit 74b4b495f9
40 changed files with 1594 additions and 0 deletions

31
src/patterns/aurora.py Normal file
View File

@@ -0,0 +1,31 @@
import utime
class Aurora:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(40, 200, 140), (80, 120, 255), (160, 80, 220)]
bands = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
shimmer = max(0, min(255, int(preset.n2) if int(preset.n2) > 0 else 40))
phase = self.driver.step % 256
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
idx = ((i * bands) // max(1, self.driver.num_leds) + (phase // 32)) % len(colors)
c = self.driver.apply_brightness(colors[idx], preset.b)
w = (255 - abs(128 - ((i * 8 + phase) & 255)) * 2)
w = max(0, min(255, w + shimmer))
self.driver.n[i] = ((c[0]*w)//255, (c[1]*w)//255, (c[2]*w)//255)
self.driver.n.write()
phase = (phase + 1) & 255
self.driver.step = phase
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

21
src/patterns/bar_graph.py Normal file
View File

@@ -0,0 +1,21 @@
import utime
class BarGraph:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(0, 255, 0), (255, 80, 0)]
level = max(0, min(100, int(preset.n1) if int(preset.n1) >= 0 else 50))
target = (self.driver.num_leds * level) // 100
lit = self.driver.apply_brightness(colors[0], preset.b)
unlit = self.driver.apply_brightness(colors[1] if len(colors) > 1 else (0, 0, 0), preset.b)
while True:
for i in range(self.driver.num_leds):
self.driver.n[i] = lit if i < target else unlit
self.driver.n.write()
yield
if not preset.a:
return
utime.sleep_ms(max(1, int(preset.d)))

View File

@@ -0,0 +1,40 @@
import utime
class BreathingDual:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 0, 140), (0, 120, 255)]
phase_offset = max(0, min(255, int(preset.n1)))
ease = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
phase = self.driver.step % 256
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
p1 = phase
p2 = (phase + phase_offset) & 255
t1 = 255 - abs(128 - p1) * 2
t2 = 255 - abs(128 - p2) * 2
if ease > 1:
t1 = (t1 * t1) // 255
t2 = (t2 * t2) // 255
c1 = self.driver.apply_brightness(colors[0], preset.b)
c2 = self.driver.apply_brightness(colors[1 % len(colors)] if len(colors) > 1 else colors[0], preset.b)
half = self.driver.num_leds // 2
for i in range(self.driver.num_leds):
if i < half:
self.driver.n[i] = ((c1[0]*t1)//255, (c1[1]*t1)//255, (c1[2]*t1)//255)
else:
self.driver.n[i] = ((c2[0]*t2)//255, (c2[1]*t2)//255, (c2[2]*t2)//255)
self.driver.n.write()
phase = (phase + 2) & 255
self.driver.step = phase
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,33 @@
import utime
class ClockSweep:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255), (60, 60, 60)]
width = max(1, int(preset.n1) if int(preset.n1) > 0 else 1)
marker = max(0, int(preset.n2) if int(preset.n2) > 0 else 0)
pos = self.driver.step % max(1, self.driver.num_leds)
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
bg = self.driver.apply_brightness(colors[1] if len(colors) > 1 else (0, 0, 0), preset.b)
fg = self.driver.apply_brightness(colors[0], preset.b)
for i in range(self.driver.num_leds):
self.driver.n[i] = bg
if marker > 0 and i % marker == 0:
self.driver.n[i] = ((bg[0]*2)//3, (bg[1]*2)//3, (bg[2]*2)//3)
for w in range(width):
self.driver.n[(pos + w) % self.driver.num_leds] = fg
self.driver.n.write()
pos = (pos + 1) % max(1, self.driver.num_leds)
self.driver.step = pos
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,43 @@
import utime
class CometDual:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255)]
tail = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
gap = max(0, int(preset.n3))
p1 = 0
p2 = self.driver.num_leds - 1 - gap
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
self.driver.n[i] = (0, 0, 0)
c1 = self.driver.apply_brightness(colors[0 % len(colors)], preset.b)
c2 = self.driver.apply_brightness(colors[1 % len(colors)] if len(colors) > 1 else colors[0], preset.b)
for t in range(tail):
i1 = p1 - t
if 0 <= i1 < self.driver.num_leds:
s = (255 * (tail - t)) // max(1, tail)
self.driver.n[i1] = ((c1[0]*s)//255, (c1[1]*s)//255, (c1[2]*s)//255)
i2 = p2 + t
if 0 <= i2 < self.driver.num_leds:
s = (255 * (tail - t)) // max(1, tail)
self.driver.n[i2] = ((c2[0]*s)//255, (c2[1]*s)//255, (c2[2]*s)//255)
self.driver.n.write()
p1 += speed
p2 -= speed
if p1 - tail > self.driver.num_leds and p2 + tail < 0:
p1 = 0
p2 = self.driver.num_leds - 1 - gap
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

34
src/patterns/fireflies.py Normal file
View File

@@ -0,0 +1,34 @@
import random
import utime
class Fireflies:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 210, 80), (120, 255, 120)]
count = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 8)
bugs = [[random.randint(0, max(0, self.driver.num_leds - 1)), random.randint(0, 255)] for _ in range(count)]
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
self.driver.n[i] = (0, 0, 0)
for b in bugs:
idx, ph = b
tri = 255 - abs(128 - ph) * 2
c = self.driver.apply_brightness(colors[idx % len(colors)], preset.b)
self.driver.n[idx] = ((c[0]*tri)//255, (c[1]*tri)//255, (c[2]*tri)//255)
b[1] = (ph + speed) & 255
if random.randint(0, 31) == 0:
b[0] = random.randint(0, max(0, self.driver.num_leds - 1))
self.driver.n.write()
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,57 @@
import utime
class GradientScroll:
def __init__(self, driver):
self.driver = driver
def _render(self, colors, phase, brightness):
num_leds = self.driver.num_leds
color_count = len(colors)
if num_leds <= 0 or color_count <= 0:
return
if color_count == 1:
self.driver.fill(self.driver.apply_brightness(colors[0], brightness))
return
full_span = color_count * 256
phase_shift = (phase * full_span) // 256
for i in range(num_leds):
pos = ((i * full_span) // num_leds + phase_shift) % full_span
idx = pos // 256
frac = pos & 255
c1 = colors[idx]
c2 = colors[(idx + 1) % color_count]
blended = (
c1[0] + ((c2[0] - c1[0]) * frac) // 256,
c1[1] + ((c2[1] - c1[1]) * frac) // 256,
c1[2] + ((c2[2] - c1[2]) * frac) // 256,
)
self.driver.n[i] = self.driver.apply_brightness(blended, brightness)
self.driver.n.write()
def run(self, preset):
"""Scrolling blended gradient.
n1: phase step amount (default 1)
"""
colors = preset.c if preset.c else [(255, 0, 0), (0, 0, 255)]
phase = self.driver.step % 256
step_amount = max(1, int(preset.n1) if int(preset.n1) > 0 else 1)
last_update = utime.ticks_ms()
while True:
delay_ms = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= delay_ms:
self._render(colors, phase, preset.b)
phase = (phase + step_amount) % 256
self.driver.step = phase
last_update = utime.ticks_add(last_update, delay_ms)
if not preset.a:
yield
return
yield

25
src/patterns/heartbeat.py Normal file
View File

@@ -0,0 +1,25 @@
import utime
class Heartbeat:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 0, 40)]
p1 = max(20, int(preset.n1) if int(preset.n1) > 0 else 120)
p2 = max(20, int(preset.n2) if int(preset.n2) > 0 else 80)
pause = max(20, int(preset.n3) if int(preset.n3) > 0 else 500)
while True:
c = self.driver.apply_brightness(colors[0], preset.b)
self.driver.fill(c)
utime.sleep_ms(p1)
self.driver.fill((0, 0, 0))
utime.sleep_ms(max(20, int(preset.d)))
self.driver.fill(c)
utime.sleep_ms(p2)
self.driver.fill((0, 0, 0))
utime.sleep_ms(pause)
yield
if not preset.a:
return

30
src/patterns/marquee.py Normal file
View File

@@ -0,0 +1,30 @@
import utime
class Marquee:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255)]
on_len = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
off_len = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
step = max(1, int(preset.n3) if int(preset.n3) > 0 else 1)
phase = self.driver.step % (on_len + off_len)
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
c = self.driver.apply_brightness(colors[0], preset.b)
for i in range(self.driver.num_leds):
m = (i + phase) % (on_len + off_len)
self.driver.n[i] = c if m < on_len else (0, 0, 0)
self.driver.n.write()
phase = (phase + step) % (on_len + off_len)
self.driver.step = phase
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,62 @@
import utime
class MeteorRain:
def __init__(self, driver):
self.driver = driver
def _fade(self, color, fade_amount):
return (
(color[0] * fade_amount) // 255,
(color[1] * fade_amount) // 255,
(color[2] * fade_amount) // 255,
)
def run(self, preset):
"""Single meteor with a fading tail.
n1: tail length (default 8)
n2: speed in LEDs per frame (default 1)
n3: fade amount per frame, 1..255 (default 192)
"""
colors = preset.c if preset.c else [(255, 255, 255)]
color_index = 0
head = 0
direction = 1
last_update = utime.ticks_ms()
while True:
delay_ms = max(1, int(preset.d))
tail_len = max(1, int(preset.n1) if int(preset.n1) > 0 else 8)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
fade_amount = int(preset.n3) if int(preset.n3) > 0 else 192
fade_amount = max(1, min(255, fade_amount))
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= delay_ms:
for i in range(self.driver.num_leds):
self.driver.n[i] = self._fade(self.driver.n[i], fade_amount)
base = colors[color_index % len(colors)]
lit = self.driver.apply_brightness(base, preset.b)
if 0 <= head < self.driver.num_leds:
self.driver.n[head] = lit
self.driver.n.write()
head += direction * speed
if head >= self.driver.num_leds + tail_len:
head = self.driver.num_leds - 1
direction = -1
color_index += 1
elif head < -tail_len:
head = 0
direction = 1
color_index += 1
last_update = utime.ticks_add(last_update, delay_ms)
if not preset.a:
yield
return
yield

30
src/patterns/orbit.py Normal file
View File

@@ -0,0 +1,30 @@
import utime
class Orbit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255), (0, 180, 255), (255, 0, 120)]
orbits = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
phase = self.driver.step % 256
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
self.driver.n[i] = (0, 0, 0)
for k in range(orbits):
idx = ((phase * (k + 1)) // 8 + (k * self.driver.num_leds // max(1, orbits))) % max(1, self.driver.num_leds)
self.driver.n[idx] = self.driver.apply_brightness(colors[k % len(colors)], preset.b)
self.driver.n.write()
phase = (phase + speed) & 255
self.driver.step = phase
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,76 @@
import utime
class PaletteMorph:
def __init__(self, driver):
self.driver = driver
def _blend(self, c1, c2, t):
return (
c1[0] + ((c2[0] - c1[0]) * t) // 255,
c1[1] + ((c2[1] - c1[1]) * t) // 255,
c1[2] + ((c2[2] - c1[2]) * t) // 255,
)
def run(self, preset):
"""Living color field (non-scrolling palette warp).
Different from `colour_cycle`: this does not scroll a fixed gradient.
Instead, each LED breathes/warps through the palette with local phase
offsets so the strip looks alive.
n1: morph duration (ms)
n2: warp rate
n3: spatial turbulence amount
"""
colors = preset.c if preset.c else [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
if len(colors) < 2:
while True:
self.driver.fill(self.driver.apply_brightness(colors[0], preset.b))
yield
morph = max(50, int(preset.n1) if int(preset.n1) > 0 else 1200)
warp_rate = max(1, int(preset.n2) if int(preset.n2) > 0 else 3)
turbulence = max(1, int(preset.n3) if int(preset.n3) > 0 else 24)
base_idx = 0
start = utime.ticks_ms()
phase = self.driver.step % 256
while True:
now = utime.ticks_ms()
age = utime.ticks_diff(now, start)
if age < morph:
t = (age * 255) // morph
else:
t = 255
# Global morph anchor between neighboring palette colors.
a = colors[base_idx % len(colors)]
b = colors[(base_idx + 1) % len(colors)]
anchor = self._blend(a, b, t)
for i in range(self.driver.num_leds):
# Non-linear local warp per LED to create "living" motion.
pos = (i * 256) // max(1, self.driver.num_leds)
wobble = ((pos * turbulence) // 32 + phase + (t // 2)) & 255
breath = 255 - abs(128 - wobble) * 2
local = (pos + (breath // 3) + (t // 4)) % 256
idx = (base_idx + ((local * len(colors)) // 256)) % len(colors)
frac = (local * len(colors)) & 255
c1 = colors[idx]
c2 = colors[(idx + 1) % len(colors)]
grad = self._blend(c1, c2, frac)
# Blend with anchor to keep coherent palette morphing.
out = self._blend(grad, anchor, 80)
self.driver.n[i] = self.driver.apply_brightness(out, preset.b)
self.driver.n.write()
if age >= morph:
base_idx = (base_idx + 1) % len(colors)
start = now
if not preset.a:
yield
return
phase = (phase + warp_rate) & 255
self.driver.step = phase
utime.sleep_ms(max(1, int(preset.d)))
yield

39
src/patterns/plasma.py Normal file
View File

@@ -0,0 +1,39 @@
import utime
class Plasma:
def __init__(self, driver):
self.driver = driver
def _wheel(self, pos):
if pos < 85:
return (pos * 3, 255 - pos * 3, 0)
if pos < 170:
pos -= 85
return (255 - pos * 3, 0, pos * 3)
pos -= 170
return (0, pos * 3, 255 - pos * 3)
def run(self, preset):
scale = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
contrast = max(1, int(preset.n3) if int(preset.n3) > 0 else 2)
t = self.driver.step % 256
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
v = ((i * scale + t) & 255)
v2 = (((i * scale // max(1, contrast)) - (t * 2)) & 255)
c = self._wheel((v + v2) & 255)
self.driver.n[i] = self.driver.apply_brightness(c, preset.b)
self.driver.n.write()
t = (t + speed) % 256
self.driver.step = t
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,40 @@
import random
import utime
class RainDrops:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(120, 180, 255)]
rate = max(1, int(preset.n1) if int(preset.n1) > 0 else 32)
width = max(1, int(preset.n2) if int(preset.n2) > 0 else 3)
drops = []
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
self.driver.n[i] = (0, 0, 0)
if random.randint(0, 255) < rate:
drops.append([random.randint(0, max(0, self.driver.num_leds - 1)), 0])
nd = []
for pos, age in drops:
for off in range(-width, width + 1):
idx = pos + off
if 0 <= idx < self.driver.num_leds:
s = 255 - min(255, abs(off) * 255 // max(1, width + 1) + age * 40)
base = self.driver.apply_brightness(colors[age % len(colors)], preset.b)
self.driver.n[idx] = ((base[0]*s)//255, (base[1]*s)//255, (base[2]*s)//255)
age += 1
if age < 8:
nd.append([pos, age])
drops = nd
self.driver.n.write()
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

66
src/patterns/scanner.py Normal file
View File

@@ -0,0 +1,66 @@
import utime
class Scanner:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Classic scanner eye with soft falloff.
n1: eye width (default 4)
n2: end pause in frames (default 0)
"""
colors = preset.c if preset.c else [(255, 0, 0)]
color_index = 0
center = 0
direction = 1
pause_frames = 0
last_update = utime.ticks_ms()
while True:
delay_ms = max(1, int(preset.d))
width = max(1, int(preset.n1) if int(preset.n1) > 0 else 4)
end_pause = max(0, int(preset.n2))
now = utime.ticks_ms()
if utime.ticks_diff(now, last_update) >= delay_ms:
base = colors[color_index % len(colors)]
base = self.driver.apply_brightness(base, preset.b)
for i in range(self.driver.num_leds):
dist = i - center
if dist < 0:
dist = -dist
if dist > width:
self.driver.n[i] = (0, 0, 0)
else:
scale = ((width - dist) * 255) // max(1, width)
self.driver.n[i] = (
(base[0] * scale) // 255,
(base[1] * scale) // 255,
(base[2] * scale) // 255,
)
self.driver.n.write()
if pause_frames > 0:
pause_frames -= 1
else:
center += direction
if center >= self.driver.num_leds - 1:
center = self.driver.num_leds - 1
direction = -1
pause_frames = end_pause
color_index += 1
elif center <= 0:
center = 0
direction = 1
pause_frames = end_pause
color_index += 1
last_update = utime.ticks_add(last_update, delay_ms)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,44 @@
import utime
class SegmentChase:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""Independent moving segments (distinct from classic two-color chase).
n1: segment size (LEDs per segment)
n2: step size (phase increment each frame)
n3: per-segment phase offset
n4: gap spacing inside segment (0 = solid segment)
"""
colors = preset.c if preset.c else [(255, 0, 0), (0, 0, 255)]
seg = max(1, int(preset.n1) if int(preset.n1) > 0 else 4)
phase_step = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
seg_offset = max(0, int(preset.n3))
gap = max(0, int(preset.n4))
phase = self.driver.step % 256
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
seg_idx = i // seg
in_seg = i % seg
local_phase = (phase + seg_idx * seg_offset) % seg
lit_idx = (in_seg + local_phase) % seg
if gap > 0 and lit_idx >= max(1, seg - gap):
self.driver.n[i] = (0, 0, 0)
else:
color_idx = seg_idx % len(colors)
self.driver.n[i] = self.driver.apply_brightness(colors[color_idx], preset.b)
self.driver.n.write()
phase = (phase + phase_step) % seg
self.driver.step = phase
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

36
src/patterns/snowfall.py Normal file
View File

@@ -0,0 +1,36 @@
import random
import utime
class Snowfall:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255), (180, 220, 255)]
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 20)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
flakes = []
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
if random.randint(0, 255) < density:
flakes.append([self.driver.num_leds - 1, random.randint(0, len(colors)-1)])
for i in range(self.driver.num_leds):
self.driver.n[i] = (0, 0, 0)
nf = []
for pos, ci in flakes:
if 0 <= pos < self.driver.num_leds:
self.driver.n[pos] = self.driver.apply_brightness(colors[ci], preset.b)
pos -= speed
if pos >= -1:
nf.append([pos, ci])
flakes = nf
self.driver.n.write()
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,31 @@
import random
import utime
class SparkleTrail:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(120, 120, 255)]
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 24)
decay = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 210))
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
for i in range(self.driver.num_leds):
r,g,b = self.driver.n[i]
self.driver.n[i] = ((r*decay)//255, (g*decay)//255, (b*decay)//255)
sparks = max(1, self.driver.num_leds * density // 255)
for _ in range(sparks):
idx = random.randint(0, max(0, self.driver.num_leds - 1))
c = self.driver.apply_brightness(colors[random.randint(0, len(colors)-1)], preset.b)
self.driver.n[idx] = c
self.driver.n.write()
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,24 @@
import utime
class StrobeBurst:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255)]
count = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
gap = max(1, int(preset.n2) if int(preset.n2) > 0 else 60)
cooldown = max(1, int(preset.n3) if int(preset.n3) > 0 else 400)
c = self.driver.apply_brightness(colors[0], preset.b)
while True:
for _ in range(count):
self.driver.fill(c)
utime.sleep_ms(max(1, int(preset.d)//2))
self.driver.fill((0, 0, 0))
utime.sleep_ms(gap)
yield
utime.sleep_ms(cooldown)
yield
if not preset.a:
return

32
src/patterns/wave.py Normal file
View File

@@ -0,0 +1,32 @@
import utime
class Wave:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(0, 180, 255)]
wavelength = max(2, int(preset.n1) if int(preset.n1) > 0 else 12)
amp = max(0, min(255, int(preset.n2) if int(preset.n2) > 0 else 180))
drift = max(1, int(preset.n3) if int(preset.n3) > 0 else 1)
phase = self.driver.step % 256
last = utime.ticks_ms()
while True:
d = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d:
base = self.driver.apply_brightness(colors[0], preset.b)
for i in range(self.driver.num_leds):
x = (i * 256 // wavelength + phase) & 255
tri = 255 - abs(128 - x) * 2
s = (tri * amp) // 255
self.driver.n[i] = ((base[0]*s)//255, (base[1]*s)//255, (base[2]*s)//255)
self.driver.n.write()
phase = (phase + drift) % 256
self.driver.step = phase
last = utime.ticks_add(last, d)
if not preset.a:
yield
return
yield

40
tests/patterns/aurora.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_aurora", {
"p": "aurora",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_aurora")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_bar_graph", {
"p": "bar_graph",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_bar_graph")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_breathing_dual", {
"p": "breathing_dual",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_breathing_dual")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_clock_sweep", {
"p": "clock_sweep",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_clock_sweep")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_comet_dual", {
"p": "comet_dual",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_comet_dual")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_fireflies", {
"p": "fireflies",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_fireflies")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
print("Test gradient_scroll")
p.edit("gradient_test", {
"p": "gradient_scroll",
"b": 220,
"d": 60,
"c": [(255, 0, 0), (0, 255, 0), (0, 0, 255)],
"n1": 2,
"a": True,
})
p.select("gradient_test")
run_for(p, wdt, 4000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_heartbeat", {
"p": "heartbeat",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_heartbeat")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

40
tests/patterns/marquee.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_marquee", {
"p": "marquee",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_marquee")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
print("Test meteor_rain")
p.edit("meteor_test", {
"p": "meteor_rain",
"b": 200,
"d": 40,
"c": [(255, 80, 0), (0, 120, 255)],
"n1": 10,
"n2": 1,
"n3": 200,
"a": True,
})
p.select("meteor_test")
run_for(p, wdt, 4000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

40
tests/patterns/orbit.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_orbit", {
"p": "orbit",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_orbit")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_palette_morph", {
"p": "palette_morph",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_palette_morph")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

40
tests/patterns/plasma.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_plasma", {
"p": "plasma",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_plasma")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_rain_drops", {
"p": "rain_drops",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_rain_drops")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

40
tests/patterns/scanner.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
print("Test scanner")
p.edit("scanner_test", {
"p": "scanner",
"b": 255,
"d": 30,
"c": [(255, 0, 0)],
"n1": 4,
"n2": 2,
"a": True,
})
p.select("scanner_test")
run_for(p, wdt, 4000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_segment_chase", {
"p": "segment_chase",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_segment_chase")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_snowfall", {
"p": "snowfall",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_snowfall")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_sparkle_trail", {
"p": "sparkle_trail",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_sparkle_trail")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_strobe_burst", {
"p": "strobe_burst",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_strobe_burst")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()

40
tests/patterns/wave.py Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets, run_tick
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
run_tick(p)
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 30))
wdt = WDT(timeout=10000)
p.edit("test_wave", {
"p": "wave",
"b": 200,
"d": 60,
"c": [(255, 0, 0), (0, 0, 255), (0, 255, 0)],
"n1": 4,
"n2": 2,
"n3": 120,
"a": True,
})
p.select("test_wave")
run_for(p, wdt, 3000)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 100)
if __name__ == "__main__":
main()